diff --git a/.travis.yml b/.travis.yml index 0ed81c30b..9ec5271d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false language: python python: -- '3.4' +- '3.5' env: secure: jT9BIioqBMkOdLZhU+WJNdnRJ+06G7qUx4QqEVldp96dJwmWpPEvA0XbitdnQt/WXYkpMlDbgSApvvGj2ZNvdpowRRe5HFX8D2Udhi2g9+cXgKrQxH6zv0evJyQLOjCINW6KtgMCJ5wkYR3qQ4BQawlDt6ecpmeboKTmvs2W8jZ09aV4IKKvdd7BwFon10QVPF5ny10G83unLtKnKgRMjSSLnaEiA78pE/LSUkekK4mhmtl+yfQf60cIuQGcN9NCYIt5PrdYYyMkbUaht9ykwL2C11sp5JYPClI9k6lrlpGJCdL9wbJwejGhR/pEqwJ4tKK8Zv+mngmkbzE6fd5ehuRMnIUAifG4t3p6WbhKwY5pJsdVyPgWcRSPXOJA7yEcAeTAvWcC++6mCIFBeMxt/yQNw02jkFHeNKRh2twTRvr4xWZHq9FsVxTEVz89OOuue3IkkyDNmVusGJ9+AVRIn9Oa+U/r3bDnrs7jz+meSwb82GZUBzFpUe2pe8qeBE572Ay7yHB73VHUgp/2A1qkZ4SnTjTpMbnS5RdXTgwtMkOs5MLZgteCVxFL3sHcr9e/B3UIUnzKUSPXXOjHyDxBwrABWo81V9Vp2IPV7P9Ofv8zroudjQxK5MOcbmiPQF+eEB9L4DvkUBNsGxtJ/nmPp6tmN0Xjo0xXVdZCEVj29Og= before_install: diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index c62bfe2c6..77b706779 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -71,7 +71,7 @@ color: $text-colour; background-image: file-url('icon-important-2x.png'); background-size: 34px 34px; - background-position: 0 0px; + background-position: 0 0; background-repeat: no-repeat; padding: 7px 0 5px 50px; } diff --git a/app/assets/stylesheets/components/email-message.scss b/app/assets/stylesheets/components/email-message.scss index b4771841a..860b3d99e 100644 --- a/app/assets/stylesheets/components/email-message.scss +++ b/app/assets/stylesheets/components/email-message.scss @@ -7,9 +7,26 @@ margin: 20px 0 10px 0; } - &-subject, - &-from { - margin: 10px 0; + &-meta { + + @include core-19; + margin: 0; + + td, + th { + @include core-19; + border-bottom: 0; + border-top: 1px solid $border-colour; + } + + th { + color: $secondary-text-colour; + } + + td { + width: 99%; + } + } &-from { diff --git a/app/assets/stylesheets/components/sms-message.scss b/app/assets/stylesheets/components/sms-message.scss index 898d04c24..cb26b296f 100644 --- a/app/assets/stylesheets/components/sms-message.scss +++ b/app/assets/stylesheets/components/sms-message.scss @@ -40,7 +40,7 @@ .sms-message-use-links { @include copy-19; - margin-top: 55px; + margin-top: 52px; a { diff --git a/app/main/__init__.py b/app/main/__init__.py index fbfd2562a..ec9691d24 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -3,7 +3,7 @@ from flask import Blueprint main = Blueprint('main', __name__) from app.main.views import ( - index, sign_in, sign_out, register, two_factor, verify, sms, add_service, + index, sign_in, sign_out, register, two_factor, verify, send, add_service, code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password, styleguide, user_profile, choose_service, api_keys, manage_users ) diff --git a/app/main/dao/templates_dao.py b/app/main/dao/templates_dao.py index d34f2b187..7d07b7d0e 100644 --- a/app/main/dao/templates_dao.py +++ b/app/main/dao/templates_dao.py @@ -4,14 +4,14 @@ from app.utils import BrowsableItem from notifications_python_client.errors import HTTPError -def insert_service_template(name, content, service_id): +def insert_service_template(name, type_, content, service_id, subject=None): return notifications_api_client.create_service_template( - name, 'sms', content, service_id) + name, type_, content, service_id, subject) -def update_service_template(id_, name, content, service_id): +def update_service_template(id_, name, type_, content, service_id, subject=None): return notifications_api_client.update_service_template( - id_, name, 'sms', content, service_id) + id_, name, type_, content, service_id) def get_service_templates(service_id): diff --git a/app/main/forms.py b/app/main/forms.py index 20b007514..ea2e1cd69 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -188,7 +188,7 @@ class ConfirmPasswordForm(Form): raise ValidationError('Invalid password') -class TemplateForm(Form): +class SMSTemplateForm(Form): name = StringField( u'Template name', validators=[DataRequired(message="Template name cannot be empty")]) @@ -198,6 +198,13 @@ class TemplateForm(Form): validators=[DataRequired(message="Template content cannot be empty")]) +class EmailTemplateForm(SMSTemplateForm): + + subject = StringField( + u'Subject', + validators=[DataRequired(message="Subject cannot be empty")]) + + class ForgotPasswordForm(Form): email_address = email_address() diff --git a/app/main/views/email.py b/app/main/views/email.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/main/views/index.py b/app/main/views/index.py index 1200cf65b..8335284cd 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -22,15 +22,3 @@ def register_from_invite(): @login_required def verify_mobile(): return render_template('views/verify-mobile.html') - - -@main.route("/services//send-email") -@login_required -def send_email(service_id): - return render_template('views/send-email.html', service_id=service_id) - - -@main.route("/services//check-email") -@login_required -def check_email(service_id): - return render_template('views/check-email.html') diff --git a/app/main/views/sms.py b/app/main/views/send.py similarity index 69% rename from app/main/views/sms.py rename to app/main/views/send.py index 42340cbaa..91a41fc5f 100644 --- a/app/main/views/sms.py +++ b/app/main/views/send.py @@ -28,15 +28,23 @@ from app.main.uploader import ( s3download ) from app.main.dao import templates_dao +from app.main.dao import services_dao from app import job_api_client -from app.utils import ( - validate_phone_number, - InvalidPhoneError -) +from app.utils import validate_recipient, InvalidPhoneError, InvalidEmailError + +first_column_header = { + 'email': 'email', + 'sms': 'phone' +} -@main.route("/services//sms/send", methods=['GET']) -def choose_sms_template(service_id): +@main.route("/services//send/", methods=['GET']) +def choose_template(service_id, template_type): + + services_dao.get_service_by_id_or_404(service_id) + + if template_type not in ['email', 'sms']: + abort(404) try: jobs = job_api_client.get_job(service_id)['data'] except HTTPError as e: @@ -44,23 +52,20 @@ def choose_sms_template(service_id): abort(404) else: raise e - print("="*80) - print(jobs) - print(len(jobs)) - print(bool(len(jobs))) return render_template( - 'views/choose-sms-template.html', + 'views/choose-{}-template.html'.format(template_type), templates=[ Template(template) for template in templates_dao.get_service_templates(service_id)['data'] + if template['template_type'] == template_type ], has_jobs=len(jobs), service_id=service_id ) -@main.route("/services//sms/send/", methods=['GET', 'POST']) +@main.route("/services//send/", methods=['GET', 'POST']) @login_required -def send_sms(service_id, template_id): +def send_messages(service_id, template_id): form = CsvUploadForm() if form.validate_on_submit(): @@ -70,48 +75,50 @@ def send_sms(service_id, template_id): upload_id = str(uuid.uuid4()) s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION']) session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']} - return redirect(url_for('.check_sms', + return redirect(url_for('.check_messages', service_id=service_id, upload_id=upload_id)) except ValueError as e: 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)) + return redirect(url_for('.send_messages', service_id=service_id, template_id=template_id)) + service = services_dao.get_service_by_id_or_404(service_id) template = Template( templates_dao.get_service_template_or_404(service_id, template_id)['data'] ) return render_template( - 'views/send-sms.html', + 'views/send.html', template=template, - column_headers=['phone'] + template.placeholders_as_markup, + column_headers=[first_column_header[template.template_type]] + template.placeholders_as_markup, form=form, + service=service, service_id=service_id ) -@main.route("/services//sms/send/.csv", methods=['GET']) +@main.route("/services//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'] placeholders = list(Template(template).placeholders) output = io.StringIO() writer = csv.writer(output) - writer.writerow(['phone'] + placeholders) + writer.writerow([first_column_header[template['template_type']]] + 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'}) -@main.route("/services//sms/send//to-self", methods=['GET']) +@main.route("/services//send//to-self", methods=['GET']) @login_required -def send_sms_to_self(service_id, template_id): +def send_message_to_self(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() writer = csv.writer(output) - writer.writerow(['phone'] + placeholders) + writer.writerow([first_column_header[template['template_type']]] + placeholders) writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders]) filedata = { 'file_name': 'Test run', @@ -121,35 +128,37 @@ def send_sms_to_self(service_id, template_id): s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION']) session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']} - return redirect(url_for('.check_sms', + return redirect(url_for('.check_messages', service_id=service_id, upload_id=upload_id)) -@main.route("/services//sms/check/", +@main.route("/services//check/", methods=['GET', 'POST']) @login_required -def check_sms(service_id, upload_id): +def check_messages(service_id, upload_id): + + upload_data = session['upload_data'] + template_id = upload_data.get('template_id') if request.method == 'GET': contents = s3download(service_id, upload_id) if not contents: flash('There was a problem reading your upload file') - upload_data = session['upload_data'] - template_id = upload_data.get('template_id') raw_template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] + recipient_type = first_column_header[raw_template['template_type']] upload_result = _get_rows(contents, raw_template) session['upload_data']['notification_count'] = len(upload_result['rows']) template = Template( raw_template, values=upload_result['rows'][0] if upload_result['valid'] else {}, - drop_values={'phone'} + drop_values={recipient_type} ) return render_template( 'views/check-sms.html', upload_result=upload_result, template=template, - column_headers=['phone number'] + list( + column_headers=[recipient_type] + list( template.placeholders if upload_result['valid'] else template.placeholders_as_markup ), original_file_name=upload_data.get('original_file_name'), @@ -157,9 +166,7 @@ def check_sms(service_id, upload_id): form=CsvUploadForm() ) elif request.method == 'POST': - upload_data = session['upload_data'] original_file_name = upload_data.get('original_file_name') - template_id = upload_data.get('template_id') notification_count = upload_data.get('notification_count') session.pop('upload_data') try: @@ -171,9 +178,9 @@ def check_sms(service_id, upload_id): raise e flash('We’ve started sending your messages', 'default_with_tick') - return redirect(url_for('main.view_job', - service_id=service_id, - job_id=upload_id)) + return redirect( + url_for('main.view_job', service_id=service_id, job_id=upload_id) + ) def _get_filedata(file): @@ -196,8 +203,12 @@ def _get_rows(contents, raw_template): for row in reader: rows.append(row) try: - validate_phone_number(row['phone']) - Template(raw_template, values=row, drop_values={'phone'}).replaced - except (InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError): + recipient_column = first_column_header[raw_template['template_type']] + validate_recipient( + row[recipient_column], + template_type=raw_template['template_type'] + ) + Template(raw_template, values=row, drop_values={recipient_column}).replaced + except (InvalidEmailError, InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError): valid = False return {"valid": valid, "rows": rows} diff --git a/app/main/views/templates.py b/app/main/views/templates.py index be1001113..09eea1906 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -5,45 +5,44 @@ 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.main.forms import SMSTemplateForm, EmailTemplateForm from app import job_api_client -from app.main.dao.services_dao import get_service_by_id +from app.main.dao.services_dao import get_service_by_id_or_404 from app.main.dao import templates_dao as tdao from app.main.dao import services_dao as sdao -@main.route("/services//templates") +form_objects = { + 'email': EmailTemplateForm, + 'sms': SMSTemplateForm +} + + +@main.route("/services//templates/add-", methods=['GET', 'POST']) @login_required -def manage_service_templates(service_id): - return redirect(url_for( - '.choose_sms_template', - service_id=service_id - )) +def add_service_template(service_id, template_type): + service = sdao.get_service_by_id_or_404(service_id) -@main.route("/services//templates/add", methods=['GET', 'POST']) -@login_required -def add_service_template(service_id): - try: - service = sdao.get_service_by_id(service_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e + if template_type not in ['sms', 'email']: + abort(404) - form = TemplateForm() + form = form_objects[template_type]() if form.validate_on_submit(): tdao.insert_service_template( - form.name.data, form.template_content.data, service_id) - return redirect(url_for( - '.choose_sms_template', service_id=service_id)) + form.name.data, template_type, form.template_content.data, service_id, form.subject.data or None + ) + return redirect( + url_for('.choose_template', service_id=service_id, template_type=template_type) + ) + return render_template( - 'views/edit-template.html', - h1='Add a text message template', + 'views/edit-{}-template.html'.format(template_type), form=form, - service_id=service_id) + template_type=template_type, + service_id=service_id + ) @main.route("/services//templates/", methods=['GET', 'POST']) @@ -51,20 +50,26 @@ def add_service_template(service_id): def edit_service_template(service_id, template_id): template = tdao.get_service_template_or_404(service_id, template_id)['data'] template['template_content'] = template['content'] - form = TemplateForm(**template) + form = form_objects[template['template_type']](**template) if form.validate_on_submit(): tdao.update_service_template( - template_id, form.name.data, - form.template_content.data, service_id) - return redirect(url_for('.choose_sms_template', service_id=service_id)) + template_id, form.name.data, template['template_type'], + form.template_content.data, service_id + ) + return redirect(url_for( + '.choose_template', + service_id=service_id, + template_type=template['template_type'] + )) return render_template( - 'views/edit-template.html', - h1='Edit template', + 'views/edit-{}-template.html'.format(template['template_type']), form=form, service_id=service_id, - template_id=template_id) + template_id=template_id, + template_type=template['template_type'] + ) @main.route("/services//templates//delete", methods=['GET', 'POST']) @@ -74,13 +79,17 @@ def delete_service_template(service_id, template_id): if request.method == 'POST': tdao.delete_service_template(service_id, template_id) - return redirect(url_for('.manage_service_templates', service_id=service_id)) + return redirect(url_for( + '.choose_template', + service_id=service_id, + template_type=template['template_type'] + )) template['template_content'] = template['content'] - form = TemplateForm(**template) + form = form_objects[template['template_type']](**template) flash('Are you sure you want to delete ‘{}’?'.format(form.name.data), 'delete') return render_template( - 'views/edit-template.html', + 'views/edit-{}-template.html'.format(template['template_type']), h1='Edit template', form=form, service_id=service_id, diff --git a/app/notify_client/api_client.py b/app/notify_client/api_client.py index f5f72298a..6d2a2016a 100644 --- a/app/notify_client/api_client.py +++ b/app/notify_client/api_client.py @@ -70,7 +70,7 @@ class NotificationsAdminAPIClient(NotificationsAPIClient): endpoint = "/service/{0}".format(service_id) return self.put(endpoint, data) - def create_service_template(self, name, type_, content, service_id): + def create_service_template(self, name, type_, content, service_id, subject=None): """ Create a service template. """ @@ -80,10 +80,14 @@ class NotificationsAdminAPIClient(NotificationsAPIClient): "content": content, "service": service_id } + if subject: + data.update({ + 'subject': subject + }) endpoint = "/service/{0}/template".format(service_id) return self.post(endpoint, data) - def update_service_template(self, id_, name, type_, content, service_id): + def update_service_template(self, id_, name, type_, content, service_id, subject=None): """ Update a service template. """ @@ -94,8 +98,12 @@ class NotificationsAdminAPIClient(NotificationsAPIClient): 'content': content, 'service': service_id } + if subject: + data.update({ + 'subject': subject + }) endpoint = "/service/{0}/template/{1}".format(service_id, id_) - return self.put(endpoint, data) + return self.post(endpoint, data) def get_service_template(self, service_id, template_id, *params): """ diff --git a/app/templates/components/email-message.html b/app/templates/components/email-message.html index b619b16fa..4b0047651 100644 --- a/app/templates/components/email-message.html +++ b/app/templates/components/email-message.html @@ -9,29 +9,27 @@ {% endif %}