diff --git a/app/__init__.py b/app/__init__.py index 281cc747e..cf4529634 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -155,7 +155,7 @@ def useful_headers_after_request(response): response.headers.add('X-Content-Type-Options', 'nosniff') response.headers.add('X-XSS-Protection', '1; mode=block') response.headers.add('Content-Security-Policy', - "default-src 'self' 'unsafe-inline'; font-src 'self' data:;") # noqa + "default-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;") # noqa if 'Cache-Control' in response.headers: del response.headers['Cache-Control'] response.headers.add( diff --git a/app/assets/images/tick-white.png b/app/assets/images/tick-white.png new file mode 100644 index 000000000..c08e57c4f Binary files /dev/null and b/app/assets/images/tick-white.png differ diff --git a/app/assets/images/tick.png b/app/assets/images/tick.png new file mode 100644 index 000000000..77591a595 Binary files /dev/null and b/app/assets/images/tick.png differ diff --git a/app/assets/images/tick.psd b/app/assets/images/tick.psd new file mode 100644 index 000000000..7f90c3098 Binary files /dev/null and b/app/assets/images/tick.psd differ diff --git a/app/assets/javascripts/fileUpload.js b/app/assets/javascripts/fileUpload.js index 5380ac87f..4df2a1702 100644 --- a/app/assets/javascripts/fileUpload.js +++ b/app/assets/javascripts/fileUpload.js @@ -3,22 +3,20 @@ Modules.FileUpload = function() { - let $field, $button, $filename; + let $field; - this.update = function() { + this.submit = function() { - $filename.text($field.val().split('\\').pop()); + $field.parents('form').trigger('submit'); }; this.start = function(component) { $field = $('.file-upload-field', component); - $button = $('.file-upload-button', component); - $filename = $('.file-upload-filename', component); // Need to put the event on the container, not the input for it to work properly - $(component).on('change', '.file-upload-field', this.update); + $(component).on('change', '.file-upload-field', this.submit); }; diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss index e4a629217..57b5db45a 100644 --- a/app/assets/stylesheets/app.scss +++ b/app/assets/stylesheets/app.scss @@ -66,3 +66,16 @@ a { font-family: monospace; overflow-x: scroll; } + +.inline { + + .block-label { + + @include media(tablet) { + float: none; + display: inline-block; + } + + } + +} diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss index 3a8898c83..7c2fd2b99 100644 --- a/app/assets/stylesheets/components/banner.scss +++ b/app/assets/stylesheets/components/banner.scss @@ -16,19 +16,13 @@ .banner-with-tick, .banner-default-with-tick { - @extend %banner; padding: $gutter-half ($gutter + $gutter-half); - - &:before { - @include core-24; - content: '✔'; - position: absolute; - top: $gutter-half; - left: $gutter-half; - margin-top: -2px; - } - + background-image: file-url('tick-white.png'); + background-size: 19px; + background-repeat: no-repeat; + background-position: $gutter-half $gutter-half; + font-weight: bold; } .banner-dangerous { diff --git a/app/assets/stylesheets/components/file-upload.scss b/app/assets/stylesheets/components/file-upload.scss index f772d4230..d674944a5 100644 --- a/app/assets/stylesheets/components/file-upload.scss +++ b/app/assets/stylesheets/components/file-upload.scss @@ -23,7 +23,7 @@ } &-button { - @include button($panel-colour); + @include button($button-colour); display: inline-block; } diff --git a/app/assets/stylesheets/components/page-footer.scss b/app/assets/stylesheets/components/page-footer.scss index 7dfe8ece0..f38a0e6ea 100644 --- a/app/assets/stylesheets/components/page-footer.scss +++ b/app/assets/stylesheets/components/page-footer.scss @@ -12,7 +12,7 @@ &-delete-link { line-height: 40px; - padding: 0 0 0 5px; + padding: 1px 0 0 15px; a { diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index 1b40c5fa8..36c137bcc 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -7,6 +7,12 @@ margin: 40px 0 5px 0; } +.table-field-headings { + th { + padding: 0 0 5px 0; + } +} + %table-field, .table-field { @@ -36,6 +42,19 @@ } + &-yes, + &-no { + display: block; + text-indent: -999em; + background-size: 19px 19px; + background-repeat: no-repeat; + background-position: 50% 50%; + } + + &-yes { + background-image: file-url('tick.png'); + } + &-missing { color: $error-colour; font-weight: bold; @@ -77,4 +96,5 @@ margin-top: -20px; border-bottom: 1px solid $border-colour; padding-bottom: 10px; + text-align: center; } diff --git a/app/assets/stylesheets/components/yes-no.scss b/app/assets/stylesheets/components/yes-no.scss new file mode 100644 index 000000000..9a71113a5 --- /dev/null +++ b/app/assets/stylesheets/components/yes-no.scss @@ -0,0 +1,36 @@ +.yes-no-wrapper { + border-bottom: 1px solid $border-colour; + margin: 0 0 $gutter 0; +} + +.yes-no { + + border-top: 1px solid $border-colour; + padding: 10px 0; + + &-label { + padding-top: 19px; + float: left; + } + + &-fields { + + text-align: right; + + .block-label { + + @include media(tablet) { + + margin-bottom: 0; + + &:last-child { + margin-right: 0; + } + + } + + } + + } + +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index eb420be44..e9c36ad7a 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -47,6 +47,7 @@ $path: '/static/images/'; @import 'components/browse-list'; @import 'components/email-message'; @import 'components/api-key'; +@import 'components/yes-no'; @import 'views/job'; @import 'views/edit-template'; diff --git a/app/main/__init__.py b/app/main/__init__.py index 7ce30ff8a..fbfd2562a 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -5,5 +5,5 @@ main = Blueprint('main', __name__) from app.main.views import ( index, sign_in, sign_out, register, two_factor, verify, sms, add_service, code_not_received, jobs, dashboard, templates, service_settings, forgot_password, - new_password, styleguide, user_profile, choose_service, api_keys + new_password, styleguide, user_profile, choose_service, api_keys, manage_users ) diff --git a/app/main/dao/services_dao.py b/app/main/dao/services_dao.py index 604b2e27d..77f6610dc 100644 --- a/app/main/dao/services_dao.py +++ b/app/main/dao/services_dao.py @@ -1,7 +1,7 @@ -from flask import url_for +from flask import url_for, abort from app import notifications_api_client -from notifications_python_client.errors import HTTPError from app.utils import BrowsableItem +from notifications_python_client.errors import HTTPError def insert_new_service(service_name, user_id): @@ -29,7 +29,9 @@ def get_service_by_id(id_): def get_service_by_id_or_404(id_): try: - return get_service_by_id(id_) + return notifications_api_client.get_service(id_)['data'] + except KeyError: + abort(404) except HTTPError as e: if e.status_code == 404: abort(404) diff --git a/app/main/forms.py b/app/main/forms.py index ddab3fe01..20b007514 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -21,10 +21,10 @@ from app.utils import ( ) -def email_address(): +def email_address(label='Email address'): gov_uk_email \ = "(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*.gov.uk)" - return EmailField('Email address', validators=[ + return EmailField(label, validators=[ Length(min=5, max=255), DataRequired(message='Email cannot be empty'), Email(message='Enter a valid email address'), @@ -96,6 +96,10 @@ class RegisterUserForm(Form): password = password() +class InviteUserForm(Form): + email_address = email_address('Their email address') + + class TwoFactorForm(Form): def __init__(self, validate_code_func, *args, **kwargs): ''' diff --git a/app/main/views/index.py b/app/main/views/index.py index 151ee4e80..1200cf65b 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -34,63 +34,3 @@ def send_email(service_id): @login_required def check_email(service_id): return render_template('views/check-email.html') - - -@main.route("/services//manage-users") -@login_required -def manage_users(service_id): - users = [ - { - 'name': 'Henry Hadlow', - 'permission_send_messages': True, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - - { - 'name': 'Pete Herlihy', - 'permission_send_messages': False, - 'permission_manage_service': False, - 'permission_manage_api_keys': False, - }, - { - 'name': 'Chris Hill-Scott', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - }, - { - 'name': 'Martyn Inglis', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - } - ] - invited_users = [ - { - 'email_localpart': 'caley.smolska', - 'permission_send_messages': True, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - - { - 'email_localpart': 'ash.stephens', - 'permission_send_messages': False, - 'permission_manage_service': False, - 'permission_manage_api_keys': False - }, - { - 'email_localpart': 'nicholas.staples', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - }, - { - 'email_localpart': 'adam.shimali', - 'permission_send_messages': True, - 'permission_manage_service': True, - 'permission_manage_api_keys': True - } - ] - return render_template('views/manage-users.html', service_id=service_id, users=users, invited_users=invited_users) diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py new file mode 100644 index 000000000..df609ee8f --- /dev/null +++ b/app/main/views/manage_users.py @@ -0,0 +1,96 @@ +from flask import ( + request, + render_template, + redirect, + abort, + url_for, + flash +) + +from flask_login import login_required, current_user + +from app.main import main +from app.main.dao import users_dao +from app.main.forms import InviteUserForm +from app.main.dao.services_dao import get_service_by_id_or_404 +from app import user_api_client + +fake_users = [ + { + 'name': '', + 'permission_send_messages': True, + 'permission_manage_service': True, + 'permission_manage_api_keys': True, + 'active': True + } +] + + +@main.route("/services//users") +@login_required +def manage_users(service_id): + return render_template( + 'views/manage-users.html', + service_id=service_id, + users=fake_users, + current_user=current_user, + invited_users=[] + ) + + +@main.route("/services//users/invite", methods=['GET', 'POST']) +@login_required +def invite_user(service_id): + + form = InviteUserForm() + + if form.validate_on_submit(): + flash('Invite sent to {}'.format(form.email_address.data), 'default_with_tick') + return redirect(url_for('.manage_users', service_id=service_id)) + + return render_template( + 'views/invite-user.html', + user={}, + service=get_service_by_id_or_404(service_id), + service_id=service_id, + form=form + ) + + +@main.route("/services//users/", methods=['GET', 'POST']) +@login_required +def edit_user(service_id, user_id): + + if request.method == 'POST': + return redirect(url_for('.manage_users', service_id=service_id)) + + return render_template( + 'views/invite-user.html', + user=fake_users[int(user_id)], + user_id=user_id, + service=get_service_by_id_or_404(service_id), + service_id=service_id + ) + + +@main.route("/services//users//delete", methods=['GET', 'POST']) +@login_required +def delete_user(service_id, user_id): + + if request.method == 'POST': + return redirect(url_for('.manage_users', service_id=service_id)) + + user = fake_users[int(user_id)] + + flash( + 'Are you sure you want to delete {}’s account?'.format(user.get('name') or user['email_localpart']), + 'delete' + ) + + return render_template( + 'views/invite-user.html', + user=user, + user_id=user_id, + service=get_service_by_id_or_404(service_id), + service_id=service_id + ) diff --git a/app/main/views/styleguide.py b/app/main/views/styleguide.py index e2b540708..eed30b20e 100644 --- a/app/main/views/styleguide.py +++ b/app/main/views/styleguide.py @@ -1,6 +1,7 @@ from flask import render_template, current_app, abort from flask_wtf import Form from wtforms import StringField, PasswordField, TextAreaField, FileField, validators +from utils.template import Template from app.main import main @@ -17,13 +18,16 @@ def styleguide(): message = TextAreaField(u'Message') file_upload = FileField('Upload a CSV file to add your recipients’ details') + sms = "Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax" + form = FormExamples() - - form.message.data = "Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax" # noqa - + form.message.data = sms form.validate() + template = Template({'content': sms}) + return render_template( 'views/styleguide.html', - form=form + form=form, + template=template ) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index a8453f9e5..be1001113 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -15,22 +15,10 @@ from app.main.dao import services_dao as sdao @main.route("/services//templates") @login_required def manage_service_templates(service_id): - try: - jobs = job_api_client.get_job(service_id)['data'] - except HTTPError as e: - if e.status_code == 404: - abort(404) - else: - raise e - return render_template( - 'views/manage-templates.html', - service_id=service_id, - has_jobs=bool(jobs), - templates=[ - Template(template) - for template in tdao.get_service_templates(service_id)['data'] - ] - ) + return redirect(url_for( + '.choose_sms_template', + service_id=service_id + )) @main.route("/services//templates/add", methods=['GET', 'POST']) @@ -50,10 +38,10 @@ def add_service_template(service_id): tdao.insert_service_template( form.name.data, form.template_content.data, service_id) return redirect(url_for( - '.manage_service_templates', service_id=service_id)) + '.choose_sms_template', service_id=service_id)) return render_template( 'views/edit-template.html', - h1='Add template', + h1='Add a text message template', form=form, service_id=service_id) @@ -69,7 +57,7 @@ def edit_service_template(service_id, template_id): tdao.update_service_template( template_id, form.name.data, form.template_content.data, service_id) - return redirect(url_for('.manage_service_templates', service_id=service_id)) + return redirect(url_for('.choose_sms_template', service_id=service_id)) return render_template( 'views/edit-template.html', diff --git a/app/templates/components/file-upload.html b/app/templates/components/file-upload.html index 1451b0ee3..437ce699f 100644 --- a/app/templates/components/file-upload.html +++ b/app/templates/components/file-upload.html @@ -1,7 +1,7 @@ {% macro file_upload(field, button_text="Choose file") %}
{% endfor %} - {% else %} - {{ banner( - 'Add a text message template to start sending messages'.format( - url_for(".add_service_template", service_id=service_id) - )|safe, - type="tip" - )}} {% endif %} +

