2018-01-31 10:26:37 +00:00
|
|
|
|
import re
|
2022-01-06 11:06:55 +00:00
|
|
|
|
from abc import ABC, abstractmethod
|
2018-02-14 14:35:16 +00:00
|
|
|
|
|
2018-02-20 11:22:17 +00:00
|
|
|
|
from wtforms import ValidationError
|
2017-02-13 13:11:29 +00:00
|
|
|
|
|
2020-06-16 18:04:29 +01:00
|
|
|
|
from app.main._commonly_used_passwords import commonly_used_passwords
|
2021-01-06 12:31:39 +00:00
|
|
|
|
from app.models.spreadsheet import Spreadsheet
|
2021-06-09 13:19:05 +01:00
|
|
|
|
from app.utils.user import is_gov_user
|
2024-05-16 10:37:37 -04:00
|
|
|
|
from notifications_utils.field import Field
|
|
|
|
|
|
from notifications_utils.formatters import formatted_list
|
|
|
|
|
|
from notifications_utils.recipients import InvalidEmailError, validate_email_address
|
|
|
|
|
|
from notifications_utils.sanitise_text import SanitiseSMS
|
2015-12-01 15:51:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
2020-06-16 18:04:29 +01:00
|
|
|
|
class CommonlyUsedPassword:
|
2015-12-01 15:51:09 +00:00
|
|
|
|
def __init__(self, message=None):
|
|
|
|
|
|
if not message:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
message = "Password is in list of commonly used passwords."
|
2015-12-01 15:51:09 +00:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2020-06-16 18:04:29 +01:00
|
|
|
|
if field.data in commonly_used_passwords:
|
2015-12-01 15:51:09 +00:00
|
|
|
|
raise ValidationError(self.message)
|
2016-01-07 12:43:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
class CsvFileValidator:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def __init__(self, message="Not a csv file"):
|
2016-01-11 15:00:51 +00:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
Accept common spreadsheet formats, not just CSV
We require users to export their spreadsheets as CSV files before
uploading them. But this seems like the sort of thing a computer should
be able to do.
So this commit adds a wrapper class which:
- takes a the uploaded file
- returns it in a normalised format, or reads it using pyexcel[1]
- gives the data back in CSV format
This allows us to accept `.csv`, `.xlsx`, `.xls` (97 and 95), `.ods`,
`.xlsm` and `.tsv` files. We can upload the resultant CSV just like
normal, and process it for errors as before.
Testing
---
To test this I’ve added a selection of common spreadsheet files as test
data. They all contain the same data, so the tests look to see that the
resultant CSV output is the same for each.
UI changes
---
This commit doesn’t change the UI, apart from to give a different error
message if a user uploads a file type that we still don’t understand.
I intend to do this as a separate pull request, in order to fulfil
https://www.pivotaltracker.com/story/show/119371637
2016-05-05 15:41:11 +01:00
|
|
|
|
if not Spreadsheet.can_handle(field.data.filename):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
"{} is not a spreadsheet that Notify can read".format(
|
|
|
|
|
|
field.data.filename
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2016-03-18 12:05:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
class ValidGovEmail:
|
2016-03-18 12:05:50 +00:00
|
|
|
|
def __call__(self, form, field):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if field.data == "":
|
2018-12-07 12:23:12 +00:00
|
|
|
|
return
|
|
|
|
|
|
|
2016-10-28 10:45:05 +01:00
|
|
|
|
from flask import url_for
|
2023-08-25 09:12:23 -07:00
|
|
|
|
|
|
|
|
|
|
message = """
|
2020-03-26 12:41:46 +00:00
|
|
|
|
Enter a public sector email address or
|
2023-08-08 16:19:17 -04:00
|
|
|
|
<a class="usa-link" href="{}">find out who can use Notify</a>
|
2023-08-25 09:12:23 -07:00
|
|
|
|
""".format(
|
|
|
|
|
|
url_for("main.features")
|
|
|
|
|
|
)
|
2016-10-28 10:45:05 +01:00
|
|
|
|
if not is_gov_user(field.data.lower()):
|
2016-03-18 12:05:50 +00:00
|
|
|
|
raise ValidationError(message)
|
2016-04-07 16:02:06 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-04-22 17:49:03 +01:00
|
|
|
|
class ValidEmail:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
message = "Enter a valid email address"
|
2018-02-14 14:35:16 +00:00
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2021-11-30 17:17:16 +00:00
|
|
|
|
if not field.data:
|
2018-12-07 12:23:12 +00:00
|
|
|
|
return
|
|
|
|
|
|
|
2018-02-14 14:35:16 +00:00
|
|
|
|
try:
|
|
|
|
|
|
validate_email_address(field.data)
|
|
|
|
|
|
except InvalidEmailError:
|
|
|
|
|
|
raise ValidationError(self.message)
|
2018-12-07 12:23:12 +00:00
|
|
|
|
|
2018-02-14 14:35:16 +00:00
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
class NoCommasInPlaceHolders:
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def __init__(self, message="You cannot put commas between double brackets"):
|
2016-04-07 16:02:06 +01:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if "," in "".join(Field(field.data).placeholders):
|
2016-04-07 16:02:06 +01:00
|
|
|
|
raise ValidationError(self.message)
|
2017-02-13 13:11:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
2022-01-06 11:06:55 +00:00
|
|
|
|
class NoElementInSVG(ABC):
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def element(self):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
|
def message(self):
|
|
|
|
|
|
pass
|
2020-03-04 14:25:14 +00:00
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2022-01-06 11:06:55 +00:00
|
|
|
|
svg_contents = field.data.stream.read().decode("utf-8")
|
2020-03-04 17:26:19 +00:00
|
|
|
|
field.data.stream.seek(0)
|
2023-08-25 09:12:23 -07:00
|
|
|
|
if f"<{self.element}" in svg_contents.lower():
|
2020-03-04 14:25:14 +00:00
|
|
|
|
raise ValidationError(self.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-01-06 11:06:55 +00:00
|
|
|
|
class NoEmbeddedImagesInSVG(NoElementInSVG):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
element = "image"
|
|
|
|
|
|
message = "This SVG has an embedded raster image in it and will not render well"
|
2022-01-06 11:06:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
2022-01-06 11:21:12 +00:00
|
|
|
|
class NoTextInSVG(NoElementInSVG):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
element = "text"
|
|
|
|
|
|
message = "This SVG has text which has not been converted to paths and may not render well"
|
2022-01-06 11:21:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
2019-05-03 15:13:39 +01:00
|
|
|
|
class OnlySMSCharacters:
|
2020-07-03 15:46:00 +01:00
|
|
|
|
def __init__(self, *args, template_type, **kwargs):
|
|
|
|
|
|
self._template_type = template_type
|
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
def __call__(self, form, field):
|
2023-08-25 09:12:23 -07:00
|
|
|
|
non_sms_characters = sorted(
|
|
|
|
|
|
list(SanitiseSMS.get_non_compatible_characters(field.data))
|
|
|
|
|
|
)
|
2019-05-03 15:13:39 +01:00
|
|
|
|
if non_sms_characters:
|
2017-02-14 17:06:32 +00:00
|
|
|
|
raise ValidationError(
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"You cannot use {} in {}. {} will not show up properly on everyone’s phones.".format(
|
|
|
|
|
|
formatted_list(
|
|
|
|
|
|
non_sms_characters,
|
|
|
|
|
|
conjunction="or",
|
|
|
|
|
|
before_each="",
|
|
|
|
|
|
after_each="",
|
|
|
|
|
|
),
|
2020-07-03 15:46:00 +01:00
|
|
|
|
{
|
2023-08-25 09:12:23 -07:00
|
|
|
|
"sms": "text messages",
|
2020-07-03 15:46:00 +01:00
|
|
|
|
}.get(self._template_type),
|
2023-08-25 09:12:23 -07:00
|
|
|
|
("It" if len(non_sms_characters) == 1 else "They"),
|
2017-02-14 17:06:32 +00:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2018-01-31 10:26:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
2022-10-04 03:04:13 +00:00
|
|
|
|
# class NoPlaceholders:
|
2020-10-05 17:17:21 +01:00
|
|
|
|
|
2022-10-04 03:04:13 +00:00
|
|
|
|
# def __init__(self, message=None):
|
|
|
|
|
|
# self.message = message or (
|
2022-12-20 09:44:33 -05:00
|
|
|
|
# 'You can’t use ((double brackets)) to personalize this message'
|
2022-10-04 03:04:13 +00:00
|
|
|
|
# )
|
2020-10-05 17:17:21 +01:00
|
|
|
|
|
2022-10-04 03:04:13 +00:00
|
|
|
|
# def __call__(self, form, field):
|
|
|
|
|
|
# if Field(field.data).placeholders:
|
|
|
|
|
|
# raise ValidationError(self.message)
|
2020-12-24 13:56:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
2021-07-26 15:10:57 +01:00
|
|
|
|
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
|
|
|
|
|
|
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
|
2018-01-31 10:26:37 +00:00
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def __init__(self, message="Use letters and numbers only"):
|
2018-01-31 10:26:37 +00:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and not re.match(self.regex, field.data):
|
|
|
|
|
|
raise ValidationError(self.message)
|
2018-02-28 11:50:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DoesNotStartWithDoubleZero:
|
2019-09-13 16:00:56 +01:00
|
|
|
|
def __init__(self, message="Cannot start with 00"):
|
2018-02-28 11:50:41 +00:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and field.data.startswith("00"):
|
|
|
|
|
|
raise ValidationError(self.message)
|
2019-11-08 17:12:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
2024-07-24 13:56:32 -07:00
|
|
|
|
class FieldCannotContainComma:
|
|
|
|
|
|
def __init__(self, message="Cannot contain a comma"):
|
|
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and "," in field.data:
|
|
|
|
|
|
raise ValidationError(self.message)
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-11-08 17:12:32 +00:00
|
|
|
|
class MustContainAlphanumericCharacters:
|
|
|
|
|
|
regex = re.compile(r".*[a-zA-Z0-9].*[a-zA-Z0-9].*")
|
|
|
|
|
|
|
2023-08-25 09:12:23 -07:00
|
|
|
|
def __init__(self, message="Must include at least two alphanumeric characters"):
|
2019-11-08 17:12:32 +00:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and not re.match(self.regex, field.data):
|
|
|
|
|
|
raise ValidationError(self.message)
|