diff --git a/app/__init__.py b/app/__init__.py index 6697fb7c4..db4a51d31 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -131,6 +131,7 @@ def create_app(): application.add_template_filter(format_notification_status_as_time) application.add_template_filter(format_notification_status_as_field_status) application.add_template_filter(format_notification_status_as_url) + application.add_template_filter(formatted_list) application.after_request(useful_headers_after_request) application.after_request(save_service_after_request) @@ -345,6 +346,33 @@ def format_notification_status_as_url(status): }.get(status) +def formatted_list( + items, + conjunction='and', + before_each='‘', + after_each='’', + separator=', ', + prefix='', + prefix_plural='' +): + if prefix: + prefix += ' ' + if prefix_plural: + prefix_plural += ' ' + + items = list(items) + if len(items) == 1: + return '{prefix}{before_each}{items[0]}{after_each}'.format(**locals()) + elif items: + formatted_items = ['{}{}{}'.format(before_each, item, after_each) for item in items] + + first_items = separator.join(formatted_items[:-1]) + last_item = formatted_items[-1] + return ( + '{prefix_plural}{first_items} {conjunction} {last_item}' + ).format(**locals()) + + @login_manager.user_loader def load_user(user_id): return user_api_client.get_user(user_id) diff --git a/app/main/forms.py b/app/main/forms.py index 5b736a57b..e3a8a97a4 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1,3 +1,5 @@ +import re + import pytz from flask_login import current_user from flask_wtf import Form @@ -23,7 +25,7 @@ from wtforms import ( from wtforms.fields.html5 import EmailField, TelField from wtforms.validators import (DataRequired, Email, Length, Regexp, Optional) -from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoCommasInPlaceHolders) +from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoCommasInPlaceHolders, OnlyGSMCharacters) def get_time_value_and_label(future_time): @@ -251,7 +253,7 @@ class ConfirmPasswordForm(Form): raise ValidationError('Invalid password') -class SMSTemplateForm(Form): +class BaseTemplateForm(Form): name = StringField( u'Template name', validators=[DataRequired(message="Can’t be empty")]) @@ -274,7 +276,12 @@ class SMSTemplateForm(Form): ) -class EmailTemplateForm(SMSTemplateForm): +class SMSTemplateForm(BaseTemplateForm): + def validate_template_content(self, field): + OnlyGSMCharacters()(None, field) + + +class EmailTemplateForm(BaseTemplateForm): subject = TextAreaField( u'Subject', validators=[DataRequired(message="Can’t be empty")]) @@ -479,7 +486,6 @@ class ServiceSmsSender(Form): ) def validate_sms_sender(form, field): - import re if field.data and not re.match('^[a-zA-Z0-9\s]+$', field.data): raise ValidationError('Use letters and numbers only') diff --git a/app/main/validators.py b/app/main/validators.py index d07d7cab7..42cd29c8d 100644 --- a/app/main/validators.py +++ b/app/main/validators.py @@ -1,13 +1,16 @@ from wtforms import ValidationError from notifications_utils.template import Template +from notifications_utils.gsm import get_non_gsm_compatible_characters + +from app import formatted_list +from app.main._blacklisted_passwords import blacklisted_passwords from app.utils import ( Spreadsheet, is_gov_user ) -from ._blacklisted_passwords import blacklisted_passwords -class Blacklist(object): +class Blacklist: def __init__(self, message=None): if not message: message = 'Password is blacklisted.' @@ -18,7 +21,7 @@ class Blacklist(object): raise ValidationError(self.message) -class CsvFileValidator(object): +class CsvFileValidator: def __init__(self, message='Not a csv file'): self.message = message @@ -28,7 +31,7 @@ class CsvFileValidator(object): raise ValidationError("{} isn’t a spreadsheet that Notify can read".format(field.data.filename)) -class ValidGovEmail(object): +class ValidGovEmail: def __call__(self, form, field): from flask import url_for @@ -40,7 +43,7 @@ class ValidGovEmail(object): raise ValidationError(message) -class NoCommasInPlaceHolders(): +class NoCommasInPlaceHolders: def __init__(self, message='You can’t have commas in your fields'): self.message = message @@ -48,3 +51,15 @@ class NoCommasInPlaceHolders(): def __call__(self, form, field): if ',' in ''.join(Template({'content': field.data}).placeholders): raise ValidationError(self.message) + + +class OnlyGSMCharacters: + def __call__(self, form, field): + non_gsm_characters = sorted(list(get_non_gsm_compatible_characters(field.data))) + if non_gsm_characters: + raise ValidationError( + 'You can’t use {} in text messages. {} won’t show up properly on everyone’s phones.'.format( + formatted_list(non_gsm_characters, conjunction='or', before_each='', after_each=''), + ('It' if len(non_gsm_characters) == 1 else 'They') + ) + ) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index a578168a5..cd6699d3b 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -163,11 +163,12 @@ def add_service_template(service_id, template_type): form.process_type.data ) except HTTPError as e: - if e.status_code == 400: - if 'content' in e.message and any(['character count greater than' in x for x in e.message['content']]): - form.template_content.errors.extend(e.message['content']) - else: - raise e + if ( + e.status_code == 400 and + 'content' in e.message and + any(['character count greater than' in x for x in e.message['content']]) + ): + form.template_content.errors.extend(e.message['content']) else: raise e else: diff --git a/app/templates/components/list.html b/app/templates/components/list.html index 26eb8e6ee..ec74264ca 100644 --- a/app/templates/components/list.html +++ b/app/templates/components/list.html @@ -1,35 +1,5 @@ -{% macro formatted_list( - items, - conjunction='and', - before_each='‘', - after_each='’', - separator=', ', - prefix='', - prefix_plural='' -) %} - {% if items|length == 1 %} - {{ prefix }} {{ before_each|safe }}{{ (items|list)[0] }}{{ after_each|safe }} - {% elif items %} - {{ prefix_plural }} - {% for item in (items|list)[0:-1] -%} - {{ before_each|safe -}} - {{ item -}} - {{ after_each|safe -}} - {% if not loop.last -%} - {{ separator -}} - {% endif -%} - {% endfor %} - {{ conjunction }} - {{ before_each|safe -}} - {{ (items|list)[-1] -}} - {{ after_each|safe }} - {%- endif %} -{%- endmacro %} - - {% macro list_of_placeholders(placeholders) %} - {{ formatted_list( - placeholders, + {{ placeholders | formatted_list( before_each="((", after_each='))', separator=' ' diff --git a/app/templates/views/check.html b/app/templates/views/check.html index e0f891f7d..b8880fe30 100644 --- a/app/templates/views/check.html +++ b/app/templates/views/check.html @@ -4,7 +4,6 @@ {% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %} {% from "components/file-upload.html" import file_upload %} {% from "components/page-footer.html" import page_footer %} -{% from "components/list.html" import formatted_list %} {% from "components/message-count-label.html" import message_count_label %} {% set file_contents_header_id = 'file-preview' %} @@ -41,17 +40,15 @@
- Your file has {{ formatted_list( - recipients.column_headers, - prefix='one column, called', - prefix_plural='columns called' + Your file has {{ recipients.column_headers | formatted_list( + prefix='one column, called ', + prefix_plural='columns called ' ) }}.
{{ skip_to_file_contents() }} @@ -67,18 +64,16 @@ your template- Your file has {{ formatted_list( - recipients.column_headers, - prefix='one column, called', - prefix_plural='columns called' + Your file has {{ recipients.column_headers | formatted_list( + prefix='one column, called ', + prefix_plural='columns called ' ) }}.
- It doesn’t have {{ formatted_list( - recipients.missing_column_headers, + It doesn’t have {{ recipients.column_headers | formatted_list( conjunction='or', - prefix='a column called', - prefix_plural='columns called' + prefix='a column called ', + prefix_plural='columns called ' ) }}.
{{ skip_to_file_contents() }} diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html index bcb4a34bc..10e3070fa 100644 --- a/app/templates/views/styleguide.html +++ b/app/templates/views/styleguide.html @@ -8,7 +8,6 @@ {% from "components/textbox.html" import textbox %} {% from "components/file-upload.html" import file_upload %} {% from "components/api-key.html" import api_key %} -{% from "components/list.html" import formatted_list %} {% block per_page_title %} Styleguide @@ -196,19 +195,19 @@- {{ formatted_list('A', prefix="one item called") }} + {{ 'A' | formatted_list(prefix="one item called") }}
- {{ formatted_list('AB', prefix_plural="two items called") }} + {{ 'AB' | formatted_list(prefix_plural="two items called") }}
- {{ formatted_list('ABC') }} + {{ 'ABC' | formatted_list }}
- {{ formatted_list('ABCD', before_each='', after_each='', conjunction='or') }}
+ {{ 'ABCD' | formatted_list(before_each='', after_each='', conjunction='or') }}