From 2d55bb7ae28bd5468bcacddf6f068483d831fd58 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Mon, 8 Feb 2016 16:36:53 +0000 Subject: [PATCH 1/5] Use `Template` to replace/highlight placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit brings in the `Template` util, added here: https://github.com/alphagov/notifications-utils/pull/1 It also does a fair bit of tidying up, which I’ve unfortunately squashed into this one massive commit. The main change is moving 404 handling into the templates dao, so that every view isn’t littered with `try: … except(HTTPError)`. It also adds new features, in a prototypy sort of way, which are: - download a prefilled example CSV - show all the columns for your template on the 'check' page --- app/__init__.py | 22 ----- app/main/dao/templates_dao.py | 23 +++++- app/main/views/jobs.py | 6 +- app/main/views/sms.py | 89 +++++++++++++-------- app/main/views/templates.py | 33 +++----- app/templates/components/email-message.html | 4 +- app/templates/components/sms-message.html | 2 +- app/templates/views/check-sms.html | 8 +- app/templates/views/job.html | 2 +- app/templates/views/manage-templates.html | 18 ++--- app/templates/views/send-sms.html | 16 ++++ requirements.txt | 2 +- tests/app/main/views/test_sms.py | 3 +- 13 files changed, 127 insertions(+), 101 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index e85140659..c0c431c0b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -62,8 +62,6 @@ def create_app(config_name, config_overrides=None): application.session_interface = ItsdangerousSessionInterface() - application.add_template_filter(placeholders) - application.add_template_filter(replace_placeholders) application.add_template_filter(nl2br) application.add_template_filter(format_datetime) application.add_template_filter(syntax_highlight_json) @@ -123,16 +121,6 @@ def convert_to_boolean(value): return value -def placeholders(value): - if not value: - return value - return Markup(re.sub( - r"\(\(([^\)]+)\)\)", # anything that looks like ((registration number)) - lambda match: "{}".format(match.group(1)), - value - )) - - def nl2br(value): _paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}') @@ -141,16 +129,6 @@ def nl2br(value): return Markup(result) -def replace_placeholders(template, values): - if not template: - return template - return Markup(re.sub( - r"\(\(([^\)]+)\)\)", # anything that looks like ((registration number)) - lambda match: values.get(match.group(1), ''), - template - )) - - def syntax_highlight_json(code): return Markup(highlight(code, JavascriptLexer(), HtmlFormatter(noclasses=True))) diff --git a/app/main/dao/templates_dao.py b/app/main/dao/templates_dao.py index 72d47bcd2..4eb05ed9d 100644 --- a/app/main/dao/templates_dao.py +++ b/app/main/dao/templates_dao.py @@ -1,4 +1,4 @@ -from flask import url_for +from flask import url_for, abort from app import notifications_api_client from app.main.utils import BrowsableItem @@ -17,9 +17,24 @@ def get_service_templates(service_id): return notifications_api_client.get_service_templates(service_id) -def get_service_template(service_id, template_id): - return notifications_api_client.get_service_template( - service_id, template_id) +def get_service_templates_or_404(service_id): + try: + get_service_templates(service_id) + except HTTPError as e: + if e.status_code == 404: + abort(404) + else: + raise e + + +def get_service_template_or_404(service_id, template_id): + try: + return notifications_api_client.get_service_template(service_id, template_id) + except HTTPError as e: + if e.status_code == 404: + abort(404) + else: + raise e def delete_service_template(service_id, template_id): diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index d19e29cd3..dd5e45e04 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -8,6 +8,7 @@ from flask import ( ) from flask_login import login_required from notifications_python_client.errors import HTTPError +from utils.template import Template from app import job_api_client from app.main import main @@ -38,7 +39,6 @@ def view_jobs(service_id): def view_job(service_id, job_id): try: job = job_api_client.get_job(service_id, job_id)['data'] - template = templates_dao.get_service_template(service_id, job['template'])['data'] messages = [] return render_template( 'views/job.html', @@ -55,7 +55,9 @@ def view_job(service_id, job_id): cost=u'£0.00', uploaded_file_name=job['original_file_name'], uploaded_file_time=job['created_at'], - template=template, + template=Template( + templates_dao.get_service_template_or_404(service_id, job['template'])['data'] + ), service_id=service_id ) except HTTPError as e: diff --git a/app/main/views/sms.py b/app/main/views/sms.py index c7c7f75c1..94a5b19f6 100644 --- a/app/main/views/sms.py +++ b/app/main/views/sms.py @@ -1,6 +1,8 @@ import csv import uuid import botocore +import re +import io from datetime import date @@ -12,12 +14,14 @@ from flask import ( flash, abort, session, - current_app + current_app, + Markup ) from flask_login import login_required from werkzeug import secure_filename from notifications_python_client.errors import HTTPError +from utils.template import Template from app.main import main from app.main.forms import CsvUploadForm @@ -35,22 +39,19 @@ from app.main.utils import ( @main.route("/services//sms/send", methods=['GET']) def choose_sms_template(service_id): - try: - templates = templates_dao.get_service_templates(service_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e - - return render_template('views/choose-sms-template.html', - templates=templates, - service_id=service_id) + return render_template( + 'views/choose-sms-template.html', + templates=[ + Template(template) for template in templates_dao.get_service_templates(service_id)['data'] + ], + service_id=service_id + ) @main.route("/services//sms/send/", methods=['GET', 'POST']) @login_required def send_sms(service_id, template_id): + form = CsvUploadForm() if form.validate_on_submit(): try: @@ -63,24 +64,33 @@ def send_sms(service_id, template_id): service_id=service_id, upload_id=upload_id)) except ValueError as e: - message = 'There was a problem uploading: {}'.format( - csv_file.filename) - flash(message) + flash('There was a problem uploading: {}'.format(csv_file.filename)) flash(str(e)) return redirect(url_for('.send_sms', service_id=service_id, template_id=template_id)) - try: - template = templates_dao.get_service_template(service_id, template_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e + template = Template( + templates_dao.get_service_template_or_404(service_id, template_id)['data'] + ) - return render_template('views/send-sms.html', - template=template, - form=form, - service_id=service_id) + return render_template( + 'views/send-sms.html', + template=template, + column_headers=['phone number'] + template.placeholders_as_markup, + form=form, + service_id=service_id + ) + + +@main.route("/services//sms/send/.csv", methods=['GET']) +@login_required +def get_example_csv(service_id, template_id): + template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] + output = io.StringIO() + csv.writer(output).writerow( + ['phone number'] + Template(template).list_placeholders + ) + + return(output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}) @main.route("/services//sms/check/", @@ -89,21 +99,23 @@ def send_sms(service_id, template_id): def check_sms(service_id, upload_id): if request.method == 'GET': - contents = s3download(service_id, upload_id) if not contents: flash('There was a problem reading your upload file') upload_result = _get_numbers(contents) upload_data = session['upload_data'] - original_file_name = upload_data.get('original_file_name') template_id = upload_data.get('template_id') - template = templates_dao.get_service_template(service_id, template_id)['data'] + template = Template( + templates_dao.get_service_template_or_404(service_id, template_id)['data'], + values=upload_result['valid'][0] if upload_result['valid'] else {}, + drop_values={'phone'} + ) return render_template( 'views/check-sms.html', upload_result=upload_result, - message_template=template['content'], - original_file_name=original_file_name, - template_id=template_id, + template=template, + column_headers=['phone number'] + template.placeholders_as_markup, + original_file_name=upload_data.get('original_file_name'), service_id=service_id ) elif request.method == 'POST': @@ -153,3 +165,16 @@ def _get_numbers(contents): except InvalidPhoneError: rejects.append({"line_number": i+2, "phone": row['phone']}) return {"valid": valid, "rejects": rejects} + + +def _get_column_headers(template_content, marked_up=False): + headers = re.findall( + r"\(\(([^\)]+)\)\)", # anything that looks like ((registration number)) + template_content + ) + if marked_up: + return [ + Markup("{}".format(header)) + for header in headers + ] + return headers diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 33d93e93a..0b3f492ff 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -1,13 +1,15 @@ from flask import request, render_template, redirect, url_for, flash, abort from flask_login import login_required +from notifications_python_client.errors import HTTPError +from utils.template import Template + from app.main import main from app.main.forms import TemplateForm from app import job_api_client from app.main.dao.services_dao import get_service_by_id from app.main.dao import templates_dao as tdao from app.main.dao import services_dao as sdao -from notifications_python_client.errors import HTTPError @main.route("/services//templates") @@ -15,7 +17,6 @@ from notifications_python_client.errors import HTTPError def manage_service_templates(service_id): try: jobs = job_api_client.get_job(service_id)['data'] - templates = tdao.get_service_templates(service_id)['data'] except HTTPError as e: if e.status_code == 404: abort(404) @@ -25,7 +26,11 @@ def manage_service_templates(service_id): 'views/manage-templates.html', service_id=service_id, has_jobs=bool(jobs), - templates=[tdao.TemplatesBrowsableItem(x) for x in templates]) + templates=[ + Template(template) + for template in tdao.get_service_templates(service_id)['data'] + ] + ) @main.route("/services//templates/add", methods=['GET', 'POST']) @@ -56,14 +61,7 @@ def add_service_template(service_id): @main.route("/services//templates/", methods=['GET', 'POST']) @login_required def edit_service_template(service_id, template_id): - try: - template = tdao.get_service_template(service_id, template_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e - + template = tdao.get_service_template_or_404(service_id, template_id)['data'] template['template_content'] = template['content'] form = TemplateForm(**template) @@ -84,21 +82,14 @@ def edit_service_template(service_id, template_id): @main.route("/services//templates//delete", methods=['GET', 'POST']) @login_required def delete_service_template(service_id, template_id): - try: - template = tdao.get_service_template(service_id, template_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e - - template['template_content'] = template['content'] - form = TemplateForm(**template) + template = tdao.get_service_template_or_404(service_id, template_id)['data'] if request.method == 'POST': tdao.delete_service_template(service_id, template_id) return redirect(url_for('.manage_service_templates', service_id=service_id)) + template['template_content'] = template['content'] + form = TemplateForm(**template) flash('Are you sure you want to delete ‘{}’?'.format(form.name.data), 'delete') return render_template( 'views/edit-template.html', diff --git a/app/templates/components/email-message.html b/app/templates/components/email-message.html index 2a8f65eb8..6d9920121 100644 --- a/app/templates/components/email-message.html +++ b/app/templates/components/email-message.html @@ -10,10 +10,10 @@ {% endif %} {% endmacro %} diff --git a/app/templates/components/sms-message.html b/app/templates/components/sms-message.html index 044f3f5c7..490254485 100644 --- a/app/templates/components/sms-message.html +++ b/app/templates/components/sms-message.html @@ -11,7 +11,7 @@ {% endif %}
- {{ body|placeholders }} + {{ body }}
{% if recipient %}