+ Add a new template +

+ {% endblock %} diff --git a/app/templates/views/edit-template.html b/app/templates/views/edit-template.html index 2563dd81e..4720a2a61 100644 --- a/app/templates/views/edit-template.html +++ b/app/templates/views/edit-template.html @@ -30,9 +30,9 @@ {{ page_footer( 'Save', delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, - delete_link_text='delete this template', - secondary_link=url_for('.manage_service_templates', service_id=service_id), - secondary_link_text='Back to templates' + delete_link_text='Delete this template', + back_link=url_for('.choose_sms_template', service_id=service_id), + back_link_text='Cancel' ) }} diff --git a/app/templates/views/invite-user.html b/app/templates/views/invite-user.html new file mode 100644 index 000000000..d1e6e5c7b --- /dev/null +++ b/app/templates/views/invite-user.html @@ -0,0 +1,50 @@ +{% extends "withnav_template.html" %} +{% from "components/yes-no.html" import yes_no %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} + +{% block page_title %} +Manage users – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + +

+ {{ user.name or user.email_localpart or "Add a new team member" }} +

+ +
+
+ + {% if user %} +

+ {{ current_user.email_address }} +

+ {% else %} + {{ textbox(form.email_address, hint='Email address must end in .gov.uk', width='1-1') }} + {% endif %} + +
+ + Permissions + + {{ yes_no('send_messages', 'Send messages', user.permission_send_messages) }} + {{ yes_no('manage_service', 'Manage service', user.permission_manage_service) }} + {{ yes_no('manage_api_keys', 'Manage API keys', user.permission_manage_api_keys) }} +
+ + {% if user %} + {{ page_footer( + 'Save', + delete_link=url_for('.delete_user', service_id=service_id, user_id=user_id), + delete_link_text='Delete this account', + back_link=url_for('.manage_users', service_id=service_id), + back_link_text='Cancel' + ) }} + {% else %} + {{ page_footer('Send invitation email') }} + {% endif %} + +
+
+{% endblock %} diff --git a/app/templates/views/manage-templates.html b/app/templates/views/manage-templates.html deleted file mode 100644 index 346c87739..000000000 --- a/app/templates/views/manage-templates.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "withnav_template.html" %} -{% from "components/sms-message.html" import sms_message %} -{% from "components/email-message.html" import email_message %} -{% from "components/browse-list.html" import browse_list %} - -{% block page_title %} -Manage templates – GOV.UK Notify -{% endblock %} - -{% block maincolumn_content %} - -

