Files
notifications-admin/app/main/forms.py
Chris Hill-Scott 188ce5e5a7 Let users register with int’national phone numbers
Right now Notify restricts you to registering with a UK mobile number.
This is because when we built the user registration stuff we couldn’t
send to international mobiles.

However we can send to international mobile numbers, and it’s totally
reasonable to expect employees of the UK government to be working
abroad, and have a foreign mobile phone – we’ve heard from one such
user.

So this commit:
- changes all places where users enter their own phone number to use
  the validation function which allows international phone numbers
- renames the `mobile_number` validation to `uk_mobile_number` to make
  it explicit, and force it to break the tests if there’s somewhere it’s
  being used that I haven’t thought of
2017-08-29 14:52:24 +01:00

685 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
import pytz
from flask_wtf import FlaskForm as Form
from datetime import datetime, timedelta
from notifications_utils.recipients import (
validate_phone_number,
InvalidPhoneError
)
from notifications_utils.columns import Columns
from wtforms import (
widgets,
validators,
StringField,
PasswordField,
ValidationError,
TextAreaField,
FileField,
BooleanField,
HiddenField,
IntegerField,
RadioField,
FieldList,
DateField,
SelectField)
from wtforms.fields.html5 import EmailField, TelField, SearchField
from wtforms.validators import (DataRequired, Email, Length, Regexp, Optional)
from flask_wtf.file import FileField as FileField_wtf, FileAllowed
from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoCommasInPlaceHolders, OnlyGSMCharacters)
def get_time_value_and_label(future_time):
return (
future_time.replace(tzinfo=None).isoformat(),
'{} at {}'.format(
get_human_day(future_time.astimezone(pytz.timezone('Europe/London'))),
get_human_time(future_time.astimezone(pytz.timezone('Europe/London')))
)
)
def get_human_time(time):
return {
'0': 'midnight',
'12': 'midday'
}.get(
time.strftime('%-H'),
time.strftime('%-I%p').lower()
)
def get_human_day(time, prefix_today_with='T'):
# Add 1 hour to get midnight today instead of midnight tomorrow
time = (time - timedelta(hours=1)).strftime('%A')
if time == datetime.utcnow().strftime('%A'):
return '{}oday'.format(prefix_today_with)
if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
return 'Tomorrow'
return time
def get_furthest_possible_scheduled_time():
return (datetime.utcnow() + timedelta(days=4)).replace(hour=0)
def get_next_hours_until(until):
now = datetime.utcnow()
hours = int((until - now).total_seconds() / (60 * 60))
return [
(now + timedelta(hours=i)).replace(minute=0, second=0).replace(tzinfo=pytz.utc)
for i in range(1, hours + 1)
]
def get_next_days_until(until):
now = datetime.utcnow()
days = int((until - now).total_seconds() / (60 * 60 * 24))
return [
get_human_day(
(now + timedelta(days=i)).replace(tzinfo=pytz.utc),
prefix_today_with='Later t'
)
for i in range(0, days + 1)
]
def email_address(label='Email address', gov_user=True):
validators = [
Length(min=5, max=255),
DataRequired(message='Cant be empty'),
Email(message='Enter a valid email address')
]
if gov_user:
validators.append(ValidGovEmail())
return EmailField(label, validators)
class UKMobileNumber(TelField):
def pre_validate(self, form):
try:
validate_phone_number(self.data)
except InvalidPhoneError as e:
raise ValidationError(str(e))
class InternationalPhoneNumber(TelField):
def pre_validate(self, form):
try:
validate_phone_number(self.data, international=True)
except InvalidPhoneError as e:
raise ValidationError(str(e))
def uk_mobile_number(label='Mobile number'):
return UKMobileNumber(label,
validators=[DataRequired(message='Cant be empty')])
def international_phone_number(label='Mobile number'):
return InternationalPhoneNumber(
label,
validators=[DataRequired(message='Cant be empty')]
)
def password(label='Password'):
return PasswordField(label,
validators=[DataRequired(message='Cant be empty'),
Length(8, 255, message='Must be at least 8 characters'),
Blacklist(message='Choose a password thats harder to guess')])
def sms_code():
verify_code = '^\d{5}$'
return StringField('Text message code',
validators=[DataRequired(message='Cant be empty'),
Regexp(regex=verify_code,
message='Code not found')])
class LoginForm(Form):
email_address = StringField('Email address', validators=[
Length(min=5, max=255),
DataRequired(message='Cant be empty'),
Email(message='Enter a valid email address')
])
password = PasswordField('Password', validators=[
DataRequired(message='Enter your password')
])
class RegisterUserForm(Form):
name = StringField('Full name',
validators=[DataRequired(message='Cant be empty')])
email_address = email_address()
mobile_number = international_phone_number()
password = password()
class RegisterUserFromInviteForm(Form):
name = StringField('Full name',
validators=[DataRequired(message='Cant be empty')])
mobile_number = international_phone_number()
password = password()
service = HiddenField('service')
email_address = HiddenField('email_address')
class PermissionsForm(Form):
send_messages = BooleanField("Send messages from existing templates")
manage_templates = BooleanField("Add and edit templates")
manage_service = BooleanField("Modify this service and its team")
manage_api_keys = BooleanField("Create and revoke API keys")
class InviteUserForm(PermissionsForm):
email_address = email_address(gov_user=False)
def __init__(self, invalid_email_address, *args, **kwargs):
super(InviteUserForm, self).__init__(*args, **kwargs)
self.invalid_email_address = invalid_email_address.lower()
def validate_email_address(self, field):
if field.data.lower() == self.invalid_email_address:
raise ValidationError("You cant send an invitation to yourself")
class TwoFactorForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
validate_code_func -- Validates the code with the API.
'''
self.validate_code_func = validate_code_func
super(TwoFactorForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data)
if not is_valid:
raise ValidationError(reason)
class EmailNotReceivedForm(Form):
email_address = email_address()
class TextNotReceivedForm(Form):
mobile_number = international_phone_number()
class ServiceNameForm(Form):
def __init__(self, *args, **kwargs):
super(ServiceNameForm, self).__init__(*args, **kwargs)
name = StringField(
u'Service name',
validators=[
DataRequired(message='Cant be empty')
])
class ConfirmPasswordForm(Form):
def __init__(self, validate_password_func, *args, **kwargs):
self.validate_password_func = validate_password_func
super(ConfirmPasswordForm, self).__init__(*args, **kwargs)
password = PasswordField(u'Enter password')
def validate_password(self, field):
if not self.validate_password_func(field.data):
raise ValidationError('Invalid password')
class BaseTemplateForm(Form):
name = StringField(
u'Template name',
validators=[DataRequired(message="Cant be empty")])
template_content = TextAreaField(
u'Message',
validators=[
DataRequired(message="Cant be empty"),
NoCommasInPlaceHolders()
]
)
process_type = RadioField(
'Use priority queue?',
choices=[
('priority', 'Yes'),
('normal', 'No'),
],
validators=[DataRequired()],
default='normal'
)
class SMSTemplateForm(BaseTemplateForm):
def validate_template_content(self, field):
OnlyGSMCharacters()(None, field)
class EmailTemplateForm(BaseTemplateForm):
subject = TextAreaField(
u'Subject',
validators=[DataRequired(message="Cant be empty")])
class LetterTemplateForm(EmailTemplateForm):
subject = TextAreaField(
u'Main heading',
validators=[DataRequired(message="Cant be empty")])
template_content = TextAreaField(
u'Body',
validators=[
DataRequired(message="Cant be empty"),
NoCommasInPlaceHolders()
]
)
class ForgotPasswordForm(Form):
email_address = email_address(gov_user=False)
class NewPasswordForm(Form):
new_password = password()
class ChangePasswordForm(Form):
def __init__(self, validate_password_func, *args, **kwargs):
self.validate_password_func = validate_password_func
super(ChangePasswordForm, self).__init__(*args, **kwargs)
old_password = password('Current password')
new_password = password('New password')
def validate_old_password(self, field):
if not self.validate_password_func(field.data):
raise ValidationError('Invalid password')
class CsvUploadForm(Form):
file = FileField('Add recipients', validators=[DataRequired(
message='Please pick a file'), CsvFileValidator()])
class ChangeNameForm(Form):
new_name = StringField(u'Your name')
class ChangeEmailForm(Form):
def __init__(self, validate_email_func, *args, **kwargs):
self.validate_email_func = validate_email_func
super(ChangeEmailForm, self).__init__(*args, **kwargs)
email_address = email_address()
def validate_email_address(self, field):
is_valid = self.validate_email_func(field.data)
if not is_valid:
raise ValidationError("The email address is already in use")
class ChangeMobileNumberForm(Form):
mobile_number = international_phone_number()
class ConfirmMobileNumberForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
self.validate_code_func = validate_code_func
super(ConfirmMobileNumberForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, msg = self.validate_code_func(field.data)
if not is_valid:
raise ValidationError(msg)
class ChooseTimeForm(Form):
def __init__(self, *args, **kwargs):
super(ChooseTimeForm, self).__init__(*args, **kwargs)
self.scheduled_for.choices = [('', 'Now')] + [
get_time_value_and_label(hour) for hour in get_next_hours_until(
get_furthest_possible_scheduled_time()
)
]
self.scheduled_for.categories = get_next_days_until(get_furthest_possible_scheduled_time())
scheduled_for = RadioField(
'When should Notify send these messages?',
default='',
validators=[
DataRequired()
]
)
class CreateKeyForm(Form):
def __init__(self, existing_key_names=[], *args, **kwargs):
self.existing_key_names = [x.lower() for x in existing_key_names]
super(CreateKeyForm, self).__init__(*args, **kwargs)
key_type = RadioField(
'Type of key',
validators=[
DataRequired()
]
)
key_name = StringField(u'Description of key', validators=[
DataRequired(message='You need to give the key a name')
])
def validate_key_name(self, key_name):
if key_name.data.lower() in self.existing_key_names:
raise ValidationError('A key with this name already exists')
class SupportType(Form):
support_type = RadioField(
'How can we help you?',
choices=[
('problem', 'Report a problem'),
('question', 'Ask a question or give feedback'),
],
validators=[DataRequired()]
)
class Feedback(Form):
name = StringField('Name')
email_address = StringField('Email address')
feedback = TextAreaField('Your message', validators=[DataRequired(message="Cant be empty")])
class Problem(Feedback):
email_address = email_address(label='Email address', gov_user=False)
class Triage(Form):
severe = RadioField(
'Is it an emergency?',
choices=[
('yes', 'Yes'),
('no', 'No'),
],
validators=[DataRequired()]
)
class RequestToGoLiveForm(Form):
mou = RadioField(
(
'Has your organisation accepted the GOV.UK Notify data sharing and financial '
'agreement (Memorandum of Understanding)?'
),
choices=[
('yes', 'Yes'),
('no', 'No'),
('dont know', 'I dont know')
],
validators=[DataRequired()]
)
channel = RadioField(
'What kind of messages will you be sending?',
choices=[
('emails', 'Emails'),
('text messages', 'Text messages'),
('emails and text messages', 'Both')
],
validators=[DataRequired()]
)
start_date = StringField(
'When will you be ready to start sending messages?',
validators=[DataRequired(message='Cant be empty')]
)
start_volume = StringField(
'How many messages do you expect to send to start with?',
validators=[DataRequired(message='Cant be empty')]
)
peak_volume = StringField(
'Will the number of messages increase and when will that start?',
validators=[DataRequired(message='Cant be empty')]
)
upload_or_api = RadioField(
'How are you going to send messages?',
choices=[
('File upload', 'Upload a spreadsheet of recipients'),
('API', 'Integrate with the GOV.UK Notify API'),
('API and file upload', 'Both')
],
validators=[DataRequired()]
)
class ProviderForm(Form):
priority = IntegerField('Priority', [validators.NumberRange(min=1, max=100, message="Must be between 1 and 100")])
class ServiceReplyToEmailFrom(Form):
email_address = email_address(label='Email reply to address')
class ServiceSmsSender(Form):
sms_sender = StringField(
'Text message sender',
validators=[
DataRequired(message="Cant be empty"),
Length(max=11, message="Enter 11 characters or fewer")
]
)
def validate_sms_sender(form, field):
if field.data and not re.match(r'^[a-zA-Z0-9\s]+$', field.data):
raise ValidationError('Use letters and numbers only')
class ServiceLetterContactBlock(Form):
letter_contact_block = TextAreaField(
validators=[
NoCommasInPlaceHolders()
]
)
def validate_letter_contact_block(form, field):
line_count = field.data.strip().count('\n')
if line_count >= 10:
raise ValidationError(
'Contains {} lines, maximum is 10'.format(line_count + 1)
)
class ServiceBrandingOrg(Form):
def __init__(self, organisations=[], *args, **kwargs):
self.organisation.choices = organisations
super(ServiceBrandingOrg, self).__init__(*args, **kwargs)
branding_type = RadioField(
'Branding',
choices=[
('govuk', 'GOV.UK only'),
('both', 'GOV.UK and organisation'),
('org', 'Organisation only')
],
validators=[
DataRequired()
]
)
organisation = RadioField(
'Organisation',
validators=[
DataRequired()
]
)
class ServiceSelectOrg(Form):
def __init__(self, organisations=[], *args, **kwargs):
self.organisation.choices = organisations
super(ServiceSelectOrg, self).__init__(*args, **kwargs)
organisation = RadioField(
'Organisation',
validators=[
DataRequired()
]
)
class ServiceManageOrg(Form):
name = StringField('Name')
colour = StringField('Colour', render_kw={'onkeyup': 'update_colour_span()', 'onblur': 'update_colour_span()'})
file = FileField_wtf('Upload a PNG logo', validators=[FileAllowed(['png'], 'PNG Images only!')])
class LetterBranding(Form):
def __init__(self, choices=[], *args, **kwargs):
super().__init__(*args, **kwargs)
self.dvla_org_id.choices = choices
dvla_org_id = RadioField(
'Which logo should this services letter have?',
validators=[
DataRequired()
]
)
class Whitelist(Form):
def populate(self, email_addresses, phone_numbers):
for form_field, existing_whitelist in (
(self.email_addresses, email_addresses),
(self.phone_numbers, phone_numbers)
):
for index, value in enumerate(existing_whitelist):
form_field[index].data = value
email_addresses = FieldList(
EmailField(
'',
validators=[
Optional(),
Email(message='Enter valid email addresses')
],
default=''
),
min_entries=5,
max_entries=5,
label="Email addresses"
)
phone_numbers = FieldList(
InternationalPhoneNumber(
'',
validators=[
Optional()
],
default=''
),
min_entries=5,
max_entries=5,
label="Mobile numbers"
)
class DateFilterForm(Form):
start_date = DateField("Start Date", [validators.optional()])
end_date = DateField("End Date", [validators.optional()])
include_from_test_key = BooleanField("Include test keys", default="checked", false_values={"N"})
class ChooseTemplateType(Form):
template_type = RadioField(
'What kind of template do you want to add?',
validators=[
DataRequired()
]
)
def __init__(self, include_letters=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.template_type.choices = filter(None, [
('email', 'Email'),
('sms', 'Text message'),
('letter', 'Letter') if include_letters else None
])
class SearchTemplatesForm(Form):
search = SearchField('Search by name')
class SearchNotificationsForm(Form):
to = SearchField('Search by phone number or email address')
class PlaceholderForm(Form):
pass
class PasswordFieldShowHasContent(StringField):
widget = widgets.PasswordInput(hide_value=False)
class ServiceInboundApiForm(Form):
url = StringField("Inbound sms url",
validators=[DataRequired(message='Cant be empty'),
Regexp(regex="^https.*",
message='Must be a valid https url')]
)
bearer_token = PasswordFieldShowHasContent("Bearer token",
validators=[DataRequired(message='Cant be empty'),
Length(min=10, message='Must be at least 10 characters')])
def get_placeholder_form_instance(
placeholder_name,
dict_to_populate_from,
optional_placeholder=False,
allow_international_phone_numbers=False,
):
if Columns.make_key(placeholder_name) == 'emailaddress':
field = email_address(label=placeholder_name, gov_user=False)
elif Columns.make_key(placeholder_name) == 'phonenumber':
if allow_international_phone_numbers:
field = international_phone_number(label=placeholder_name)
else:
field = uk_mobile_number(label=placeholder_name)
elif optional_placeholder:
field = StringField(placeholder_name)
else:
field = StringField(placeholder_name, validators=[
DataRequired(message='Cant be empty')
])
PlaceholderForm.placeholder_value = field
return PlaceholderForm(
placeholder_value=dict_to_populate_from.get(placeholder_name, '')
)