diff --git a/app/templates/views/check-sms.html b/app/templates/views/check-sms.html index 2a61a83f2..d4b761075 100644 --- a/app/templates/views/check-sms.html +++ b/app/templates/views/check-sms.html @@ -17,14 +17,14 @@ {% for rejected in upload_result.rejects %}

Line {{rejected.line_number}}: {{rejected.phone }} {% endfor %} -

Go back and resolve errors

+

Go back and resolve errors

{% else %}
{{ sms_message( - message_template|replace_placeholders(upload_result.valid[0]) + template.replaced )}}
@@ -32,13 +32,13 @@
- Back + Back
{% call(item) list_table( upload_result.valid, caption=original_file_name, - field_headings=['Phone number'] + field_headings=column_headers ) %} {% call field() %} {{ item.phone }} diff --git a/app/templates/views/job.html b/app/templates/views/job.html index c72e6d34f..40af13526 100644 --- a/app/templates/views/job.html +++ b/app/templates/views/job.html @@ -17,7 +17,7 @@
{{ sms_message( - template['content'], + template.formatted_as_markup, )}}
diff --git a/app/templates/views/manage-templates.html b/app/templates/views/manage-templates.html index 2d3da827e..346c87739 100644 --- a/app/templates/views/manage-templates.html +++ b/app/templates/views/manage-templates.html @@ -26,19 +26,19 @@ Manage templates – GOV.UK Notify
{% for template in templates %} - {% if template.get_field('template_type') == 'sms' %} - {{ sms_message( - template.get_field('content'), - name=template.title, - id=template.get_field('id'), - edit_link=url_for('.edit_service_template', service_id=template.get_field('service'), template_id=template.get_field('id')) - ) }} - {% elif template.get_field('template_type') == 'email' %} + {% if template.template_type == 'email' %} {{ email_message( template.get_field('subject'), template.get_field('content'), name=template.get_field('name'), - edit_link=url_for('.edit_service_template', service_id=template.get_field('service'), template_id=template.get_field('id')) + edit_link=url_for('.edit_service_template', service_id=service_id, template_id=template.id) + ) }} + {% else %} + {{ sms_message( + template.formatted_as_markup, + name=template.name, + id=template.id, + edit_link=url_for('.edit_service_template', service_id=service_id, template_id=template.id) ) }} {% endif %} {% endfor %} diff --git a/app/templates/views/send-sms.html b/app/templates/views/send-sms.html index 66f7491ee..c43c52265 100644 --- a/app/templates/views/send-sms.html +++ b/app/templates/views/send-sms.html @@ -2,6 +2,7 @@ {% from "components/sms-message.html" import sms_message %} {% from "components/page-footer.html" import page_footer %} {% from "components/file-upload.html" import file_upload %} +{% from "components/table.html" import list_table %} {% block page_title %} Send text messages – GOV.UK Notify @@ -32,5 +33,20 @@ "Continue to preview" ) }} + {% if column_headers %} + {% call(item) list_table( + [], + caption='Preview', + field_headings=column_headers, + field_headings_visible=True, + caption_visible=False, + empty_message="Your data here" + ) %} + {% endcall %} + {% endif %} + + {% endblock %} diff --git a/requirements.txt b/requirements.txt index 2c5fed09d..631ea6965 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,4 @@ Pygments==2.0.2 git+https://github.com/alphagov/notifications-python-client.git@0.2.5#egg=notifications-python-client==0.2.5 -git+https://github.com/alphagov/notifications-utils.git@0.0.3#egg=notifications-utils==0.0.3 +git+https://github.com/alphagov/notifications-utils.git@0.1.0#egg=notifications-utils==0.1.0 diff --git a/tests/app/main/views/test_sms.py b/tests/app/main/views/test_sms.py index 9f4c4c158..57af2f99e 100644 --- a/tests/app/main/views/test_sms.py +++ b/tests/app/main/views/test_sms.py @@ -8,9 +8,8 @@ def test_choose_sms_template(app_, api_user_active, mock_login, mock_get_user, - mock_get_service_templates, mock_check_verify_code, - mock_get_service_template): + mock_get_service_templates): with app_.test_request_context(): with app_.test_client() as client: client.login(api_user_active) From efb2140bbbd862b6ace38b3220a5eafd94e478e8 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Wed, 17 Feb 2016 14:20:55 +0000 Subject: [PATCH 2/5] Check CSV files match the template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a first stab at checking whether a CSV file has the right data to fill the placeholders. The UI is very much first bash, but I’d like to get this merged and see how it feels. The main thing is that we’ve got all the bit in place now to do this logic. --- app/assets/stylesheets/components/table.scss | 7 +++ app/main/views/sms.py | 26 +++++++--- app/templates/views/check-sms.html | 51 +++++++++++++++++--- app/templates/views/choose-sms-template.html | 2 +- app/templates/views/send-sms.html | 14 ++++-- 5 files changed, 80 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index 650509f24..1b40c5fa8 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -36,6 +36,13 @@ } + &-missing { + color: $error-colour; + font-weight: bold; + border-left: 5px solid $error-colour; + padding-left: 7px; + } + } } diff --git a/app/main/views/sms.py b/app/main/views/sms.py index 94a5b19f6..d191063ff 100644 --- a/app/main/views/sms.py +++ b/app/main/views/sms.py @@ -14,11 +14,10 @@ from flask import ( flash, abort, session, - current_app, - Markup + current_app ) -from flask_login import login_required +from flask_login import login_required, current_user from werkzeug import secure_filename from notifications_python_client.errors import HTTPError from utils.template import Template @@ -72,10 +71,19 @@ def send_sms(service_id, template_id): templates_dao.get_service_template_or_404(service_id, template_id)['data'] ) + example_data = [dict( + phone=current_user.mobile_number, + **{ + header: "test {}".format(header) for header in template.placeholders + } + )] + return render_template( 'views/send-sms.html', template=template, - column_headers=['phone number'] + template.placeholders_as_markup, + column_headers=['phone'] + template.placeholders_as_markup, + placeholders=template.placeholders, + example_data=example_data, form=form, service_id=service_id ) @@ -85,10 +93,11 @@ def send_sms(service_id, template_id): @login_required def get_example_csv(service_id, template_id): template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] + placeholders = list(Template(template).placeholders) output = io.StringIO() - csv.writer(output).writerow( - ['phone number'] + Template(template).list_placeholders - ) + writer = csv.writer(output) + writer.writerow(['phone'] + placeholders) + writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders]) return(output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}) @@ -116,7 +125,8 @@ def check_sms(service_id, upload_id): template=template, column_headers=['phone number'] + template.placeholders_as_markup, original_file_name=upload_data.get('original_file_name'), - service_id=service_id + service_id=service_id, + form=CsvUploadForm() ) elif request.method == 'POST': upload_data = session['upload_data'] diff --git a/app/templates/views/check-sms.html b/app/templates/views/check-sms.html index d4b761075..87656b2af 100644 --- a/app/templates/views/check-sms.html +++ b/app/templates/views/check-sms.html @@ -2,6 +2,7 @@ {% from "components/sms-message.html" import sms_message %} {% from "components/table.html" import list_table, field %} {% from "components/placeholder.html" import placeholder %} +{% from "components/file-upload.html" import file_upload %} {% from "components/page-footer.html" import page_footer %} {% block page_title %} @@ -23,17 +24,40 @@
- {{ sms_message( - template.replaced - )}} + {% if template.missing_data or template.additional_data %} + {{ sms_message(template.formatted_as_markup)}} + {% else %} + {{ sms_message(template.replaced)}} + {% endif %}
-
- - - Back -
+ {% if template.missing_data %} + {{ banner( + "Add these columns to your CSV file: " + ", ".join(template.missing_data), + type="dangerous" + ) }} + {% elif template.additional_data %} + {{ banner( + "Remove these columns from your CSV file:" + ", ".join(template.missing_data), + type="dangerous" + ) }} + {% else %} +
+ + + Back +
+ {% endif %} + + {% if template.missing_data or template.additional_data %} +
+ {{file_upload(form.file, button_text='Choose a CSV file')}} + {{ page_footer( + "Upload" + ) }} +
+ {% endif %} {% call(item) list_table( upload_result.valid, @@ -43,6 +67,17 @@ {% call field() %} {{ item.phone }} {% endcall %} + {% for column in template.placeholders %} + {% if item.get(column) %} + {% call field() %} + {{ item.get(column) }} + {% endcall %} + {% else %} + {% call field(status='missing') %} + missing + {% endcall %} + {% endif %} + {% endfor %} {% endcall %} {% endif %} diff --git a/app/templates/views/choose-sms-template.html b/app/templates/views/choose-sms-template.html index e12ed204f..383d34834 100644 --- a/app/templates/views/choose-sms-template.html +++ b/app/templates/views/choose-sms-template.html @@ -17,7 +17,7 @@
{% for template in templates %}
- {{ sms_message(template.content, name=template.name) }} + {{ sms_message(template.formatted_as_markup, name=template.name) }}