Files
notifications-admin/app/main/forms.py

1877 lines
61 KiB
Python
Raw Normal View History

import weakref
from datetime import datetime, timedelta
from itertools import chain
from numbers import Number
import pytz
from flask import Markup, render_template, request
from flask_login import current_user
from flask_wtf import FlaskForm as Form
from flask_wtf.file import FileAllowed
from flask_wtf.file import FileField as FileField_wtf
from flask_wtf.file import FileSize
from notifications_utils.formatters import strip_all_whitespace
from notifications_utils.insensitive_dict import InsensitiveDict
2023-08-25 08:57:24 -07:00
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
from werkzeug.utils import cached_property
2016-01-12 10:43:23 +00:00
from wtforms import (
BooleanField,
DateField,
EmailField,
FieldList,
FileField,
HiddenField,
IntegerField,
PasswordField,
)
from wtforms import RadioField as WTFormsRadioField
from wtforms import (
SearchField,
SelectMultipleField,
StringField,
TelField,
TextAreaField,
ValidationError,
validators,
Add flake8 linting to project The GDS Way™[1] recommends using Flake8 to lint Python projects. This commit takes the Flake8 config from Digital Marketplace API[2] and removes the bits we don’t need. It changes the `max_complexity` setting to 14, which is the most complex code we have in this repo currently (we shouldn’t be writing code _more_ complex than what we already have). This commit also fixes the errors found by Flake8, which includes 6(!) tests which were never getting run because they had the same names as existing tests. Here is a full list of the errors that were found and fixed: ``` ./app/__init__.py:2:1: F401 're' imported but unused ./app/__init__.py:4:1: F401 'json' imported but unused ./app/__init__.py:8:1: F401 'dateutil' imported but unused ./app/__init__.py:11:1: F401 'flask.escape' imported but unused ./app/__init__.py:41:1: F401 'app.proxy_fix' imported but unused ./app/__init__.py:129:5: F821 undefined name 'proxy_fix' ./app/__init__.py:221:19: F821 undefined name 'highlight' ./app/__init__.py:221:35: F821 undefined name 'JavascriptLexer' ./app/__init__.py:221:54: F821 undefined name 'HtmlFormatter' ./app/config.py:2:1: F401 'datetime.timedelta' imported but unused ./app/event_handlers.py:2:1: F401 'flask_login.current_user' imported but unused ./app/utils.py:11:1: F401 'dateutil.parser' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.two_factor' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.notifications' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.add_service' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.forgot_password' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.inbound_number' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.styleguide' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.organisations' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.letter_jobs' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.verify' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.conversation' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.api_keys' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.send' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.dashboard' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.jobs' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.manage_users' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.sign_in' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.sign_out' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.code_not_received' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.invites' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.platform_admin' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.providers' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.service_settings' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.index' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.new_password' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.user_profile' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.feedback' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.choose_service' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.templates' imported but unused ./app/main/__init__.py:5:1: F401 'app.main.views.register' imported but unused ./app/main/forms.py:12:1: F401 'wtforms.SelectField' imported but unused ./app/main/views/api_keys.py:37:29: E241 multiple spaces after ':' ./app/main/views/feedback.py:3:1: F401 'flask.flash' imported but unused ./app/main/views/feedback.py:122:17: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/inbound_number.py:1:1: F401 'flask.url_for' imported but unused ./app/main/views/inbound_number.py:1:1: F401 'flask.session' imported but unused ./app/main/views/inbound_number.py:1:1: F401 'flask.redirect' imported but unused ./app/main/views/inbound_number.py:1:1: F401 'flask.request' imported but unused ./app/main/views/inbound_number.py:13:1: F401 'flask.jsonify' imported but unused ./app/main/views/jobs.py:31:1: F401 'app.utils.get_template' imported but unused ./app/main/views/letter_jobs.py:1:1: F401 'datetime' imported but unused ./app/main/views/letter_jobs.py:6:1: F401 'app.format_datetime_24h' imported but unused ./app/main/views/manage_users.py:111:9: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/notifications.py:121:5: F841 local variable 'status_args' is assigned to but never used ./app/main/views/organisations.py:1:1: F401 'flask.request' imported but unused ./app/main/views/service_settings.py:77:9: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/service_settings.py:82:9: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/service_settings.py:420:13: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/sign_in.py:12:1: F401 'flask_login.confirm_login' imported but unused ./app/main/views/sign_in.py:17:1: F401 'app.service_api_client' imported but unused ./app/main/views/sign_in.py:62:13: E123 closing bracket does not match indentation of opening bracket's line ./app/main/views/templates.py:4:1: F401 'flask.json' imported but unused ./app/main/views/templates.py:17:1: F401 'notifications_utils.formatters.escape_html' imported but unused ./app/main/views/templates.py:23:1: F401 'app.utils.get_help_argument' imported but unused ./app/main/views/templates.py:64:13: E123 closing bracket does not match indentation of opening bracket's line ./app/notify_client/service_api_client.py:6:1: F401 '.notification_api_client' imported but unused ./app/notify_client/user_api_client.py:1:1: F401 'uuid' imported but unused ./app/notify_client/user_api_client.py:3:1: F401 'flask.session' imported but unused ./tests/__init__.py:1:1: F401 'csv' imported but unused ./tests/app/main/test_asset_fingerprinter.py:2:1: F401 'os' imported but unused ./tests/app/main/test_asset_fingerprinter.py:4:1: F401 'unittest.mock' imported but unused ./tests/app/main/test_asset_fingerprinter.py:98:9: F841 local variable 'string_with_unicode_character' is assigned to but never used ./tests/app/main/test_errorhandlers.py:2:1: F401 'flask.url_for' imported but unused ./tests/app/main/test_permissions.py:26:13: F841 local variable 'response' is assigned to but never used ./tests/app/main/test_placeholder_form.py:3:1: F401 'wtforms.Label' imported but unused ./tests/app/main/test_placeholder_form.py:11:10: F841 local variable 'req' is assigned to but never used ./tests/app/main/test_two_factor_form.py:10:67: F841 local variable 'req' is assigned to but never used ./tests/app/main/test_two_factor_form.py:23:65: F841 local variable 'req' is assigned to but never used ./tests/app/main/test_two_factor_form.py:37:48: F841 local variable 'req' is assigned to but never used ./tests/app/main/test_two_factor_form.py:51:67: F841 local variable 'req' is assigned to but never used ./tests/app/main/test_two_factor_form.py:65:67: F841 local variable 'req' is assigned to but never used ./tests/app/main/views/test_accept_invite.py:356:5: F841 local variable 'element' is assigned to but never used ./tests/app/main/views/test_activity.py:11:1: F811 redefinition of unused 'mock_get_notifications' from line 11 ./tests/app/main/views/test_activity.py:18:1: F401 'datetime.datetime' imported but unused ./tests/app/main/views/test_activity.py:102:5: F841 local variable 'content' is assigned to but never used ./tests/app/main/views/test_activity.py:104:5: F841 local variable 'notification' is assigned to but never used ./tests/app/main/views/test_activity.py:337:5: F841 local variable '_notifications_mock' is assigned to but never used ./tests/app/main/views/test_activity.py:373:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_activity.py:378:9: E121 continuation line under-indented for hanging indent ./tests/app/main/views/test_activity.py:404:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_activity.py:407:9: E121 continuation line under-indented for hanging indent ./tests/app/main/views/test_api_keys.py:354:5: F841 local variable 'response' is assigned to but never used ./tests/app/main/views/test_conversation.py:5:1: F401 'bs4.BeautifulSoup' imported but unused ./tests/app/main/views/test_conversation.py:198:5: F841 local variable 'mock_get_inbound_sms' is assigned to but never used ./tests/app/main/views/test_dashboard.py:53:5: F841 local variable 'mock_template_stats' is assigned to but never used ./tests/app/main/views/test_dashboard.py:72:5: F841 local variable 'mock_template_stats' is assigned to but never used ./tests/app/main/views/test_jobs.py:2:1: F401 'uuid' imported but unused ./tests/app/main/views/test_jobs.py:3:1: F401 'urllib.parse.urlparse' imported but unused ./tests/app/main/views/test_jobs.py:3:1: F401 'urllib.parse.quote' imported but unused ./tests/app/main/views/test_jobs.py:3:1: F401 'urllib.parse.parse_qs' imported but unused ./tests/app/main/views/test_jobs.py:9:1: F401 'app.main.views.jobs.get_status_filters' imported but unused ./tests/app/main/views/test_jobs.py:10:1: F401 'tests.notification_json' imported but unused ./tests/app/main/views/test_letters.py:6:1: F401 'tests.service_json' imported but unused ./tests/app/main/views/test_notifications.py:5:1: F401 'app.utils.REQUESTED_STATUSES' imported but unused ./tests/app/main/views/test_notifications.py:5:1: F401 'app.utils.DELIVERED_STATUSES' imported but unused ./tests/app/main/views/test_notifications.py:5:1: F401 'app.utils.SENDING_STATUSES' imported but unused ./tests/app/main/views/test_notifications.py:5:1: F401 'app.utils.FAILURE_STATUSES' imported but unused ./tests/app/main/views/test_platform_admin.py:242:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_platform_admin.py:247:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_send.py:3:1: F401 'unittest.mock.Mock' imported but unused ./tests/app/main/views/test_send.py:18:1: F811 redefinition of unused 'mock_get_service' from line 18 ./tests/app/main/views/test_send.py:18:1: F401 'tests.conftest.multiple_letter_contact_blocks' imported but unused ./tests/app/main/views/test_send.py:18:1: F401 'tests.conftest.no_sms_senders' imported but unused ./tests/app/main/views/test_send.py:18:1: F401 'tests.conftest.multiple_sms_senders' imported but unused ./tests/app/main/views/test_send.py:18:1: F401 'tests.conftest.no_letter_contact_blocks' imported but unused ./tests/app/main/views/test_send.py:102:5: F841 local variable 'response' is assigned to but never used ./tests/app/main/views/test_send.py:870:5: F841 local variable 'response' is assigned to but never used ./tests/app/main/views/test_send.py:1367:5: F841 local variable 'service_id' is assigned to but never used ./tests/app/main/views/test_send.py:1451:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_send.py:1620:80: E226 missing whitespace around arithmetic operator ./tests/app/main/views/test_send.py:1909:13: E126 continuation line over-indented for hanging indent ./tests/app/main/views/test_send.py:1912:9: E121 continuation line under-indented for hanging indent ./tests/app/main/views/test_service_settings.py:13:1: F811 redefinition of unused 'no_reply_to_email_addresses' from line 13 ./tests/app/main/views/test_service_settings.py:13:1: F401 'tests.conftest.single_reply_to_email_address' imported but unused ./tests/app/main/views/test_service_settings.py:28:5: E123 closing bracket does not match indentation of opening bracket's line ./tests/app/main/views/test_service_settings.py:104:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:166:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:186:5: F841 local variable 'mocked_get_fn' is assigned to but never used ./tests/app/main/views/test_service_settings.py:217:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:237:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:257:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:307:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:340:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:466:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:555:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:615:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:719:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:874:5: F841 local variable 'page' is assigned to but never used ./tests/app/main/views/test_service_settings.py:902:5: F841 local variable 'page' is assigned to but never used ./tests/app/main/views/test_service_settings.py:954:5: F841 local variable 'page' is assigned to but never used ./tests/app/main/views/test_service_settings.py:986:5: F841 local variable 'page' is assigned to but never used ./tests/app/main/views/test_service_settings.py:1101:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1121:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1271:1: F811 redefinition of unused 'test_set_letter_contact_block_saves' from line 1189 ./tests/app/main/views/test_service_settings.py:1433:5: F841 local variable 'page' is assigned to but never used ./tests/app/main/views/test_service_settings.py:1495:5: F841 local variable 'mocked_get_fn' is assigned to but never used ./tests/app/main/views/test_service_settings.py:1540:5: F841 local variable 'mocked_get_fn' is assigned to but never used ./tests/app/main/views/test_service_settings.py:1570:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1589:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1621:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1641:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1658:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1676:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1697:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1759:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_service_settings.py:1775:1: F811 redefinition of unused 'single_reply_to_email_address' from line 13 ./tests/app/main/views/test_templates.py:3:1: F401 'uuid' imported but unused ./tests/app/main/views/test_templates.py:11:1: F401 'tests.conftest.mock_get_user' imported but unused ./tests/app/main/views/test_templates.py:514:1: F811 redefinition of unused 'mock_get_user' from line 11 ./tests/app/main/views/test_templates.py:672:1: F811 redefinition of unused 'mock_get_user' from line 11 ./tests/app/main/views/test_templates.py:795:1: F811 redefinition of unused 'mock_get_user' from line 11 ./tests/app/main/views/test_templates.py:835:1: F811 redefinition of unused 'mock_get_user' from line 11 ./tests/app/main/views/test_two_factor.py:67:13: E126 continuation line over-indented for hanging indent ./tests/app/notify_client/test_notification_client.py:79:5: F841 local variable 'mock_post' is assigned to but never used ``` 1. https://gds-way.cloudapps.digital/manuals/programming-languages/python/linting.html#how-to-use-flake8 2. https://github.com/alphagov/digitalmarketplace-api/blob/d5ab8afef4a0472f9d266d94ab4ffe1333a9aaad/.flake8
2017-10-18 14:51:26 +01:00
)
from wtforms.validators import (
URL,
DataRequired,
InputRequired,
Length,
Optional,
Regexp,
)
from app.formatters import (
format_auth_type,
format_thousands,
guess_name_from_email_address,
)
from app.main.validators import (
CommonlyUsedPassword,
CsvFileValidator,
DoesNotStartWithDoubleZero,
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
2019-11-08 17:12:32 +00:00
MustContainAlphanumericCharacters,
NoCommasInPlaceHolders,
NoEmbeddedImagesInSVG,
NoTextInSVG,
OnlySMSCharacters,
ValidEmail,
ValidGovEmail,
)
2023-07-12 12:09:44 -04:00
from app.models.organization import Organization
2023-12-18 16:10:12 -05:00
from app.utils import merge_jsonlike
2023-11-16 12:24:27 -08:00
from app.utils.csv import get_user_preferred_timezone
2022-10-04 03:04:13 +00:00
from app.utils.user_permissions import all_ui_permissions, permission_options
def get_time_value_and_label(future_time):
2023-11-22 08:09:28 -08:00
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
2024-02-07 12:14:00 -08:00
future_time.astimezone(preferred_tz).isoformat(),
2023-11-16 12:24:27 -08:00
"{} at {} {}".format(
2023-11-22 08:09:28 -08:00
get_human_day(future_time.astimezone(preferred_tz)),
get_human_time(future_time.astimezone(preferred_tz)),
2023-11-16 12:24:27 -08:00
get_user_preferred_timezone(),
),
)
def get_human_time(time):
return {"0": "midnight", "12": "noon"}.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
2023-11-16 12:24:27 -08:00
2023-11-22 08:09:28 -08:00
preferred_tz = pytz.timezone(get_user_preferred_timezone())
time = (time - timedelta(hours=1)).strftime("%A")
2023-11-22 08:09:28 -08:00
if time == datetime.now(preferred_tz).strftime("%A"):
return "{}oday".format(prefix_today_with)
2023-11-22 08:09:28 -08:00
if time == (datetime.now(preferred_tz) + timedelta(days=1)).strftime("%A"):
return "Tomorrow"
return time
def get_furthest_possible_scheduled_time():
# We want local time to find date boundaries
2023-11-22 08:09:28 -08:00
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (datetime.now(preferred_tz) + timedelta(days=4)).replace(hour=0)
def get_next_hours_until(until):
2023-11-22 08:09:28 -08:00
preferred_tz = pytz.timezone(get_user_preferred_timezone())
now = datetime.now(preferred_tz)
hours = int((until - now).total_seconds() / (60 * 60))
return [
(now + timedelta(hours=i)).replace(minute=0, second=0, microsecond=0)
for i in range(1, hours + 1)
]
def get_next_days_until(until):
2023-11-22 08:09:28 -08:00
preferred_tz = pytz.timezone(get_user_preferred_timezone())
now = datetime.now(preferred_tz)
days = int((until - now).total_seconds() / (60 * 60 * 24))
return [
get_human_day((now + timedelta(days=i)), prefix_today_with="Later t")
for i in range(0, days + 1)
]
class RadioField(WTFormsRadioField):
def __init__(self, *args, thing="an option", **kwargs):
super().__init__(*args, **kwargs)
self.thing = thing
self.validate_choice = False
def pre_validate(self, form):
super().pre_validate(form)
if self.data not in dict(self.choices).keys():
raise ValidationError(f"Select {self.thing}")
def email_address(label="Email address", gov_user=True, required=True):
validators = [
ValidEmail(),
]
if gov_user:
validators.append(ValidGovEmail())
if required:
validators.append(DataRequired(message="Cannot be empty"))
return GovukEmailField(label, validators)
class UKMobileNumber(TelField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(UKMobileNumber, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_text_input_field_widget(
self, field, type="tel", param_extensions=param_extensions, **kwargs
)
def pre_validate(self, form):
try:
validate_phone_number(self.data)
except InvalidPhoneError as e:
raise ValidationError(str(e))
class InternationalPhoneNumber(TelField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(InternationalPhoneNumber, self).__init__(
label, validators=validators, **kwargs
)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_text_input_field_widget(
self, field, type="tel", param_extensions=param_extensions, **kwargs
)
def pre_validate(self, form):
try:
2017-11-10 12:35:21 +00:00
if self.data:
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="Cannot be empty")])
def international_phone_number(label="Mobile number"):
return InternationalPhoneNumber(
label, validators=[DataRequired(message="Cannot be empty")]
)
def password(label="Password"):
return GovukPasswordField(
label,
validators=[
DataRequired(message="Cannot be empty"),
Length(8, 255, message="Must be at least 8 characters"),
CommonlyUsedPassword(message="Choose a password thats harder to guess"),
],
)
def govuk_text_input_field_widget(
self, field, type=None, param_extensions=None, **kwargs
):
Fix ignoring submitted data that was falsey This was causing some tests for the "estimate_volume" endpoint to fail due to the surprising way that form handles "''": - The form is the exclusive user of the ForgivingIntegerField [^1]. - The field secretly/silently converts "''" to the integer 0 [^2]. If the validations fail, we don't want to surprise the user with a "0" when they didn't enter one. The field already handles this by massaging the values in the __call__ method that generates the HTML for the form [^3]. However, there are two scenarios: - User submits field with '' - converted to integer 0. - User submits field with '0' - remains as a string. In the case where "value" is "''", the parent class will use the converted value from form.data instead [^4]. This seems to be an oversight and so we get either the integer 0 (from form.data) or the string '0' (from the value kwarg). Complicado! Previously it was a fluke that we avoided replaying the integer 0 to the user; the previous commit removes the fluke. This fixes the conditional to always use the data in the "value" kwarg if it has been provided, as it's meant to override "form.data". [^1]: https://github.com/alphagov/notifications-admin/commit/9f634493841d3dad54ca709f4a688dabb207fd15 [^2]: https://github.com/alphagov/notifications-admin/blob/a22b8cf684b5581aad54d6e36d98e498085925a5/app/main/forms.py#L364 [^3]: https://github.com/alphagov/notifications-admin/blob/a22b8cf684b5581aad54d6e36d98e498085925a5/app/main/forms.py#L393 [^4]: https://github.com/alphagov/notifications-admin/commit/a22b8cf684b5581aad54d6e36d98e498085925a5#diff-a1c8d24b22d4478fe71f75fd43b71b18dd82aae97bc63de84473a6da1902909bR215
2022-03-07 12:40:18 +00:00
value = kwargs["value"] if "value" in kwargs else field.data
value = str(value) if isinstance(value, Number) else value
# error messages
error_message = None
if field.errors:
error_message_format = (
"html" if kwargs.get("error_message_with_html") else "text"
)
error_message = {
"attributes": {
"data-module": "track-error",
"data-error-type": field.errors[0],
"data-error-label": field.name,
},
error_message_format: field.errors[0],
}
# convert to parameters that usa understands
params = {
"errorMessage": error_message,
"id": field.id,
"label": {"text": field.label.text},
"name": field.name,
"value": value,
}
if type:
params["type"] = type
# extend default params with any sent in
merge_jsonlike(params, self.param_extensions)
# add any sent in through use in templates
merge_jsonlike(params, param_extensions)
return Markup(
render_template("components/components/input/template.njk", params=params)
)
class GovukTextInputField(StringField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukTextInputField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, **kwargs):
return govuk_text_input_field_widget(self, field, **kwargs)
class GovukPasswordField(PasswordField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukPasswordField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_text_input_field_widget(
self, field, type="password", param_extensions=param_extensions, **kwargs
)
class GovukEmailField(EmailField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukEmailField, self).__init__(label, validators=validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
params = {
"attributes": {"spellcheck": "false"}
} # email addresses don't need to be spellchecked
merge_jsonlike(params, param_extensions)
return govuk_text_input_field_widget(
self, field, type="email", param_extensions=params, **kwargs
)
class GovukSearchField(SearchField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukSearchField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
params = {"classes": ""} # email addresses don't need to be spellchecked
merge_jsonlike(params, param_extensions)
return govuk_text_input_field_widget(
self, field, type="search", param_extensions=params, **kwargs
)
2020-08-07 10:24:34 +01:00
class GovukDateField(DateField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
2020-08-07 10:24:34 +01:00
super(GovukDateField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_text_input_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
2020-08-07 10:24:34 +01:00
class GovukIntegerField(IntegerField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukIntegerField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_text_input_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
class SMSCode(GovukTextInputField):
validators = [
DataRequired(message="Cannot be empty"),
Regexp(regex=r"^\d+$", message="Numbers only"),
Length(min=6, message="Not enough numbers"),
Length(max=6, message="Too many numbers"),
]
def __call__(self, **kwargs):
params = {"attributes": {"pattern": "[0-9]*"}}
if "param_extensions" in kwargs:
merge_jsonlike(kwargs["param_extensions"], params)
return super().__call__(type="tel", **kwargs)
def process_formdata(self, valuelist):
if valuelist:
self.data = InsensitiveDict.make_key(valuelist[0])
class ForgivingIntegerField(GovukTextInputField):
# Actual value is 2147483647 but this is a scary looking arbitrary number
POSTGRES_MAX_INT = 2000000000
def __init__(self, label=None, things="items", format_error_suffix="", **kwargs):
self.things = things
self.format_error_suffix = format_error_suffix
super().__init__(label, **kwargs)
def process_formdata(self, valuelist):
if valuelist:
value = valuelist[0].replace(",", "").replace(" ", "")
try:
value = int(value)
except ValueError:
pass
if value == "":
value = 0
return super().process_formdata([value])
def pre_validate(self, form):
if self.data:
error = None
try:
if int(self.data) > self.POSTGRES_MAX_INT:
error = "Number of {} must be {} or less".format(
self.things,
format_thousands(self.POSTGRES_MAX_INT),
)
except ValueError:
error = "Enter the number of {} {}".format(
self.things,
self.format_error_suffix,
)
if error:
raise ValidationError(error)
return super().pre_validate(form)
def __call__(self, **kwargs):
if self.get_form().is_submitted() and not self.get_form().validate():
return super().__call__(value=(self.raw_data or [None])[0], **kwargs)
try:
value = int(self.data)
value = format_thousands(value)
except (ValueError, TypeError):
value = self.data if self.data is not None else ""
return super().__call__(value=value, **kwargs)
class FieldWithNoneOption:
# This is a special value that is specific to our forms. This is
# more expicit than casting `None` to a string `'None'` which can
# have unexpected edge cases
NONE_OPTION_VALUE = "__NONE__"
# When receiving Python data, eg when instantiating the form object
# we want to convert that data to our special value, so that it gets
# recognised as being one of the valid choices
def process_data(self, value):
self.data = self.NONE_OPTION_VALUE if value is None else value
# After validation we want to convert it back to a Python `None` for
# use elsewhere, eg posting to the API
def post_validate(self, form, validation_stopped):
if self.data == self.NONE_OPTION_VALUE and not validation_stopped:
self.data = None
class RadioFieldWithNoneOption(FieldWithNoneOption, RadioField):
pass
class NestedFieldMixin:
def children(self):
# start map with root option as a single child entry
child_map = {
None: [option for option in self if option.data == self.NONE_OPTION_VALUE]
}
# add entries for all other children
for option in self:
# assign all options with a NONE_OPTION_VALUE (not always None) to the None key
if option.data == self.NONE_OPTION_VALUE:
child_ids = [
folder["id"]
for folder in self.all_template_folders
if folder["parent_id"] is None
]
key = self.NONE_OPTION_VALUE
else:
child_ids = [
folder["id"]
for folder in self.all_template_folders
if folder["parent_id"] == option.data
]
key = option.data
child_map[key] = [option for option in self if option.data in child_ids]
return child_map
# to be used as the only version of .children once radios are converted
@cached_property
def _children(self):
return self.children()
def get_items_from_options(self, field):
items = []
for option in self._children[None]:
item = self.get_item_from_option(option)
if option.data in self._children:
item["children"] = self.render_children(
field.name, option.label.text, self._children[option.data]
)
items.append(item)
return items
def render_children(self, name, label, options):
params = {
"name": name,
"fieldset": {"legend": {"text": label, "classes": "usa-sr-only"}},
"formGroup": {"classes": "usa-form-group--nested"},
"asList": True,
"items": [],
}
for option in options:
item = self.get_item_from_option(option)
if len(self._children[option.data]):
item["children"] = self.render_children(
name, option.label.text, self._children[option.data]
)
params["items"].append(item)
return render_template("forms/fields/checkboxes/template.njk", params=params)
class NestedRadioField(RadioFieldWithNoneOption, NestedFieldMixin):
pass
class NestedCheckboxesField(SelectMultipleField, NestedFieldMixin):
NONE_OPTION_VALUE = None
class HiddenFieldWithNoneOption(FieldWithNoneOption, HiddenField):
pass
class RadioFieldWithRequiredMessage(RadioField):
def __init__(self, *args, required_message="Not a valid choice", **kwargs):
self.required_message = required_message
super().__init__(*args, **kwargs)
def pre_validate(self, form):
try:
return super().pre_validate(form)
except ValueError:
raise ValidationError(self.required_message)
class StripWhitespaceForm(Form):
class Meta:
def bind_field(self, form, unbound_field, options):
# FieldList simply doesn't support filters.
# @see: https://github.com/wtforms/wtforms/issues/148
no_filter_fields = (FieldList, PasswordField, GovukPasswordField)
filters = (
[strip_all_whitespace]
if not issubclass(unbound_field.field_class, no_filter_fields)
else []
)
filters += unbound_field.kwargs.get("filters", [])
bound = unbound_field.bind(form=form, filters=filters, **options)
bound.get_form = weakref.ref(
form
) # GC won't collect the form if we don't use a weakref
return bound
def render_field(self, field, render_kw):
render_kw.setdefault("required", False)
return super().render_field(field, render_kw)
Convert ListEntry component to use new fields ListEntry component uses FieldList field to group textboxes. Textboxes can be text inputs, email fields or international phone number fields. This converts all field-lists to use: - GovukTextInputField - GovukEmailField - InternationalPhoneNumber Affects these forms: - OrganisationDomainsForm - GuestList Also changes to related Javascript: Update list-entry JS tests to match new HTML Updates the HTML the JS operates on in the test (a fixture representing the HTML in the page on load) to match the new GOVUK Frontend we are generating. Make list-entry JS work with GOVUK Frontend HTML The existing list-entry JS did a few things that clashed with how the new HTML works: - added a 'input-' prefix to the id attributes of all text-inputs - did not make its name and id attributes values match The new HTML has id and name attributes that match so these changes remove the prefix for id attributes and makes them match the name attribute. To understand these changes, it is useful to know how the values for id and name attributes are generated: 1. the id attribute for the component element is stored 2. the 'list-entry-' prefix is removed and the remainder is used to generate ids For example, if the component's id is 'list-entry-domains', the id will be 'domains-1', where the text-input is the first one. This also adds some logic to the HoganJS template to make the value attribute optional, so it is only added if it has a non-null value. This matches the behaviour of the text-input component used in the new list-entry component. Also change whitelist references to guestlist in tests - we forgot to do it earlier, when we moved from calling this feature whitelist to calling it guestlist.
2020-08-07 10:48:21 +01:00
class StripWhitespaceStringField(GovukTextInputField):
def __init__(self, label=None, param_extensions=None, **kwargs):
kwargs["filters"] = tuple(
chain(
kwargs.get("filters", ()),
(strip_all_whitespace,),
)
)
Convert ListEntry component to use new fields ListEntry component uses FieldList field to group textboxes. Textboxes can be text inputs, email fields or international phone number fields. This converts all field-lists to use: - GovukTextInputField - GovukEmailField - InternationalPhoneNumber Affects these forms: - OrganisationDomainsForm - GuestList Also changes to related Javascript: Update list-entry JS tests to match new HTML Updates the HTML the JS operates on in the test (a fixture representing the HTML in the page on load) to match the new GOVUK Frontend we are generating. Make list-entry JS work with GOVUK Frontend HTML The existing list-entry JS did a few things that clashed with how the new HTML works: - added a 'input-' prefix to the id attributes of all text-inputs - did not make its name and id attributes values match The new HTML has id and name attributes that match so these changes remove the prefix for id attributes and makes them match the name attribute. To understand these changes, it is useful to know how the values for id and name attributes are generated: 1. the id attribute for the component element is stored 2. the 'list-entry-' prefix is removed and the remainder is used to generate ids For example, if the component's id is 'list-entry-domains', the id will be 'domains-1', where the text-input is the first one. This also adds some logic to the HoganJS template to make the value attribute optional, so it is only added if it has a non-null value. This matches the behaviour of the text-input component used in the new list-entry component. Also change whitelist references to guestlist in tests - we forgot to do it earlier, when we moved from calling this feature whitelist to calling it guestlist.
2020-08-07 10:48:21 +01:00
super(GovukTextInputField, self).__init__(label, **kwargs)
self.param_extensions = param_extensions
class LoginForm(StripWhitespaceForm):
email_address = GovukEmailField(
"Email address",
validators=[
Length(min=5, max=255),
DataRequired(message="Cannot be empty"),
ValidEmail(),
],
)
password = GovukPasswordField(
"Password", validators=[DataRequired(message="Enter your password")]
)
class RegisterUserForm(StripWhitespaceForm):
name = GovukTextInputField(
"Full name", validators=[DataRequired(message="Cannot be empty")]
)
email_address = email_address()
mobile_number = international_phone_number()
password = password()
# always register as sms type
auth_type = HiddenField("auth_type", default="sms_auth")
class RegisterUserFromInviteForm(RegisterUserForm):
def __init__(self, invited_user):
super().__init__(
service=invited_user.service,
email_address=invited_user.email_address,
auth_type=invited_user.auth_type,
name=guess_name_from_email_address(invited_user.email_address),
)
mobile_number = InternationalPhoneNumber("Mobile number", validators=[])
service = HiddenField("service")
email_address = HiddenField("email_address")
auth_type = HiddenField("auth_type", validators=[DataRequired()])
2017-11-10 12:35:21 +00:00
def validate_mobile_number(self, field):
if self.auth_type.data == "sms_auth" and not field.data:
raise ValidationError("Cannot be empty")
2017-11-10 12:35:21 +00:00
2018-02-19 16:53:29 +00:00
class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
def __init__(self, invited_org_user):
super().__init__(
2023-07-12 12:09:44 -04:00
organization=invited_org_user.organization,
email_address=invited_org_user.email_address,
2018-02-19 16:53:29 +00:00
)
name = GovukTextInputField(
"Full name", validators=[DataRequired(message="Cannot be empty")]
2018-02-19 16:53:29 +00:00
)
mobile_number = InternationalPhoneNumber(
"Mobile number", validators=[DataRequired(message="Cannot be empty")]
)
2018-02-19 16:53:29 +00:00
password = password()
organization = HiddenField("organization")
email_address = HiddenField("email_address")
auth_type = HiddenField("auth_type", validators=[DataRequired()])
2018-02-19 16:53:29 +00:00
def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
# error messages
error_message = None
if field.errors:
error_message = {
"attributes": {
"data-module": "track-error",
"data-error-type": field.errors[0],
"data-error-label": field.name,
},
"text": field.errors[0],
}
params = {
"name": field.name,
"errorMessage": error_message,
"items": [
{
"name": field.name,
"id": field.id,
"text": field.label.text,
"value": "y",
"checked": field.data,
}
],
}
# extend default params with any sent in during instantiation
if self.param_extensions:
merge_jsonlike(params, self.param_extensions)
# add any sent in though use in templates
if param_extensions:
merge_jsonlike(params, param_extensions)
return Markup(render_template("forms/fields/checkboxes/macro.njk", params=params))
def govuk_checkboxes_field_widget(
self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs
):
def _wrap_in_collapsible(field_label, checkboxes_string):
# wrap the checkboxes HTML in the HTML needed by the collapisble JS
result = Markup(
f'<div class="selection-wrapper"'
f' data-module="collapsible-checkboxes"'
f' data-field-label="{field_label}">'
f" {checkboxes_string}"
f"</div>"
)
return result
# error messages
error_message = None
if field.errors:
error_message = {
"attributes": {
"data-module": "track-error",
"data-error-type": field.errors[0],
"data-error-label": field.name,
},
"text": field.errors[0],
}
# returns either a list or a hierarchy of lists
# depending on how get_items_from_options is implemented
items = self.get_items_from_options(field)
params = {
"name": field.name,
"fieldset": {
"attributes": {"id": field.name},
"legend": {
"text": field.label.text,
},
},
"asList": self.render_as_list,
"errorMessage": error_message,
"items": items,
}
# extend default params with any sent in during instantiation
if self.param_extensions:
merge_jsonlike(params, self.param_extensions)
# add any sent in though use in templates
if param_extensions:
merge_jsonlike(params, param_extensions)
if wrap_in_collapsible:
# add a blank hint to act as an ARIA live-region
params.update(
{
"hint": {
"html": '<div class="selection-summary" role="region" aria-live="polite"></div>'
}
}
)
return _wrap_in_collapsible(
self.field_label,
Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)),
2022-08-05 00:25:03 -07:00
)
else:
return Markup(
render_template("forms/fields/checkboxes/macro.njk", params=params)
)
def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
# error messages
error_message = None
if field.errors:
error_message = {
"attributes": {
"data-module": "track-error",
"data-error-type": field.errors[0],
"data-error-label": field.name,
},
"text": field.errors[0],
}
# returns either a list or a hierarchy of lists
# depending on how get_items_from_options is implemented
items = self.get_items_from_options(field)
params = {
"name": field.name,
"fieldset": {
"attributes": {"id": field.name},
"legend": {
"text": field.label.text,
2023-08-28 12:29:25 -04:00
"classes": "usa-legend text-bold",
},
},
"errorMessage": error_message,
"items": items,
}
# extend default params with any sent in during instantiation
if self.param_extensions:
merge_jsonlike(params, self.param_extensions)
# add any sent in though use in templates
if param_extensions:
merge_jsonlike(params, param_extensions)
return Markup(
render_template("components/components/radios/template.njk", params=params)
)
class GovukCheckboxField(BooleanField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukCheckboxField, self).__init__(
label, validators, false_values=None, **kwargs
)
self.param_extensions = param_extensions
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_checkbox_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
class GovukCheckboxesField(SelectMultipleField):
render_as_list = False
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukCheckboxesField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
def get_item_from_option(self, option):
return {
"name": option.name,
"id": option.id,
"text": option.label.text,
"value": str(option.data), # to protect against non-string types like uuids
"checked": option.checked,
}
def get_items_from_options(self, field):
return [self.get_item_from_option(option) for option in field]
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_checkboxes_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# Wraps checkboxes rendering in HTML needed by the collapsible JS
class GovukCollapsibleCheckboxesField(GovukCheckboxesField):
def __init__(
self, label="", validators=None, field_label="", param_extensions=None, **kwargs
):
super(GovukCollapsibleCheckboxesField, self).__init__(
label, validators, param_extensions, **kwargs
)
self.field_label = field_label
def widget(self, field, **kwargs):
return govuk_checkboxes_field_widget(
self, field, wrap_in_collapsible=True, param_extensions=None, **kwargs
)
# GovukCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
# collapsible JS
# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
class GovukCollapsibleNestedCheckboxesField(
NestedFieldMixin, GovukCollapsibleCheckboxesField
):
NONE_OPTION_VALUE = None
render_as_list = True
class GovukRadiosField(RadioField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(GovukRadiosField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
def get_item_from_option(self, option):
return {
"name": option.name,
"id": option.id,
"text": option.label.text,
"value": str(option.data), # to protect against non-string types like uuids
"checked": option.checked,
}
def get_items_from_options(self, field):
return [self.get_item_from_option(option) for option in field]
# self.__call__ renders the HTML for the field by:
# 1. delegating to self.meta.render_field which
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return govuk_radios_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
class OptionalGovukRadiosField(GovukRadiosField):
def pre_validate(self, form):
if self.data is None:
return
super().pre_validate(form)
class OnOffField(GovukRadiosField):
def __init__(self, label, choices=None, *args, **kwargs):
choices = choices or [
(True, "On"),
(False, "Off"),
]
super().__init__(
label,
*args,
choices=choices,
thing=f"{choices[0][1].lower()} or {choices[1][1].lower()}",
**kwargs,
)
def process_formdata(self, valuelist):
if valuelist:
value = valuelist[0]
self.data = (value == "True") if value in ["True", "False"] else value
def iter_choices(self):
for value, label in self.choices:
# This overrides WTForms default behaviour which is to check
# self.coerce(value) == self.data
# where self.coerce returns a string for a boolean input
yield (value, label, (self.data in {value, self.coerce(value)}))
2023-07-12 12:09:44 -04:00
class OrganizationTypeField(GovukRadiosField):
def __init__(self, *args, include_only=None, validators=None, **kwargs):
super().__init__(
*args,
choices=[
(value, label)
for value, label in Organization.TYPE_LABELS.items()
if not include_only or value in include_only
],
thing="the type of organization",
validators=validators or [],
**kwargs,
)
2020-06-26 22:32:42 +01:00
class GovukRadiosFieldWithNoneOption(FieldWithNoneOption, GovukRadiosField):
pass
# guard against data entries that aren't a known permission
def filter_by_permissions(valuelist):
if valuelist is None:
return None
else:
return [
entry
for entry in valuelist
if any(entry in option for option in permission_options)
]
class AuthTypeForm(StripWhitespaceForm):
auth_type = GovukRadiosField(
"Sign in using",
choices=[
("sms_auth", format_auth_type("sms_auth")),
("email_auth", format_auth_type("email_auth")),
],
)
class BasePermissionsForm(StripWhitespaceForm):
def __init__(self, all_template_folders=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.folder_permissions.choices = []
if all_template_folders is not None:
self.folder_permissions.all_template_folders = all_template_folders
self.folder_permissions.choices = [
(item["id"], item["name"])
for item in ([{"name": "Templates", "id": None}] + all_template_folders)
]
folder_permissions = GovukCollapsibleNestedCheckboxesField(
"Folders this team member can see", field_label="folder"
)
login_authentication = GovukRadiosField(
"Sign in using",
choices=[
("sms_auth", "Text message code"),
("email_auth", "Email link"),
],
thing="how this team member should sign in",
validators=[DataRequired()],
)
2016-03-03 13:00:12 +00:00
permissions_field = GovukCheckboxesField(
"Permissions",
filters=[filter_by_permissions],
choices=[(value, label) for value, label in permission_options],
param_extensions={"hint": {"text": "All team members can see sent messages."}},
)
@property
def permissions(self):
return set(self.permissions_field.data)
@classmethod
def from_user(cls, user, service_id, **kwargs):
fix bug stopping editing of permissions of webauthn platform admins We hide the radio field in the HTML for platform admins, as we don't want anyone to be able to change their auth type. However, when the form is validated, the form has a field called login_authentication that it expects a value for. It silently fails as it complains that when the user POSTed they didn't select a value for that radio field, but the error message is on the radio fields that don't get displayed to the user so they'd never know. Fixing this is actually pretty hard. We use this form in two places, one where we have a user to edit, one where we are creating an invite from scratch. So sometimes we don't know about a user's auth type. In addition, radio buttons are mandatory by design, but now sometimes we don't just want to make it optional but explicitly ignore the value being passed in? To solve this, remove the field entirely from the form if the user is a platform admin. This means that if the code in manage_users.py tries to access the login_authentication value from the form, it'll error, but I think that's okay to leave for now given we concede that this isn't a perfect final solution. The tests didn't flag this previously as they tried to set from sms_auth (the default for `platform_admin_user`) TO email_auth or sms_auth. Also, the diagnosis of this bug was confounded further by the fact that `mock_get_users_by_service` sets what is returned by the API - the service model then takes the IDs out of that response and calls `User.get_user_by_id` for the matching ID (as in, the code only uses get_users_by_service to ensure the user belongs to that service). This means that we accidentally set the form editing the current user, as when we log in we set `get_user_by_id` to return the user of our choice
2021-06-10 23:09:36 +01:00
form = cls(
**kwargs,
**{
"permissions_field": (
user.permissions_for_service(service_id) & all_ui_permissions
)
},
login_authentication=user.auth_type,
)
fix bug stopping editing of permissions of webauthn platform admins We hide the radio field in the HTML for platform admins, as we don't want anyone to be able to change their auth type. However, when the form is validated, the form has a field called login_authentication that it expects a value for. It silently fails as it complains that when the user POSTed they didn't select a value for that radio field, but the error message is on the radio fields that don't get displayed to the user so they'd never know. Fixing this is actually pretty hard. We use this form in two places, one where we have a user to edit, one where we are creating an invite from scratch. So sometimes we don't know about a user's auth type. In addition, radio buttons are mandatory by design, but now sometimes we don't just want to make it optional but explicitly ignore the value being passed in? To solve this, remove the field entirely from the form if the user is a platform admin. This means that if the code in manage_users.py tries to access the login_authentication value from the form, it'll error, but I think that's okay to leave for now given we concede that this isn't a perfect final solution. The tests didn't flag this previously as they tried to set from sms_auth (the default for `platform_admin_user`) TO email_auth or sms_auth. Also, the diagnosis of this bug was confounded further by the fact that `mock_get_users_by_service` sets what is returned by the API - the service model then takes the IDs out of that response and calls `User.get_user_by_id` for the matching ID (as in, the code only uses get_users_by_service to ensure the user belongs to that service). This means that we accidentally set the form editing the current user, as when we log in we set `get_user_by_id` to return the user of our choice
2021-06-10 23:09:36 +01:00
return form
class PermissionsForm(BasePermissionsForm):
pass
class BaseInviteUserForm:
email_address = email_address(gov_user=False)
def __init__(self, inviter_email_address, *args, **kwargs):
super().__init__(*args, **kwargs)
self.inviter_email_address = inviter_email_address
def validate_email_address(self, field):
if current_user.platform_admin:
return
if field.data.lower() == self.inviter_email_address.lower():
2019-09-12 16:42:33 +01:00
raise ValidationError("You cannot send an invitation to yourself")
class InviteUserForm(BaseInviteUserForm, PermissionsForm):
pass
class InviteOrgUserForm(BaseInviteUserForm, StripWhitespaceForm):
pass
2018-02-19 16:53:29 +00:00
class TwoFactorForm(StripWhitespaceForm):
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 = SMSCode("Text message code")
2023-04-26 11:24:01 -04:00
def validate(self, extra_validators=None):
if not self.sms_code.validate(self):
return False
is_valid, reason = self.validate_code_func(self.sms_code.data)
if not is_valid:
self.sms_code.errors.append(reason)
return False
return True
class TextNotReceivedForm(StripWhitespaceForm):
mobile_number = international_phone_number()
class RenameServiceForm(StripWhitespaceForm):
name = GovukTextInputField(
"Service name",
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(max=255, message="Service name must be 255 characters or fewer"),
],
)
2023-07-12 12:09:44 -04:00
class RenameOrganizationForm(StripWhitespaceForm):
name = GovukTextInputField(
"Organization name",
2018-03-06 17:12:31 +00:00
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(
max=255, message="Organization name must be 255 characters or fewer"
),
],
)
2018-03-06 17:12:31 +00:00
2023-07-12 12:09:44 -04:00
class OrganizationOrganizationTypeForm(StripWhitespaceForm):
organization_type = OrganizationTypeField("What type of organization is this?")
2023-07-12 12:09:44 -04:00
class AdminOrganizationDomainsForm(StripWhitespaceForm):
def populate(self, domains_list):
for index, value in enumerate(domains_list):
self.domains[index].data = value
domains = FieldList(
StripWhitespaceStringField(
"",
validators=[
Optional(),
],
default="",
),
min_entries=20,
max_entries=20,
label="Domain names",
)
class CreateServiceForm(StripWhitespaceForm):
name = GovukTextInputField(
"Whats your service called?",
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(max=255, message="Service name must be 255 characters or fewer"),
],
)
organization_type = OrganizationTypeField("Where is this service run?")
2023-07-12 12:09:44 -04:00
class AdminNewOrganizationForm(
RenameOrganizationForm, OrganizationOrganizationTypeForm
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class AdminServiceSMSAllowanceForm(StripWhitespaceForm):
free_sms_allowance = GovukIntegerField(
"Numbers of text message fragments per year",
validators=[InputRequired(message="Cannot be empty")],
)
class AdminServiceMessageLimitForm(StripWhitespaceForm):
message_limit = GovukIntegerField(
"Max number of messages the service has per send",
validators=[DataRequired(message="Cannot be empty")],
)
class AdminServiceRateLimitForm(StripWhitespaceForm):
rate_limit = GovukIntegerField(
"Number of messages the service can send in a rolling 60 second window",
validators=[DataRequired(message="Cannot be empty")],
)
class ConfirmPasswordForm(StripWhitespaceForm):
def __init__(self, validate_password_func, *args, **kwargs):
self.validate_password_func = validate_password_func
super(ConfirmPasswordForm, self).__init__(*args, **kwargs)
password = GovukPasswordField("Enter password")
def validate_password(self, field):
if not self.validate_password_func(field.data):
raise ValidationError("Invalid password")
class BaseTemplateForm(StripWhitespaceForm):
name = GovukTextInputField(
"Template name", validators=[DataRequired(message="Cannot be empty")]
)
template_content = TextAreaField(
"Message",
validators=[DataRequired(message="Cannot be empty"), NoCommasInPlaceHolders()],
)
process_type = HiddenField("normal")
2017-02-15 15:06:47 +00:00
class SMSTemplateForm(BaseTemplateForm):
def validate_template_content(self, field):
OnlySMSCharacters(template_type="sms")(None, field)
2017-02-15 15:06:47 +00:00
class EmailTemplateForm(BaseTemplateForm):
subject = TextAreaField(
"Subject", validators=[DataRequired(message="Cannot be empty")]
)
class ForgotPasswordForm(StripWhitespaceForm):
email_address = email_address(gov_user=False)
2016-01-04 14:00:39 +00:00
class NewPasswordForm(StripWhitespaceForm):
new_password = password()
class ChangePasswordForm(StripWhitespaceForm):
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(StripWhitespaceForm):
file = FileField(
"Add recipients",
validators=[
DataRequired(message="Please pick a file"),
CsvFileValidator(),
FileSize(max_size=10e6, message="File must be smaller than 10Mb"), # 10Mb
],
)
class ChangeNameForm(StripWhitespaceForm):
new_name = GovukTextInputField("Your name")
2024-01-08 10:13:26 -08:00
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
2023-12-04 14:52:48 -08:00
class ChangePreferredTimezoneForm(StripWhitespaceForm):
def __init__(self, *args, **kwargs):
super(ChangePreferredTimezoneForm, self).__init__(*args, **kwargs)
2024-02-08 15:10:07 -05:00
self.new_preferred_timezone.choices = [
("America/Puerto_Rico", "America/Puerto_Rico"),
("US/Eastern", "US/Eastern"),
("US/Central", "US/Central"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Alaska", "US/Alaska"),
("US/Hawaii", "US/Hawaii"),
("US/Aleutian", "US/Aleutian"),
("US/Samoa", "US/Samoa"),
]
2023-12-04 14:52:48 -08:00
2024-02-08 15:10:07 -05:00
new_preferred_timezone = GovukRadiosField(
"What timezone would you like to use?",
)
2023-12-04 14:52:48 -08:00
class ChangeEmailForm(StripWhitespaceForm):
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):
# The validate_email_func can be used to call API to check if the email address is already in
# use. We don't want to run that check for invalid email addresses, since that will cause an error.
# If there are any other validation errors on the email_address, we should skip this check.
if self.email_address.errors:
return
is_valid = self.validate_email_func(field.data)
2018-02-19 16:53:29 +00:00
if is_valid:
raise ValidationError("The email address is already in use")
class ChangeNonGovEmailForm(ChangeEmailForm):
email_address = email_address(gov_user=False)
class ChangeMobileNumberForm(StripWhitespaceForm):
mobile_number = international_phone_number()
class ChooseTimeForm(StripWhitespaceForm):
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 = GovukRadiosField(
"When should Notify send these messages?",
default="",
)
class CreateKeyForm(StripWhitespaceForm):
def __init__(self, existing_keys, *args, **kwargs):
self.existing_key_names = [
key["name"].lower() for key in existing_keys if not key["expiry_date"]
]
super().__init__(*args, **kwargs)
key_type = GovukRadiosField(
"Type of key",
thing="the type of key",
)
key_name = GovukTextInputField(
"Name for this 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 EstimateUsageForm(StripWhitespaceForm):
volume_email = ForgivingIntegerField(
"How many emails do you expect to send in the next year?",
things="emails",
format_error_suffix="you expect to send",
)
volume_sms = ForgivingIntegerField(
"How many text messages do you expect to send in the next year?",
things="text messages",
format_error_suffix="you expect to send",
2016-10-24 13:38:55 +01:00
)
consent_to_research = GovukRadiosField(
"Can we contact you when were doing user research?",
choices=[
("yes", "Yes"),
("no", "No"),
],
thing="yes or no",
param_extensions={
"hint": {
"text": "You do not have to take part and you can unsubscribe at any time"
}
},
)
at_least_one_volume_filled = True
def validate(self, *args, **kwargs):
2022-12-05 15:33:44 -05:00
if self.volume_email.data == self.volume_sms.data == 0:
self.at_least_one_volume_filled = False
return False
return super().validate(*args, **kwargs)
class ServiceContactDetailsForm(StripWhitespaceForm):
contact_details_type = RadioField(
"Type of contact details",
choices=[
("url", "Link"),
("email_address", "Email address"),
("phone_number", "Phone number"),
],
)
url = GovukTextInputField("URL")
email_address = GovukEmailField("Email address")
# This is a text field because the number provided by the user can also be a short code
phone_number = GovukTextInputField("Phone number")
2023-04-26 11:24:01 -04:00
def validate(self, extra_validators=None):
if self.contact_details_type.data == "url":
self.url.validators = [DataRequired(), URL(message="Must be a valid URL")]
elif self.contact_details_type.data == "email_address":
self.email_address.validators = [
DataRequired(),
Length(min=5, max=255),
ValidEmail(),
]
elif self.contact_details_type.data == "phone_number":
def valid_phone_number(self, num):
try:
validate_phone_number(num.data, international=True)
return True
except InvalidPhoneError:
raise ValidationError("Must be a valid phone number")
self.phone_number.validators = [
DataRequired(),
Length(min=5, max=20),
valid_phone_number,
]
2023-04-26 11:24:01 -04:00
return super().validate(extra_validators)
class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label="Reply-to email address", gov_user=False)
is_default = GovukCheckboxField("Make this email address the default")
class ServiceSmsSenderForm(StripWhitespaceForm):
sms_sender = GovukTextInputField(
"Text message sender",
validators=[
2019-09-12 16:42:33 +01:00
DataRequired(message="Cannot be empty"),
Length(max=11, message="Enter 11 characters or fewer"),
Length(min=3, message="Enter 3 characters or more"),
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly(),
DoesNotStartWithDoubleZero(),
],
)
is_default = GovukCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm):
is_default = GovukCheckboxField("Make this text message sender the default")
2017-10-30 14:30:43 +00:00
class AdminNotesForm(StripWhitespaceForm):
notes = TextAreaField(validators=[])
class AdminBillingDetailsForm(StripWhitespaceForm):
billing_contact_email_addresses = GovukTextInputField("Contact email addresses")
billing_contact_names = GovukTextInputField("Contact names")
billing_reference = GovukTextInputField("Reference")
purchase_order_number = GovukTextInputField("Purchase order number")
notes = TextAreaField(validators=[])
class ServiceOnOffSettingForm(StripWhitespaceForm):
def __init__(self, name, *args, truthy="On", falsey="Off", **kwargs):
super().__init__(*args, **kwargs)
self.enabled.label.text = name
self.enabled.choices = [
(True, truthy),
(False, falsey),
]
enabled = OnOffField("Choices")
class ServiceSwitchChannelForm(ServiceOnOffSettingForm):
def __init__(self, channel, *args, **kwargs):
name = "Send {}".format(
{
"email": "emails",
"sms": "text messages",
}.get(channel)
)
super().__init__(name, *args, **kwargs)
class SVGFileUpload(StripWhitespaceForm):
file = FileField_wtf(
"Upload an SVG logo",
validators=[
FileAllowed(["svg"], "SVG Images only!"),
DataRequired(message="You need to upload a file to submit"),
NoEmbeddedImagesInSVG(),
NoTextInSVG(),
],
)
Convert ListEntry component to use new fields ListEntry component uses FieldList field to group textboxes. Textboxes can be text inputs, email fields or international phone number fields. This converts all field-lists to use: - GovukTextInputField - GovukEmailField - InternationalPhoneNumber Affects these forms: - OrganisationDomainsForm - GuestList Also changes to related Javascript: Update list-entry JS tests to match new HTML Updates the HTML the JS operates on in the test (a fixture representing the HTML in the page on load) to match the new GOVUK Frontend we are generating. Make list-entry JS work with GOVUK Frontend HTML The existing list-entry JS did a few things that clashed with how the new HTML works: - added a 'input-' prefix to the id attributes of all text-inputs - did not make its name and id attributes values match The new HTML has id and name attributes that match so these changes remove the prefix for id attributes and makes them match the name attribute. To understand these changes, it is useful to know how the values for id and name attributes are generated: 1. the id attribute for the component element is stored 2. the 'list-entry-' prefix is removed and the remainder is used to generate ids For example, if the component's id is 'list-entry-domains', the id will be 'domains-1', where the text-input is the first one. This also adds some logic to the HoganJS template to make the value attribute optional, so it is only added if it has a non-null value. This matches the behaviour of the text-input component used in the new list-entry component. Also change whitelist references to guestlist in tests - we forgot to do it earlier, when we moved from calling this feature whitelist to calling it guestlist.
2020-08-07 10:48:21 +01:00
class EmailFieldInGuestList(GovukEmailField, StripWhitespaceStringField):
pass
class InternationalPhoneNumberInGuestList(
InternationalPhoneNumber, StripWhitespaceStringField
):
pass
class GuestList(StripWhitespaceForm):
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
def populate(self, email_addresses, phone_numbers):
for form_field, existing_guest_list in (
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
(self.email_addresses, email_addresses),
(self.phone_numbers, phone_numbers),
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
):
for index, value in enumerate(existing_guest_list):
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
form_field[index].data = value
email_addresses = FieldList(
EmailFieldInGuestList("", validators=[Optional(), ValidEmail()], default=""),
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
min_entries=5,
max_entries=5,
label="Email addresses",
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
)
phone_numbers = FieldList(
InternationalPhoneNumberInGuestList("", validators=[Optional()], default=""),
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
min_entries=5,
max_entries=5,
label="Mobile numbers",
Add a page to manage a service’s whitelist Services who are in alpha or building prototypes need a way of sending to any email address or phone number without having to sign the MOU. This commit adds a page where they can whitelist up to 5 email addresses and 5 phone numbers. It uses the ‘list entry’ UI pattern from the Digital Marketplace frontend toolkit [1] [2] [3]. I had to do some modification: - of the Javascript, to make it work with the GOV.UK Module pattern - of the template to make it work with WTForms - of the content security policy, because the list entry pattern uses Hogan[1], which needs to use `eval()` (this should be fine if we’re only allowing it for scripts that we serve) - of our SASS lint config, to allow browser-targeting mixins to come after normal rules (so that they can override them) This commit also adds a new form class to validate and populate the two whitelists. The validation is fairly rudimentary at the moment, and doesn’t highlight which item in the list has the error, but it’s probably good enough. The list can only be updated all-at-once, this is how it’s possible to remove items from the list without having to make multiple `POST` requests. 1. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/templates/forms/list-entry.html 2. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/scss/forms/_list-entry.scss 3. https://github.com/alphagov/digitalmarketplace-frontend-toolkit/blob/434ad307913651ecb041ab94bdee748ebe066d1a/toolkit/javascripts/list-entry.js 4. http://twitter.github.io/hogan.js/
2016-09-20 12:30:00 +01:00
)
class DateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("Start Date", [validators.optional()])
end_date = GovukDateField("End Date", [validators.optional()])
include_from_test_key = GovukCheckboxField("Include test keys")
Merge email, text message + letter templates pages Right now we have separate pages for email and text message templates. In the future we will also have a separate page for letter templates. This commit changes Notify to only have one page for all templates. What is the problem? --- The left-hand navigation is getting quite crowded, at 8 items for a service that can send letters. Research suggests that the number of objects an average human can hold in working memory is 7 ± 2 [1]. So we’re at the limit of how many items the navigation should have. In the future we will need to search/sort/filter templates by attributes other than type, for example: - show me the ‘confirmation’ templates - show me the most recently used templates - show me all templates containing the placeholder `((ref_no))` These are hypothetical for now, but these needs (or others) may become real in the future. At this point pre-filtering the list of templates by type would restrict what searches a user could do. So by making this change now we’re in a better position to iterate the design in the future. What’s the change? --- This commit replaces the ‘Email templates’, ‘Text message templates’ and ‘Letter templates’ pages with one page called ‘Templates’. This new templates page shows all the templates for the service, sorted by most recently created first (as before). To add a new template there is a new page with a form asking you what kind of template you want to create. This is necessary because in the past we knew what kind of template you wanted to create based on the kind you were looking at. What’s the impact of this change on new users? --- This change alters the onboarding process slightly. We still want to take people through the empty templates page from the call-to-action on the dashboard because it helps them understand that to send a message using Notify you need a template. But because we don’t have separate pages for emails/text messages we will have to send users through the extra step of choosing what kind of template to create. This is a bit clunkier on first use but: - it still gets the point across - it takes them through the actual flow they will be using to create new templates in the future (ie they’re learning how to use Notify, not just being taken through a special onboarding route) I’m not too worried about this change in terms of the experience for new users. Furthermore, by making it now we get to validate whether it’s causing any problems in the lab research booked for next week. What’s the impact of this change on current services? --- Looking at the top 15 services by number of templates[2], most are using either text messages or emails. So this change would not have a significant impact on these services because the page will not get any longer. In other words we wouldn’t be making it worse for them. Those services who do use both are not using as many templates. The worst-case scenario is SSCS, who have 16 templates, evenly split between email and text messages. So they would go from having 8 templates per page to 16, which is still less than half the number that HMPO or Digital Marketplace are managing. References --- 1. https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two 2. Template usage by service Service name | Template count | Template types ---------------------------------------|----------------|--------------- Her Majesty's Passport Office | 40 | sms Digital Marketplace | 40 | email GovWifi-Staging | 19 | sms GovWifi | 18 | sms Digital Apprenticeship Service | 16 | email SSCS | 16 | both Crown Commercial Service MI Collection | 15 | email Help with Prison Visits | 12 | both Digital Future | 12 | email Export Licensing Service | 11 | email Civil Money Claims | 9 | both DVLA Drivers Medical Service | 9 | sms GOV.UK Notify | 8 | both Manage your benefit overpayments | 8 | both Tax Renewals | 8 | both
2017-02-28 12:16:35 +00:00
class RequiredDateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("Start Date")
end_date = GovukDateField("End Date")
class BillingReportDateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("First day covered by report")
end_date = GovukDateField("Last day covered by report")
class SearchByNameForm(StripWhitespaceForm):
search = GovukSearchField(
"Search by name",
validators=[
DataRequired("You need to enter full or partial name to search by.")
],
)
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
class AdminSearchUsersByEmailForm(StripWhitespaceForm):
search = GovukSearchField(
"Search by name or email address",
validators=[
DataRequired(
"You need to enter full or partial email address to search by."
)
],
)
class SearchUsersForm(StripWhitespaceForm):
search = GovukSearchField("Search by name or email address")
class SearchNotificationsForm(StripWhitespaceForm):
to = GovukSearchField()
labels = {
"email": "Search by email address",
"sms": "Search by phone number",
}
def __init__(self, message_type, *args, **kwargs):
super().__init__(*args, **kwargs)
self.to.label.text = self.labels.get(
message_type,
"Search by phone number or email address",
)
class SearchTemplatesForm(StripWhitespaceForm):
search = GovukSearchField()
def __init__(self, api_keys, *args, **kwargs):
super().__init__(*args, **kwargs)
self.search.label.text = (
"Search by name or ID" if api_keys else "Search by name"
)
class PlaceholderForm(StripWhitespaceForm):
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
pass
class AdminServiceInboundNumberForm(StripWhitespaceForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.inbound_number.choices = kwargs["inbound_number_choices"]
inbound_number = GovukRadiosField(
"Set inbound number",
thing="an inbound number",
)
class CallbackForm(StripWhitespaceForm):
url = GovukTextInputField(
2017-12-08 10:52:38 +00:00
"URL",
validators=[
DataRequired(message="Cannot be empty"),
Regexp(regex="^https.*", message="Must be a valid https URL"),
],
)
bearer_token = GovukPasswordField(
"Bearer token",
validators=[
DataRequired(message="Cannot be empty"),
Length(min=10, message="Must be at least 10 characters"),
],
)
2023-04-26 11:24:01 -04:00
def validate(self, extra_validators=None):
return super().validate(extra_validators) or self.url.data == ""
class SMSPrefixForm(StripWhitespaceForm):
enabled = OnOffField("") # label is assigned on instantiation
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
def get_placeholder_form_instance(
placeholder_name,
dict_to_populate_from,
template_type,
allow_international_phone_numbers=False,
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
):
if (
InsensitiveDict.make_key(placeholder_name) == "emailaddress"
and template_type == "email"
):
field = email_address(label=placeholder_name, gov_user=False)
elif (
InsensitiveDict.make_key(placeholder_name) == "phonenumber"
and template_type == "sms"
):
if allow_international_phone_numbers:
field = international_phone_number(
label=placeholder_name
) # TODO: modify as necessary for non-us numbers
else:
field = uk_mobile_number(
label=placeholder_name
) # TODO: replace with us_mobile_number
else:
field = GovukTextInputField(
placeholder_name, validators=[DataRequired(message="Cannot be empty")]
)
PlaceholderForm.placeholder_value = field
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
return PlaceholderForm(
placeholder_value=dict_to_populate_from.get(placeholder_name, "")
Show one field at a time on send yourself a test The send yourself a test feature is useful for two things: - constructing an email/text message/letter without uploading a CSV file - seeing what the thing your going to send will look like (either by getting it in your inbox or downloading the PDF) - learning the concept of placeholders, ie understanding they’re thing that gets populated with _stuff_ The problem we’re seeing is that the current UI breaks when a template has a lot of placeholders. This is especially apparent with letter templates, which have a minimum of 7 placeholders by virtue of the address. The idea behind having the form fields side-by-side was to help people understand the relationship between their spreadsheet columns and the placeholders. But this means that the page was doing a lot of work, trying to teach: - replacement of placeholders - link between placeholders and spreadsheet columns The latter is better explained by the example spreadsheet shown on the upload page. So it can safely be removed from the send yourself a test page – in other words the fields don’t need to be shown side by side. Showing them one-at-a-time works well because: - it’s really obvious, even on first use, what the page is asking you to do - as your step through each placeholder, you see the message build up with the data you’ve entered – you’re learning how replacement of placeholders works by repetition This also means adding a matching endpoint for viewing each step of making the test letter as a PDF/PNG because we can’t reuse the view of the template without any placeholders filled any more.
2017-05-04 09:30:55 +01:00
)
class SetSenderForm(StripWhitespaceForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sender.choices = kwargs["sender_choices"]
self.sender.label.text = kwargs["sender_label"]
sender = GovukRadiosField()
class SetTemplateSenderForm(StripWhitespaceForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sender.choices = kwargs["sender_choices"]
self.sender.label.text = "Select your sender"
sender = GovukRadiosField()
2018-02-12 09:26:13 +00:00
2023-07-12 12:09:44 -04:00
class AdminSetOrganizationForm(StripWhitespaceForm):
2018-02-12 09:26:13 +00:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.organizations.choices = kwargs["choices"]
2018-02-12 09:26:13 +00:00
2023-07-12 12:09:44 -04:00
organizations = GovukRadiosField(
"Select an organization", validators=[DataRequired()]
2018-02-12 09:26:13 +00:00
)
class AdminServiceAddDataRetentionForm(StripWhitespaceForm):
notification_type = GovukRadiosField(
"What notification type?",
choices=[
("email", "Email"),
("sms", "SMS"),
],
thing="notification type",
)
days_of_retention = GovukIntegerField(
label="Days of retention",
validators=[
validators.NumberRange(min=3, max=90, message="Must be between 3 and 90")
],
)
class AdminServiceEditDataRetentionForm(StripWhitespaceForm):
days_of_retention = GovukIntegerField(
label="Days of retention",
validators=[
validators.NumberRange(min=3, max=90, message="Must be between 3 and 90")
],
)
2018-11-01 16:02:43 +00:00
class TemplateFolderForm(StripWhitespaceForm):
def __init__(self, all_service_users=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if all_service_users is not None:
self.users_with_permission.all_service_users = all_service_users
self.users_with_permission.choices = [
(item.id, item.name) for item in all_service_users
]
users_with_permission = GovukCollapsibleCheckboxesField(
"Team members who can see this folder", field_label="team member"
)
name = GovukTextInputField(
"Folder name", validators=[DataRequired(message="Cannot be empty")]
)
def required_for_ops(*operations):
operations = set(operations)
def validate(form, field):
if form.op not in operations and any(field.raw_data):
# super weird
raise validators.StopValidation("Must be empty")
if form.op in operations and not any(field.raw_data):
raise validators.StopValidation("Cannot be empty")
return validate
class TemplateAndFoldersSelectionForm(Form):
"""
This form expects the form data to include an operation, based on which submit button is clicked.
If enter is pressed, unknown will be sent by a hidden submit button at the top of the form.
The value of this operation affects which fields are required, expected to be empty, or optional.
* unknown
currently not implemented, but in the future will try and work out if there are any obvious commands that can be
assumed based on which fields are empty vs populated.
* move-to-existing-folder
must have data for templates_and_folders checkboxes, and move_to radios
* move-to-new-folder
must have data for move_to_new_folder_name, cannot have data for move_to_existing_folder_name
* add-new-folder
must have data for move_to_existing_folder_name, cannot have data for move_to_new_folder_name
"""
ALL_TEMPLATES_FOLDER = {
"name": "Templates",
"id": RadioFieldWithNoneOption.NONE_OPTION_VALUE,
}
def __init__(
self,
all_template_folders,
template_list,
available_template_types,
allow_adding_copy_of_template,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self.available_template_types = available_template_types
self.templates_and_folders.choices = template_list.as_id_and_name
self.op = None
self.is_move_op = self.is_add_folder_op = self.is_add_template_op = False
self.move_to.all_template_folders = all_template_folders
self.move_to.choices = [
(item["id"], item["name"])
for item in ([self.ALL_TEMPLATES_FOLDER] + all_template_folders)
]
self.add_template_by_template_type.choices = list(
filter(
None,
[
# ('email', 'Email') if 'email' in available_template_types else None,
2023-10-03 11:14:21 -04:00
("sms", "Start with a blank template")
if "sms" in available_template_types
else None,
("copy-existing", "Copy an existing template")
if allow_adding_copy_of_template
else None,
],
)
)
@property
def trying_to_add_unavailable_template_type(self):
return all(
(
self.is_add_template_op,
self.add_template_by_template_type.data,
self.add_template_by_template_type.data
not in self.available_template_types,
)
)
def is_selected(self, template_folder_id):
return template_folder_id in (self.templates_and_folders.data or [])
2023-04-26 11:24:01 -04:00
def validate(self, extra_validators=None):
self.op = request.form.get("operation")
self.is_move_op = self.op in {"move-to-existing-folder", "move-to-new-folder"}
self.is_add_folder_op = self.op in {"add-new-folder", "move-to-new-folder"}
self.is_add_template_op = self.op in {"add-new-template"}
if not (self.is_add_folder_op or self.is_move_op or self.is_add_template_op):
return False
2023-04-26 11:24:01 -04:00
return super().validate(extra_validators)
def get_folder_name(self):
if self.op == "add-new-folder":
return self.add_new_folder_name.data
elif self.op == "move-to-new-folder":
return self.move_to_new_folder_name.data
return None
templates_and_folders = GovukCheckboxesField(
"Choose templates or folders",
validators=[required_for_ops("move-to-new-folder", "move-to-existing-folder")],
choices=[], # added to keep order of arguments, added properly in __init__
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
)
# if no default set, it is set to None, which process_data transforms to '__NONE__'
# this means '__NONE__' (self.ALL_TEMPLATES option) is selected when no form data has been submitted
# set default to empty string so process_data method doesn't perform any transformation
move_to = NestedRadioField(
"Choose a folder",
default="",
validators=[required_for_ops("move-to-existing-folder"), Optional()],
)
add_new_folder_name = GovukTextInputField(
"Folder name", validators=[required_for_ops("add-new-folder")]
)
move_to_new_folder_name = GovukTextInputField(
"Folder name", validators=[required_for_ops("move-to-new-folder")]
)
add_template_by_template_type = RadioFieldWithRequiredMessage(
"New template",
validators=[
required_for_ops("add-new-template"),
Optional(),
],
required_message="Select the type of template you want to add",
)
class AdminClearCacheForm(StripWhitespaceForm):
model_type = GovukCheckboxesField(
"What do you want to clear today",
)
def validate_model_type(self, field):
if not field.data:
raise ValidationError("Select at least one option")
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
security_key_name = GovukTextInputField(
"Name of key",
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(max=255, message="Name of key must be 255 characters or fewer"),
],
)