Manage templates

- - - {% if not has_jobs %} - {{ banner( - 'Send yourself a text message'.format( - url_for(".choose_sms_template", service_id=service_id) - )|safe, - subhead='Next step', - type="tip" - )}} - {% endif %} - -
-
- - {% for template in templates %} - {% 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=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 %} - -

- Add new template -

- -
-
- -{% endblock %} diff --git a/app/templates/views/manage-users.html b/app/templates/views/manage-users.html index ac881a790..c12fb8ddc 100644 --- a/app/templates/views/manage-users.html +++ b/app/templates/views/manage-users.html @@ -1,65 +1,55 @@ {% extends "withnav_template.html" %} -{% from "components/table.html" import list_table, row, field %} +{% from "components/table.html" import list_table, row, field, boolean_field, hidden_field_heading %} {% from "components/page-footer.html" import page_footer %} +{% set table_options = { + 'field_headings': [ + 'Name', 'Send messages', 'Manage service', 'Manage API keys', hidden_field_heading('Link to change') + ], + 'field_headings_visible': True, + 'caption_visible': True +} %} + {% block page_title %} Manage users – GOV.UK Notify {% endblock %} {% block maincolumn_content %} -

Manage users

+

+ Manage team +

-

- Invite users -

+ Invite a team member -{% call(item) list_table( - users, - caption='Active users', - field_headings=['Name', 'Send messages', 'Manage Service', 'Manage API keys', 'Link to change'], - field_headings_visible=True, - caption_visible=True -) %} - {% call field() %} - {{ item.name }} + {% call(item) list_table( + users, caption='Active', **table_options + ) %} + {% call field() %} + {{ current_user.name }} + {% endcall %} + {{ boolean_field(item.permission_send_messages) }} + {{ boolean_field(item.permission_manage_service) }} + {{ boolean_field(item.permission_manage_api_keys) }} + {% call field(align='right') %} + Change + {% endcall %} {% endcall %} - {% call field() %} - {{ "✔" if item.permission_send_messages else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_service else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_api_keys else "❌" }} - {% endcall %} - {% call field(align='right') %} - Change - {% endcall %} -{% endcall %} -{% call(item) list_table( - invited_users, - caption='Invited users', - field_headings=['Name', 'Send messages', 'Manage Service', 'Manage API keys', 'Link to change'], - field_headings_visible=True, - caption_visible=True -) %} - {% call field() %} - {{ item.email_localpart }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_send_messages else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_service else "❌" }} - {% endcall %} - {% call field() %} - {{ "✔" if item.permission_manage_api_keys else "❌" }} - {% endcall %} - {% call field(align='right') %} - Change - {% endcall %} -{% endcall %} + {% if invited_users %} + {% call(item) list_table( + invited_users, caption='Invited', **table_options + ) %} + {% call field() %} + {{ item.email_localpart }} + {% endcall %} + {{ boolean_field(item.permission_send_messages) }} + {{ boolean_field(item.permission_manage_service) }} + {{ boolean_field(item.permission_manage_api_keys) }} + {% call field(align='right') %} + Change + {% endcall %} + {% endcall %} + {% endif %} {% endblock %} diff --git a/app/templates/views/send-sms.html b/app/templates/views/send-sms.html index 351115c7f..32be81c72 100644 --- a/app/templates/views/send-sms.html +++ b/app/templates/views/send-sms.html @@ -20,16 +20,11 @@ - {{file_upload(form.file, button_text='Choose a CSV file')}} -

