2018-01-31 10:26:37 +00:00
|
|
|
|
import re
|
2018-02-14 14:35:16 +00:00
|
|
|
|
|
2017-12-05 14:48:36 +00:00
|
|
|
|
from notifications_utils.field import Field
|
2018-02-20 11:22:17 +00:00
|
|
|
|
from notifications_utils.recipients import (
|
|
|
|
|
|
InvalidEmailError,
|
|
|
|
|
|
validate_email_address,
|
|
|
|
|
|
)
|
2018-05-25 10:18:39 +01:00
|
|
|
|
from notifications_utils.sanitise_text import SanitiseGSM
|
2018-02-20 11:22:17 +00:00
|
|
|
|
from wtforms import ValidationError
|
|
|
|
|
|
from wtforms.validators import Email
|
2017-02-13 13:11:29 +00:00
|
|
|
|
|
2017-02-14 17:06:32 +00:00
|
|
|
|
from app import formatted_list
|
2017-02-13 13:11:29 +00:00
|
|
|
|
from app.main._blacklisted_passwords import blacklisted_passwords
|
2018-09-03 11:06:30 +01:00
|
|
|
|
from app.utils import AgreementInfo, Spreadsheet, is_gov_user
|
2015-12-01 15:51:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
class Blacklist:
|
2015-12-01 15:51:09 +00:00
|
|
|
|
def __init__(self, message=None):
|
|
|
|
|
|
if not message:
|
|
|
|
|
|
message = 'Password is blacklisted.'
|
|
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2016-09-27 11:28:12 +01:00
|
|
|
|
if field.data in blacklisted_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:
|
2016-01-11 15:00:51 +00:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, message='Not a csv file'):
|
|
|
|
|
|
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):
|
|
|
|
|
|
raise ValidationError("{} isn’t 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):
|
2018-12-07 12:23:12 +00:00
|
|
|
|
|
|
|
|
|
|
if field.data == '':
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2016-10-28 10:45:05 +01:00
|
|
|
|
from flask import url_for
|
2016-03-18 12:05:50 +00:00
|
|
|
|
message = (
|
2017-08-10 13:51:46 +01:00
|
|
|
|
'Enter a government email address.'
|
2016-03-18 12:05:50 +00:00
|
|
|
|
' If you think you should have access'
|
2016-12-12 11:25:43 +00:00
|
|
|
|
' <a href="{}">contact us</a>').format(url_for('main.support'))
|
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
|
|
|
|
|
|
|
|
|
|
|
2018-02-14 14:35:16 +00:00
|
|
|
|
class ValidEmail(Email):
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__('Enter a valid email address')
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2018-12-07 12:23:12 +00:00
|
|
|
|
|
|
|
|
|
|
if field.data == '':
|
|
|
|
|
|
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
|
|
|
|
return super().__call__(form, field)
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-02-13 13:11:29 +00:00
|
|
|
|
class NoCommasInPlaceHolders:
|
2016-04-07 16:02:06 +01:00
|
|
|
|
|
2017-12-11 10:50:55 +00:00
|
|
|
|
def __init__(self, message='You can’t put commas between double brackets'):
|
2016-04-07 16:02:06 +01:00
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
2017-12-05 14:48:36 +00: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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OnlyGSMCharacters:
|
|
|
|
|
|
def __call__(self, form, field):
|
2018-05-25 10:18:39 +01:00
|
|
|
|
non_gsm_characters = sorted(list(SanitiseGSM.get_non_compatible_characters(field.data)))
|
2017-02-13 13:11:29 +00:00
|
|
|
|
if non_gsm_characters:
|
2017-02-14 17:06:32 +00:00
|
|
|
|
raise ValidationError(
|
2017-02-15 16:21:14 +00:00
|
|
|
|
'You can’t use {} in text messages. {} won’t show up properly on everyone’s phones.'.format(
|
|
|
|
|
|
formatted_list(non_gsm_characters, conjunction='or', before_each='', after_each=''),
|
|
|
|
|
|
('It' if len(non_gsm_characters) == 1 else 'They')
|
2017-02-14 17:06:32 +00:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2018-01-31 10:26:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
2018-01-31 10:38:44 +00:00
|
|
|
|
class LettersNumbersAndFullStopsOnly:
|
2018-01-31 10:26:37 +00:00
|
|
|
|
|
2018-01-31 10:38:44 +00:00
|
|
|
|
regex = re.compile(r'^[a-zA-Z0-9\s\.]+$')
|
2018-01-31 10:26:37 +00:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, message='Use letters and numbers only'):
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, message="Can't start with 00"):
|
|
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and field.data.startswith("00"):
|
|
|
|
|
|
raise ValidationError(self.message)
|
2018-09-03 11:06:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KnownGovernmentDomain:
|
|
|
|
|
|
|
|
|
|
|
|
message = 'Not a known government domain (you might need to update domains.yml)'
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
if field.data and AgreementInfo(field.data).owner is None:
|
|
|
|
|
|
raise ValidationError(self.message)
|
2018-09-06 12:12:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CanonicalGovernmentDomain:
|
|
|
|
|
|
|
|
|
|
|
|
message = 'Not {} domain (use {} if appropriate)'
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self, form, field):
|
|
|
|
|
|
|
|
|
|
|
|
if not field.data:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
domain = AgreementInfo(field.data)
|
|
|
|
|
|
|
|
|
|
|
|
if not domain.is_canonical:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
self.message.format('a canonical', domain.canonical_domain)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if field.data != domain.canonical_domain:
|
|
|
|
|
|
raise ValidationError(
|
|
|
|
|
|
self.message.format('an organisation-level', domain.canonical_domain)
|
|
|
|
|
|
)
|