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" }}
+
+
+
+{% 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(
- """
-
-
- Add a template
-
-
- Send yourself a text message
-
-
- """.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 %}
+
+ Add a job now
+
+
+
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)