Download an example CSV file

- {{ page_footer( - "Continue to preview" - ) }} - + {{file_upload(form.file, button_text='Upload a CSV file')}} {% endblock %} diff --git a/app/templates/views/service_dashboard.html b/app/templates/views/service_dashboard.html index 09443fc40..e0b5527a5 100644 --- a/app/templates/views/service_dashboard.html +++ b/app/templates/views/service_dashboard.html @@ -25,21 +25,9 @@ {% if not jobs %} {{ banner( - """ -
    -
  1. - Add a template -
  2. -
  3. - Send yourself a text message -
  4. -
- """.format( - url_for(".add_service_template", service_id=service_id), - url_for(".choose_sms_template", service_id=service_id) - )|safe, + 'Send yourself a text message', subhead='Get started', - type="tip" + type='tip' )}} {% else %} {% call(item) list_table( diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html index a52144df3..a45f1a30e 100644 --- a/app/templates/views/styleguide.html +++ b/app/templates/views/styleguide.html @@ -5,9 +5,11 @@ {% from "components/browse-list.html" import browse_list %} {% from "components/page-footer.html" import page_footer %} {% from "components/sms-message.html" import sms_message %} -{% from "components/table.html" import mapping_table, list_table, row, field, right_aligned_field_heading %} +{% from "components/email-message.html" import email_message %} +{% from "components/table.html" import mapping_table, list_table, row, field, text_field, boolean_field, right_aligned_field_heading %} {% from "components/textbox.html" import textbox %} {% from "components/file-upload.html" import file_upload %} +{% from "components/yes-no.html" import yes_no %} {% from "components/api-key.html" import api_key %} {% block page_title %} @@ -25,21 +27,26 @@

Banner

-

Used to show the status of a thing or action.

+
+
+

Used to show the status of a thing or action.

- {{ banner("You sent 1,234 text messages", with_tick=True) }} + {{ banner("You sent 1,234 text messages", with_tick=True) }} - {{ banner('You’re not allowed to do this', 'dangerous')}} + {{ banner('You’re not allowed to do this', 'dangerous')}} - {{ banner('Are you sure you want to delete?', 'dangerous', delete_button="Yes, delete this thing")}} + {{ banner('Are you sure you want to delete?', 'dangerous', delete_button="Yes, delete this thing")}} - {{ banner( - 'Send your first message'|safe, - subhead='Get started', - type='tip' - )}} + {{ banner( + 'Send your first message'|safe, + subhead='Get started', + type='tip' + )}} + + {{ banner('You could go to jail', 'important')}} +
+
- {{ banner('You could go to jail', 'important')}}

Big number

@@ -118,7 +125,7 @@

SMS message

-

Used to show, preview or choose an SMS message.

+

Used to show, preview or choose an SMS template.

@@ -127,82 +134,105 @@ name='Two week reminder', ) }} {{ sms_message( - 'Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax' + template.formatted_as_markup ) }} {{ sms_message( - 'Your vehicle tax for registration number is due on date. Renew online at www.gov.uk/vehicle-tax', + 'Your vehicle tax for LC12 BFL is due on 1 March 2016. Renew online at www.gov.uk/vehicle-tax', '+44 7700 900 306' ) }} {{ sms_message( - 'Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax', + template.formatted_as_markup, name='Two week reminder', edit_link='#' ) }}
+

