mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-06 11:23:48 -05:00
561 lines
16 KiB
Python
561 lines
16 KiB
Python
import re
|
||
|
||
import pytz
|
||
from flask_login import current_user
|
||
from flask_wtf import Form
|
||
from datetime import datetime, timedelta
|
||
from notifications_utils.recipients import (
|
||
validate_phone_number,
|
||
InvalidPhoneError
|
||
)
|
||
from wtforms import (
|
||
validators,
|
||
StringField,
|
||
PasswordField,
|
||
ValidationError,
|
||
TextAreaField,
|
||
FileField,
|
||
BooleanField,
|
||
HiddenField,
|
||
IntegerField,
|
||
RadioField,
|
||
FieldList,
|
||
DateField,
|
||
SelectField)
|
||
from wtforms.fields.html5 import EmailField, TelField
|
||
from wtforms.validators import (DataRequired, Email, Length, Regexp, Optional)
|
||
|
||
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='Can’t 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))
|
||
|
||
|
||
def mobile_number():
|
||
return UKMobileNumber('Mobile number',
|
||
validators=[DataRequired(message='Can’t be empty')])
|
||
|
||
|
||
def password(label='Password'):
|
||
return PasswordField(label,
|
||
validators=[DataRequired(message='Can’t be empty'),
|
||
Length(8, 255, message='Must be at least 8 characters'),
|
||
Blacklist(message='Choose a password that’s harder to guess')])
|
||
|
||
|
||
def sms_code():
|
||
verify_code = '^\d{5}$'
|
||
return StringField('Text message code',
|
||
validators=[DataRequired(message='Can’t 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='Can’t 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='Can’t be empty')])
|
||
email_address = email_address()
|
||
mobile_number = mobile_number()
|
||
password = password()
|
||
|
||
|
||
class RegisterUserFromInviteForm(Form):
|
||
name = StringField('Full name',
|
||
validators=[DataRequired(message='Can’t be empty')])
|
||
mobile_number = mobile_number()
|
||
password = password()
|
||
service = HiddenField('service')
|
||
email_address = HiddenField('email_address')
|
||
|
||
|
||
class PermissionsForm(Form):
|
||
send_messages = BooleanField("Send messages from existing templates")
|
||
manage_service = BooleanField("Modify this service, its team, and its templates")
|
||
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 can’t 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 = mobile_number()
|
||
|
||
|
||
class AddServiceForm(Form):
|
||
def __init__(self, names_func, *args, **kwargs):
|
||
"""
|
||
Keyword arguments:
|
||
names_func -- Returns a list of unique service_names already registered
|
||
on the system.
|
||
"""
|
||
self._names_func = names_func
|
||
super(AddServiceForm, self).__init__(*args, **kwargs)
|
||
|
||
name = StringField(
|
||
'Service name',
|
||
validators=[
|
||
DataRequired(message='Can’t be empty')
|
||
]
|
||
)
|
||
|
||
def validate_name(self, a):
|
||
from app.utils import email_safe
|
||
# make sure the email_from will be unique to all services
|
||
if email_safe(a.data) in self._names_func():
|
||
raise ValidationError('This service name is already in use')
|
||
|
||
|
||
class ServiceNameForm(Form):
|
||
def __init__(self, names_func, *args, **kwargs):
|
||
"""
|
||
Keyword arguments:
|
||
names_func -- Returns a list of unique service_names already registered
|
||
on the system.
|
||
"""
|
||
self._names_func = names_func
|
||
super(ServiceNameForm, self).__init__(*args, **kwargs)
|
||
|
||
name = StringField(
|
||
u'Service name',
|
||
validators=[
|
||
DataRequired(message='Can’t be empty')
|
||
])
|
||
|
||
def validate_name(self, a):
|
||
from app.utils import email_safe
|
||
# make sure the email_from will be unique to all services
|
||
if email_safe(a.data) in self._names_func():
|
||
raise ValidationError('This service name is already in use')
|
||
|
||
|
||
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="Can’t be empty")])
|
||
|
||
template_content = TextAreaField(
|
||
u'Message',
|
||
validators=[
|
||
DataRequired(message="Can’t 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="Can’t be empty")])
|
||
|
||
|
||
class LetterTemplateForm(EmailTemplateForm):
|
||
pass
|
||
|
||
|
||
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 = mobile_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(
|
||
'What should Notify do when you use this 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="Can’t 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'),
|
||
('don’t know', 'I don’t 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='Can’t be empty')]
|
||
)
|
||
start_volume = StringField(
|
||
'How many messages do you expect to send to start with?',
|
||
validators=[DataRequired(message='Can’t be empty')]
|
||
)
|
||
peak_volume = StringField(
|
||
'Will the number of messages increase and when will that start?',
|
||
validators=[DataRequired(message='Can’t 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=[
|
||
Length(max=11, message="Enter 11 characters or fewer")
|
||
]
|
||
)
|
||
|
||
def validate_sms_sender(form, field):
|
||
if field.data and not re.match('^[a-zA-Z0-9\s]+$', field.data):
|
||
raise ValidationError('Use letters and numbers only')
|
||
|
||
|
||
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 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(
|
||
UKMobileNumber(
|
||
'',
|
||
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"})
|