Merge pull request #187 from alphagov/invite-users

Add pages to invite, edit, and delete users
This commit is contained in:
minglis
2016-02-22 14:13:39 +00:00
39 changed files with 578 additions and 342 deletions

View File

@@ -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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

BIN
app/assets/images/tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

BIN
app/assets/images/tick.psd Normal file

Binary file not shown.

View File

@@ -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);
};

View File

@@ -66,3 +66,16 @@ a {
font-family: monospace;
overflow-x: scroll;
}
.inline {
.block-label {
@include media(tablet) {
float: none;
display: inline-block;
}
}
}

View File

@@ -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 {

View File

@@ -23,7 +23,7 @@
}
&-button {
@include button($panel-colour);
@include button($button-colour);
display: inline-block;
}

View File

@@ -12,7 +12,7 @@
&-delete-link {
line-height: 40px;
padding: 0 0 0 5px;
padding: 1px 0 0 15px;
a {

View File

@@ -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;
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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';

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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):
'''

View File

@@ -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/<service_id>/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)

View File

@@ -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/<service_id>/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/<service_id>/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/<service_id>/users/<user_id>", 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/<service_id>/users/<user_id>/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
)

View File

@@ -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
)

View File

@@ -15,22 +15,10 @@ from app.main.dao import services_dao as sdao
@main.route("/services/<service_id>/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/<service_id>/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',

View File

@@ -1,7 +1,7 @@
{% macro file_upload(field, button_text="Choose file") %}
<div class="form-group{% if field.errors %} error{% endif %}" data-module="file-upload">
<label class="file-upload-label" for="{{ field.name }}">
{{ field.label }}
<span class="visually-hidden">{{ field.label }}</span>
{% if hint %}
<span class="form-hint">
{{ hint }}

View File

@@ -13,14 +13,14 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="button{% if destructive %}-destructive{% endif %}" value="{{ button_text }}" />
{% endif %}
{% if delete_link %}
<span class="page-footer-delete-link">
or <a href="{{ delete_link }}">{{ delete_link_text }}</a>
</span>
{% endif %}
{% if back_link %}
<a class="page-footer-back-link" href="{{ back_link }}">{{ back_link_text }}</a>
{% endif %}
{% if delete_link %}
<span class="page-footer-delete-link">
<a href="{{ delete_link }}">{{ delete_link_text }}</a>
</span>
{% endif %}
{% if secondary_link and secondary_link_text %}
<a class="page-footer-secondary-link" href="{{ secondary_link }}">{{ secondary_link_text }}</a>
{% endif %}

View File

@@ -55,6 +55,18 @@
</td>
{%- endmacro %}
{% macro text_field(text) -%}
{% call field() %}
{{ text }}
{% endcall %}
{%- endmacro %}
{% macro boolean_field(yes) -%}
{% call field(status='yes' if yes else 'no') %}
{{ "Yes" if yes else "No" }}
{% endcall %}
{%- endmacro %}
{% macro right_aligned_field_heading(text) %}
<span class="table-field-heading-right-aligned">{{ text }}</span>
{%- endmacro %}

View File

@@ -0,0 +1,17 @@
{% macro yes_no(name, label, current_value=None) %}
<fieldset class='yes-no'>
<legend class='yes-no-label'>
{{ label }}
</legend>
<div class='yes-no-fields inline'>
<label class='block-label'>
<input type='radio' name='{{ name }}' {% if current_value == True %}checked{% endif %} />
Yes
</label>
<label class='block-label'>
<input type='radio' name='{{ name }}' {% if current_value == False %}checked{% endif %} />
No
</label>
</div>
</fieldset>
{% endmacro %}

View File

@@ -5,7 +5,7 @@
{{ banner(
message,
'default' if ((category == 'default') or (category == 'default_with_tick')) else 'dangerous',
delete_button="Yes, delete this template" if 'delete' == category else None,
delete_button="Yes, delete" if 'delete' == category else None,
with_tick=True if category == 'default_with_tick' else False
)}}
{% endfor %}

View File

@@ -7,12 +7,11 @@
<li><a href="{{ url_for('.send_email', service_id=service_id) }}">Send emails</a></li>
</ul>
<ul>
<li><a href="{{ url_for('.manage_service_templates', service_id=service_id) }}">Manage templates</a></li>
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage users</a></li>
<li><a href="{{ url_for('.service_settings', service_id=service_id) }}">Manage service settings</a></li>
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage team</a></li>
<li><a href="{{ url_for('.service_settings', service_id=service_id) }}">Manage settings</a></li>
</ul>
<ul>
<li><a href="{{ url_for('.documentation', service_id=service_id) }}">Developer documentation</a></li>
<li><a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys</a></li>
<li><a href="{{ url_for('.documentation', service_id=service_id) }}">Developer documentation</a></li>
</ul>
</nav>

View File

@@ -57,7 +57,7 @@
{% endif %}
{% endcall %}
<p>
<p class='table-show-more-link'>
<a href="{{ url_for('.create_api_key', service_id=service_id) }}">Create a new API key</a>
</p>

View File

@@ -23,18 +23,16 @@
<div class="sms-message-use-links">
<a href="{{ url_for(".send_sms", service_id=service_id, template_id=template.id) }}">Add recipients</a>
<a href="{{ url_for(".send_sms_to_self", service_id=service_id, template_id=template.id) }}">Send yourself a test</a>
<a href="{{ url_for(".edit_service_template", service_id=service_id, template_id=template.id) }}">Edit template</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
{{ banner(
'<a href="{}">Add a text message template</a> to start sending messages'.format(
url_for(".add_service_template", service_id=service_id)
)|safe,
type="tip"
)}}
{% endif %}
<p>
<a href="{{ url_for('.add_service_template', service_id=service_id) }}" class="button">Add a new template</a>
</p>
</form>
{% endblock %}

View File

@@ -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'
) }}
</form>

View File

@@ -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 %}
<h1 class="heading-large">
{{ user.name or user.email_localpart or "Add a new team member" }}
</h1>
<div class="grid-row">
<form method="post" class="column-three-quarters">
{% if user %}
<p class='bottom-gutter'>
{{ current_user.email_address }}
</p>
{% else %}
{{ textbox(form.email_address, hint='Email address must end in .gov.uk', width='1-1') }}
{% endif %}
<fieldset class='yes-no-wrapper'>
<legend class='heading-small'>
Permissions
</legend>
{{ 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) }}
</fieldset>
{% 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 %}
</form>
</div>
{% endblock %}

View File

@@ -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 %}
<h1 class="heading-large">Manage templates</h1>
{% if not has_jobs %}
{{ banner(
'<a href="{}">Send yourself a text message</a>'.format(
url_for(".choose_sms_template", service_id=service_id)
)|safe,
subhead='Next step',
type="tip"
)}}
{% endif %}
<div class="grid-row">
<div class="column-two-thirds">
{% 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 %}
<p>
<a href="{{ url_for('.add_service_template', service_id=service_id) }}" class="button">Add new template</a>
</p>
</div>
</div>
{% endblock %}

View File

@@ -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 %}
<h1 class="heading-large">Manage users</h1>
<h1 class="heading-large">
Manage team
</h1>
<p>
<a href="#" class="button">Invite users</a>
</p>
<a href="{{ url_for('.invite_user', service_id=service_id) }}" class="button">Invite a team member</a>
{% 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') %}
<a href="{{ url_for('.edit_user', service_id=service_id, user_id=0)}}">Change</a>
{% 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') %}
<a href="#">Change</a>
{% 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') %}
<a href="#">Change</a>
{% 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') %}
<a href="{{ url_for('.edit_user', service_id=service_id, user_id=item.id)}}">Change</a>
{% endcall %}
{% endcall %}
{% endif %}
{% endblock %}

View File

@@ -20,16 +20,11 @@
</div>
</div>
{{file_upload(form.file, button_text='Choose a CSV file')}}
<p>
<a href="{{ url_for('.get_example_csv', service_id=service_id, template_id=template.id) }}">Download an example CSV file</a>
</p>
{{ page_footer(
"Continue to preview"
) }}
{{file_upload(form.file, button_text='Upload a CSV file')}}
</form>
{% endblock %}

View File

@@ -25,21 +25,9 @@
{% if not jobs %}
{{ banner(
"""
<ol>
<li>
<a href='{}'>Add a template</a>
</li>
<li>
<a href='{}'>Send yourself a text message</a>
</li>
</ol>
""".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(

View File

@@ -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 @@
</p>
<h2 class="heading-large">Banner</h2>
<p>Used to show the status of a thing or action.</p>
<div class="grid-row">
<div class="column-three-quarters">
<p>Used to show the status of a thing or action.</p>
{{ banner("You sent 1,234 text messages", with_tick=True) }}
{{ banner("You sent 1,234 text messages", with_tick=True) }}
{{ banner('Youre not allowed to do this', 'dangerous')}}
{{ banner('Youre 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(
'<a href="#">Send your first message</a>'|safe,
subhead='Get started',
type='tip'
)}}
{{ banner(
'<a href="#">Send your first message</a>'|safe,
subhead='Get started',
type='tip'
)}}
{{ banner('You could go to jail', 'important')}}
</div>
</div>
{{ banner('You could go to jail', 'important')}}
<h2 class="heading-large">Big number</h2>
@@ -118,7 +125,7 @@
<h2 class="heading-large">SMS message</h2>
<p>Used to show, preview or choose an SMS message.</p>
<p>Used to show, preview or choose an SMS template.</p>
<div class="grid-row">
<div class="column-half">
@@ -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='#'
) }}
</div>
</div>
<h2 class="heading-large">Email message</h2>
<p>Used to show, preview or choose an email template.</p>
<div class="grid-row">
<div class="column-two-thirds">
{{ 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",
) }}
</div>
</div>
<h2 class="heading-large">Tables</h2>
{% call mapping_table(
caption='Account settings',
field_headings=['Label', 'Value', 'Action'],
field_headings_visible=False,
caption_visible=True
) %}
{% call row() %}
{% call field() %}
Username
<div class="grid-row">
<div class="column-three-quarters">
<p>
Used for comparing rows of data.
</p>
{% 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') %}
<a href="#">Change</a>
{% endcall %}
{% endcall %}
{% endcall %}
{% call field() %}
admin
{% endcall %}
{% call field(align='right') %}
<a href="#">Change</a>
{% 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 havent 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 havent scheduled any jobs yet'
) %}
{% call field() %}
{{ item.job }}
{% endcall %}
{% call field() %}
{{ item.time }}
{% endcall %}
{% endcall %}
<p class="table-show-more-link">
<a href="#">Add a job now</a>
</p>
</div>
</div>
<h2 class="heading-large">Textbox</h2>
{{ textbox(form.username) }}
@@ -213,6 +243,17 @@
<h2 class="heading-large">File upload</h2>
{{ file_upload(form.file_upload) }}
<h2 class="heading-large">Yes/no</h2>
<div class="grid-row">
<div class='column-half'>
<fieldset class='yes-no-wrapper'>
{{ yes_no('manage_service', 'Manage service', True) }}
{{ yes_no('templates', 'Create templates', True) }}
</fieldset>
</div>
</div>
<h2 class="heading-large">API key</h2>
{{ api_key('d30512af92e1386d63b90e5973b49a10') }}

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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)

View File

@@ -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)