Email message

+ +

Used to show, preview or choose an email template.

+ +
+
+ {{ email_message( + subject="Vehicle tax reminder", + body="Dear Alice Smith,\n\nYour vehicle tax for LC12 BFL is due on 1 March 2016.\n\nRenew online at www.gov.uk/vehicle-tax", + from_name="Vehicle tax", + from_address="vehicle.tax@notifications.service.gov.uk", + name="Two week reminder", + ) }} +
+
+

Tables

- {% call mapping_table( - caption='Account settings', - field_headings=['Label', 'Value', 'Action'], - field_headings_visible=False, - caption_visible=True - ) %} - {% call row() %} - {% call field() %} - Username +
+
+

+ Used for comparing rows of data. +

+ {% call mapping_table( + caption='Account settings', + field_headings=['Label', 'True', 'False', 'Action'], + field_headings_visible=False, + caption_visible=True + ) %} + {% call row() %} + {{ text_field('Username' )}} + {{ boolean_field(True) }} + {{ boolean_field(False) }} + {% call field(align='right') %} + Change + {% endcall %} + {% endcall %} {% endcall %} - {% call field() %} - admin - {% endcall %} - {% call field(align='right') %} - Change - {% endcall %} - {% endcall %} - {% endcall %} - {% call(item) list_table( - [ - { - 'file': 'dispatch_20151114.csv', 'status': 'Queued' - }, - { - 'file': 'dispatch_20151117.csv', 'status': 'Delivered' - }, - { - 'file': 'remdinder_monday.csv', 'status': 'Failed' - } - ], - caption='Messages', - field_headings=['File', right_aligned_field_heading('Status')], - field_headings_visible=True, - caption_visible=False - ) %} - {% call field() %} - {{ item.file }} - {% endcall %} - {% call field( - align='right', - status='error' if item.status == 'Failed' else 'default' - ) %} - {{ item.status }} - {% endcall %} - {% endcall %} + {% call(item) list_table( + [ + { + 'file': 'dispatch_20151114.csv', 'status': 'Queued' + }, + { + 'file': 'dispatch_20151117.csv', 'status': 'Delivered' + }, + { + 'file': 'remdinder_monday.csv', 'status': 'Failed' + } + ], + caption='Messages', + field_headings=['File', right_aligned_field_heading('Status')], + field_headings_visible=True, + caption_visible=False + ) %} + {% call field() %} + {{ item.file }} + {% endcall %} + {% call field( + align='right', + status='error' if item.status == 'Failed' else 'default' + ) %} + {{ item.status }} + {% endcall %} + {% endcall %} - {% call(item) list_table( - [], - caption='Jobs', - field_headings=['Job', 'Time'], - caption_visible=True, - empty_message='You haven’t scheduled any jobs yet' - ) %} - {% call field() %} - {{ item.job }} - {% endcall %} - {% call field() %} - {{ item.time }} - {% endcall %} - {% endcall %} + {% call(item) list_table( + [], + caption='Jobs', + field_headings=['Job', 'Time'], + caption_visible=True, + empty_message='You haven’t scheduled any jobs yet' + ) %} + {% call field() %} + {{ item.job }} + {% endcall %} + {% call field() %} + {{ item.time }} + {% endcall %} + {% endcall %} + +
+

Textbox

{{ textbox(form.username) }} @@ -213,6 +243,17 @@

File upload

{{ file_upload(form.file_upload) }} +

Yes/no

+
+
+
+ {{ yes_no('manage_service', 'Manage service', True) }} + {{ yes_no('templates', 'Create templates', True) }} +
+
+
+ +

API key

{{ api_key('d30512af92e1386d63b90e5973b49a10') }} diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 7fbc03610..006e1505a 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -15,7 +15,9 @@ var gulp = require('gulp'), src: 'app/assets/', dist: 'app/static/', templates: 'app/templates/', - npm: 'node_modules/' + npm: 'node_modules/', + template: 'node_modules/govuk_template_jinja/', + toolkit: 'node_modules/govuk_frontend_toolkit/' }; // 3. TASKS @@ -23,18 +25,24 @@ var gulp = require('gulp'), // Move GOV.UK template resources -gulp.task('copy:govuk_template:template', () => gulp.src(paths.npm + '/govuk_template_jinja/views/layouts/govuk_template.html') +gulp.task('copy:govuk_template:template', () => gulp.src(paths.template + 'views/layouts/govuk_template.html') .pipe(gulp.dest(paths.templates)) ); -gulp.task('copy:govuk_template:assets', () => gulp.src(paths.npm + '/govuk_template_jinja/assets/**/*') - .pipe(gulp.dest(paths.dist)) +gulp.task('copy:govuk_template:css', () => gulp.src(paths.template + 'assets/stylesheets/**/*.css') + .pipe(plugins.sass({outputStyle: 'compressed'})) + .pipe(gulp.dest(paths.dist + 'stylesheets/')) +); + +gulp.task('copy:govuk_template:js', () => gulp.src(paths.template + 'assets/javascripts/**/*.js') + .pipe(plugins.uglify()) + .pipe(gulp.dest(paths.dist + 'javascripts/')) ); gulp.task('javascripts', () => gulp .src([ - paths.npm + 'govuk_frontend_toolkit/javascripts/govuk/modules.js', - paths.npm + 'govuk_frontend_toolkit/javascripts/govuk/selection-buttons.js', + paths.toolkit + 'javascripts/govuk/modules.js', + paths.toolkit + 'javascripts/govuk/selection-buttons.js', paths.src + 'javascripts/apiKey.js', paths.src + 'javascripts/autofocus.js', paths.src + 'javascripts/highlightTags.js', @@ -59,10 +67,11 @@ gulp.task('sass', () => gulp outputStyle: 'compressed', includePaths: [ paths.npm + 'govuk-elements-sass/public/sass/', - paths.npm + 'govuk_frontend_toolkit/stylesheets/' + paths.toolkit + 'stylesheets/' ] })) - .pipe(gulp.dest(paths.dist + '/stylesheets')) + .pipe(plugins.base64({baseDir: 'app'})) + .pipe(gulp.dest(paths.dist + 'stylesheets/')) ); @@ -71,9 +80,10 @@ gulp.task('sass', () => gulp gulp.task('images', () => gulp .src([ paths.src + 'images/**/*', - paths.npm + 'govuk_frontend_toolkit/images/**/*' + paths.toolkit + 'images/**/*', + paths.template + 'assets/images/**/*' ]) - .pipe(gulp.dest(paths.dist + '/images')) + .pipe(gulp.dest(paths.dist + 'images/')) ); @@ -82,10 +92,11 @@ gulp.task('watchForChanges', function() { gulp.watch(paths.src + 'javascripts/**/*', ['javascripts']); gulp.watch(paths.src + 'stylesheets/**/*', ['sass']); gulp.watch(paths.src + 'images/**/*', ['images']); + gulp.watch('gulpfile.babel.js', ['default']); }); gulp.task('lint:sass', () => gulp - .src(paths.src + '/stylesheets/**/*.scss') + .src(paths.src + 'stylesheets/**/*.scss') .pipe(plugins.sassLint()) .pipe(plugins.sassLint.format(stylish)) .pipe(plugins.sassLint.failOnError()) @@ -104,7 +115,14 @@ gulp.task('lint', // Default: compile everything gulp.task('default', - ['copy:govuk_template:template', 'copy:govuk_template:assets', 'javascripts', 'sass', 'images'] + [ + 'copy:govuk_template:template', + 'copy:govuk_template:css', + 'copy:govuk_template:js', + 'javascripts', + 'sass', + 'images' + ] ); // Optional: recompile on changes diff --git a/package.json b/package.json index add7e8f76..fdc1d7edf 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "gulp": "3.9.0", "gulp-add-src": "0.2.0", "gulp-babel": "6.1.1", + "gulp-base64": "0.1.3", "gulp-concat": "2.6.0", "gulp-include": "2.1.0", "gulp-jquery": "1.1.1", diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 4fd7148fb..bf6303f25 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -6,4 +6,4 @@ def test_owasp_useful_headers_set(app_): assert response.headers['X-Frame-Options'] == 'deny' assert response.headers['X-Content-Type-Options'] == 'nosniff' assert response.headers['X-XSS-Protection'] == '1; mode=block' - assert response.headers['Content-Security-Policy'] == "default-src 'self' 'unsafe-inline'; font-src 'self' data:;" # noqa + assert response.headers['Content-Security-Policy'] == "default-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;" # noqa diff --git a/tests/app/main/views/test_manage_users.py b/tests/app/main/views/test_manage_users.py new file mode 100644 index 000000000..62ee8d642 --- /dev/null +++ b/tests/app/main/views/test_manage_users.py @@ -0,0 +1,84 @@ +import json +from flask import url_for + + +def test_should_show_overview_page( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.manage_users', service_id=55555)) + + assert 'Manage team' in response.get_data(as_text=True) + assert response.status_code == 200 + + +def test_should_show_page_for_one_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.edit_user', service_id=55555, user_id=0)) + + assert response.status_code == 200 + + +def test_redirect_after_saving_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.post(url_for( + 'main.edit_user', service_id=55555, user_id=0 + )) + + assert response.status_code == 302 + assert response.location == url_for( + 'main.manage_users', service_id=55555, _external=True + ) + + +def test_should_show_page_for_inviting_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.get(url_for('main.invite_user', service_id=55555)) + + assert 'Add a new team member' in response.get_data(as_text=True) + assert response.status_code == 200 + + +def test_invite_user( + app_, + api_user_active, + mock_login, + mock_get_service +): + with app_.test_request_context(): + with app_.test_client() as client: + client.login(api_user_active) + response = client.post( + url_for('main.invite_user', service_id=55555), + data={'email_address': 'test@example.gov.uk'}, + follow_redirects=True + ) + + assert response.status_code == 200 + assert 'Invite sent to test@example.gov.uk' in response.get_data(as_text=True) diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index 3319f1468..cecc59353 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -16,7 +16,7 @@ def test_should_return_list_of_all_templates(app_, client.login(api_user_active) service_id = str(uuid.uuid4()) response = client.get(url_for( - '.manage_service_templates', service_id=service_id)) + '.manage_service_templates', service_id=service_id), follow_redirects=True) assert response.status_code == 200 mock_get_service_templates.assert_called_with(service_id) @@ -72,7 +72,7 @@ def test_should_redirect_when_saving_a_template(app_, assert response.status_code == 302 assert response.location == url_for( - '.manage_service_templates', service_id=service_id, _external=True) + '.choose_sms_template', service_id=service_id, _external=True) mock_update_service_template.assert_called_with( template_id, name, 'sms', content, service_id)