mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-13 18:38:33 -04:00
Merge branch 'master' of github.com:alphagov/notifications-admin into deactivate-services-plat-admin
This commit is contained in:
1
Makefile
1
Makefile
@@ -67,6 +67,7 @@ generate-version-file: ## Generates the app version file
|
||||
|
||||
.PHONY: build
|
||||
build: dependencies generate-version-file ## Build project
|
||||
./venv/bin/pip-accel wheel --wheel-dir=wheelhouse -r requirements.txt
|
||||
npm run build
|
||||
|
||||
.PHONY: build-codedeploy-artifact
|
||||
|
||||
@@ -117,6 +117,7 @@ def create_app():
|
||||
application.add_template_filter(format_date)
|
||||
application.add_template_filter(format_date_normal)
|
||||
application.add_template_filter(format_date_short)
|
||||
application.add_template_filter(format_datetime_relative)
|
||||
application.add_template_filter(format_delta)
|
||||
application.add_template_filter(format_notification_status)
|
||||
application.add_template_filter(format_notification_status_as_time)
|
||||
@@ -232,6 +233,23 @@ def format_datetime_short(date):
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
return '{} at {}'.format(
|
||||
get_human_day(date),
|
||||
format_time(date)
|
||||
)
|
||||
|
||||
|
||||
def get_human_day(time):
|
||||
# Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
time = (gmt_timezones(time) - timedelta(hours=1)).strftime('%A')
|
||||
if time == datetime.utcnow().strftime('%A'):
|
||||
return 'today'
|
||||
if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
|
||||
return 'tomorrow'
|
||||
return time
|
||||
|
||||
|
||||
def format_time(date):
|
||||
return {
|
||||
'12:00AM': 'Midnight',
|
||||
|
||||
@@ -2,88 +2,144 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
var render = ($options, $button) => (
|
||||
filterOptionVisibility($options) && setButtonState($options, $button)
|
||||
);
|
||||
let states = {
|
||||
'initial': Hogan.compile(`
|
||||
<div class="radio-select-column">
|
||||
<label class="block-label js-block-label" for="{{name}}-0">
|
||||
<input checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value=""> Now
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-select-column">
|
||||
{{#categories}}
|
||||
<input type='button' class='button tertiary-button js-category-button' value='{{.}}' />
|
||||
{{/categories}}
|
||||
</div>
|
||||
`),
|
||||
'choose': Hogan.compile(`
|
||||
<div class="radio-select-column">
|
||||
<label class="block-label js-block-label" for="{{name}}-0">
|
||||
<input checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="" class="js-initial-option"> Now
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-select-column">
|
||||
{{#choices}}
|
||||
<label class="block-label js-block-label" for="{{id}}">
|
||||
<input type="radio" value="{{value}}" id="{{id}}" name="{{name}}" class="js-option" />
|
||||
{{label}}
|
||||
</label>
|
||||
{{/choices}}
|
||||
</div>
|
||||
`),
|
||||
'chosen': Hogan.compile(`
|
||||
<div class="radio-select-column">
|
||||
<label class="block-label js-block-label" for="{{name}}-0">
|
||||
<input id="{{name}}-0" name="{{name}}" type="radio" value="" class="js-initial-option"> Now
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-select-column">
|
||||
{{#choices}}
|
||||
<label class="block-label js-block-label" for="{{id}}">
|
||||
<input checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
|
||||
{{label}}
|
||||
</label>
|
||||
{{/choices}}
|
||||
</div>
|
||||
<div class="radio-select-column">
|
||||
<input type='button' class='button tertiary-button js-reset-button' value='Choose a different time' />
|
||||
</div>
|
||||
`)
|
||||
};
|
||||
|
||||
var filterOptionVisibility = $options => $options
|
||||
.removeClass('js-visible')
|
||||
.filter(
|
||||
(index, element) => (index === 0 || $(element).has(':checked').length)
|
||||
)
|
||||
.addClass('js-visible');
|
||||
|
||||
var setButtonState = ($options, $button) => $button
|
||||
.addClass('js-visible')
|
||||
.prop(
|
||||
'value',
|
||||
$options.has(':checked').find('input').attr('id') === $options.eq(0).find('input').attr('id') ?
|
||||
'Later' : 'Choose a different time'
|
||||
let focusSelected = function() {
|
||||
setTimeout(
|
||||
() => $('[type=radio]:checked').parent('label').blur().trigger('focus').addClass('selected'),
|
||||
10
|
||||
);
|
||||
|
||||
// Workaround because GOV.UK SelectionButtons doesn’t deselect in this case
|
||||
var deselectUnchecked = $options => $options
|
||||
.filter(
|
||||
(index, element) => $(element).not(':has(:checked)')
|
||||
).removeClass('selected');
|
||||
|
||||
var refocus = $element => setTimeout(
|
||||
() => $element.blur().trigger('focus'),
|
||||
10
|
||||
);
|
||||
|
||||
var renderIfComponentLosesFocus = ($options, $button, $focused) => () =>
|
||||
($focused.attr('type') !== 'radio') &&
|
||||
render($options, $button) &&
|
||||
refocus($focused); // Make sure that window scrolls to focused element
|
||||
};
|
||||
|
||||
Modules.RadioSelect = function() {
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
let $component = $(component);
|
||||
let $options = $('label', $component);
|
||||
let render = (state, data) => $component.html(states[state].render(data));
|
||||
let choices = $('label', $component).toArray().map(function(element) {
|
||||
let $element = $(element);
|
||||
return {
|
||||
'id': $element.attr('for'),
|
||||
'label': $.trim($element.text()),
|
||||
'value': $element.find('input').attr('value')
|
||||
};
|
||||
});
|
||||
let categories = $component.data('categories').split(',');
|
||||
let name = $component.find('input').eq(0).attr('name');
|
||||
|
||||
$component.append(
|
||||
$button = $('<input type="button" value="Later" class="tertiary-button" />')
|
||||
);
|
||||
$component
|
||||
.on('click', '.js-category-button', function(event) {
|
||||
|
||||
$button.on('click', () =>
|
||||
$options.addClass('js-visible').has(':checked').focus() &&
|
||||
$button.removeClass('js-visible')
|
||||
);
|
||||
event.preventDefault();
|
||||
let wordsInDay = $(this).attr('value').split(' ');
|
||||
let day = wordsInDay[wordsInDay.length - 1].toLowerCase();
|
||||
render('choose', {
|
||||
'choices': choices.filter(
|
||||
element => element.label.toLowerCase().indexOf(day) > -1
|
||||
),
|
||||
'name': name
|
||||
});
|
||||
$('.js-option').eq(0).parent('label').trigger('focus');
|
||||
|
||||
$component.on('keydown', 'input[type=radio]', function() {
|
||||
})
|
||||
.on('click', '.js-option', function(event) {
|
||||
|
||||
// intercept keypresses which aren’t enter or space
|
||||
if (event.which !== 13 && event.which !== 32) {
|
||||
setTimeout(
|
||||
renderIfComponentLosesFocus($options, $button, $(document.activeElement)),
|
||||
200
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// stop click being triggered by keyboard events
|
||||
if (!event.pageX) return true;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
let value = $(this).attr('value');
|
||||
render('chosen', {
|
||||
'choices': choices.filter(
|
||||
element => element.value == value
|
||||
),
|
||||
'name': name
|
||||
});
|
||||
focusSelected();
|
||||
|
||||
render($options, $button);
|
||||
refocus($(this));
|
||||
})
|
||||
.on('keydown', 'input[type=radio]', function(event) {
|
||||
|
||||
// intercept keypresses which aren’t enter or space
|
||||
if (event.which !== 13 && event.which !== 32) {
|
||||
return true;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
let value = $(this).attr('value');
|
||||
render('chosen', {
|
||||
'choices': choices.filter(
|
||||
element => element.value == value
|
||||
),
|
||||
'name': name
|
||||
});
|
||||
focusSelected();
|
||||
|
||||
})
|
||||
.on('click', '.js-reset-button', function(event) {
|
||||
|
||||
event.preventDefault();
|
||||
render('initial', {
|
||||
'categories': categories,
|
||||
'name': name
|
||||
});
|
||||
focusSelected();
|
||||
|
||||
});
|
||||
|
||||
render('initial', {
|
||||
'categories': categories,
|
||||
'name': name
|
||||
});
|
||||
|
||||
$component.on('click', 'input[type=radio]', function(event) {
|
||||
|
||||
deselectUnchecked($options);
|
||||
|
||||
// stop click being triggered by keyboard events
|
||||
if (!event.pageX) return true;
|
||||
|
||||
render($options, $button);
|
||||
refocus($(this));
|
||||
|
||||
});
|
||||
|
||||
render($options, $button);
|
||||
$component.css({'height': 'auto'});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
margin-bottom: $gutter-half;
|
||||
}
|
||||
|
||||
.bottom-gutter-3-2 {
|
||||
margin-bottom: $gutter * 3/2;
|
||||
}
|
||||
|
||||
.bottom-gutter-2 {
|
||||
margin-bottom: $gutter * 2;
|
||||
}
|
||||
|
||||
@@ -214,3 +214,12 @@ summary::-webkit-details-marker {
|
||||
details .arrow {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.block-label-hint {
|
||||
@include core-16;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.block-label input[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
vertical-align: top;
|
||||
|
||||
.block-label {
|
||||
margin-right: 10px;
|
||||
margin-right: 5px;
|
||||
padding-right: $gutter - 10px;
|
||||
padding-left: 54px - 10px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,31 +17,21 @@
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: auto;
|
||||
padding: 20px 30px 15px 30px;
|
||||
padding: 20px $gutter-half 15px $gutter-half;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.js-enabled & {
|
||||
|
||||
height: 60px;
|
||||
overflow: visible;
|
||||
|
||||
.block-label {
|
||||
&:last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-label,
|
||||
.tertiary-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.js-visible {
|
||||
|
||||
display: block;
|
||||
|
||||
&.tertiary-button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.js-block-label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -118,6 +118,10 @@
|
||||
|
||||
}
|
||||
|
||||
&-noborder {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
&-index {
|
||||
width: 15px;
|
||||
}
|
||||
@@ -159,6 +163,12 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.table-row-group {
|
||||
border-top: 1px solid $border-colour;
|
||||
border-bottom: 1px solid $border-colour;
|
||||
}
|
||||
|
||||
.table-empty-message {
|
||||
@include core-16;
|
||||
color: $secondary-text-colour;
|
||||
|
||||
@@ -27,27 +27,58 @@ from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoC
|
||||
def get_time_value_and_label(future_time):
|
||||
return (
|
||||
future_time.replace(tzinfo=None).isoformat(),
|
||||
get_human_time(future_time.astimezone(pytz.timezone('Europe/London')))
|
||||
'{} at {}'.format(
|
||||
get_human_day(future_time.astimezone(pytz.timezone('Europe/London'))),
|
||||
get_human_time(future_time.astimezone(pytz.timezone('Europe/London')))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_human_time(time):
|
||||
return {
|
||||
'0': 'Midnight',
|
||||
'12': 'Midday'
|
||||
'0': 'midnight',
|
||||
'12': 'midday'
|
||||
}.get(
|
||||
time.strftime('%-H'),
|
||||
time.strftime('%-I%p').lower()
|
||||
)
|
||||
|
||||
|
||||
def get_next_hours_from(now, hours=23):
|
||||
def get_human_day(time, prefix_today_with='T'):
|
||||
# Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
time = (time - timedelta(hours=1)).strftime('%A')
|
||||
if time == datetime.utcnow().strftime('%A'):
|
||||
return '{}oday'.format(prefix_today_with)
|
||||
if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
|
||||
return 'Tomorrow'
|
||||
return time
|
||||
|
||||
|
||||
def get_furthest_possible_scheduled_time():
|
||||
return (datetime.utcnow() + timedelta(days=4)).replace(hour=0)
|
||||
|
||||
|
||||
def get_next_hours_until(until):
|
||||
now = datetime.utcnow()
|
||||
hours = int((until - now).total_seconds() / (60 * 60))
|
||||
return [
|
||||
(now + timedelta(hours=i)).replace(minute=0, second=0).replace(tzinfo=pytz.utc)
|
||||
for i in range(1, hours + 1)
|
||||
]
|
||||
|
||||
|
||||
def get_next_days_until(until):
|
||||
now = datetime.utcnow()
|
||||
days = int((until - now).total_seconds() / (60 * 60 * 24))
|
||||
return [
|
||||
get_human_day(
|
||||
(now + timedelta(days=i)).replace(tzinfo=pytz.utc),
|
||||
prefix_today_with='Later t'
|
||||
)
|
||||
for i in range(0, days + 1)
|
||||
]
|
||||
|
||||
|
||||
def email_address(label='Email address', gov_user=True):
|
||||
validators = [
|
||||
Length(min=5, max=255),
|
||||
@@ -310,8 +341,11 @@ class ChooseTimeForm(Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChooseTimeForm, self).__init__(*args, **kwargs)
|
||||
self.scheduled_for.choices = [('', 'Now')] + [
|
||||
get_time_value_and_label(hour) for hour in get_next_hours_from(datetime.utcnow())
|
||||
get_time_value_and_label(hour) for hour in get_next_hours_until(
|
||||
get_furthest_possible_scheduled_time()
|
||||
)
|
||||
]
|
||||
self.scheduled_for.categories = get_next_days_until(get_furthest_possible_scheduled_time())
|
||||
|
||||
scheduled_for = RadioField(
|
||||
'When should Notify send these messages?',
|
||||
@@ -350,26 +384,47 @@ class Feedback(Form):
|
||||
|
||||
|
||||
class RequestToGoLiveForm(Form):
|
||||
channel = StringField(
|
||||
'Are you sending emails or text messages or both?',
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
mou = RadioField(
|
||||
(
|
||||
'Has your organisation accepted the GOV.UK Notify data sharing and financial '
|
||||
'agreement (Memorandum of Understanding)?'
|
||||
),
|
||||
choices=[
|
||||
('yes', 'Yes'),
|
||||
('no', 'No'),
|
||||
('don’t know', 'I don’t know')
|
||||
],
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
channel = RadioField(
|
||||
'What kind of messages will you be sending?',
|
||||
choices=[
|
||||
('emails', 'Emails'),
|
||||
('text messages', 'Text messages'),
|
||||
('emails and text messages', 'Both')
|
||||
],
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
start_date = StringField(
|
||||
'When will you be ready to start sending messages?',
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
)
|
||||
start_volume = StringField(
|
||||
'How many messages do you expect to send per month to start with? Give an estimate in numbers.',
|
||||
'How many messages do you expect to send to start with?',
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
)
|
||||
peak_volume = StringField(
|
||||
'Will the number of messages a month increase and when will that start? Give an estimate.',
|
||||
'Will the number of messages increase and when will that start?',
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
)
|
||||
upload_or_api = StringField(
|
||||
'Are you uploading a list of contacts that you’re sending your message to, ' +
|
||||
'or are you integrating your system with ours?',
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
upload_or_api = RadioField(
|
||||
'How are you going to send messages?',
|
||||
choices=[
|
||||
('File upload', 'Upload a spreadsheet of recipients'),
|
||||
('API', 'Integrate with the GOV.UK Notify API'),
|
||||
('API and file upload', 'Both')
|
||||
],
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import request, render_template, redirect, url_for, flash
|
||||
from flask import request, render_template, redirect, url_for, flash, Markup, abort
|
||||
from flask_login import login_required
|
||||
from app.main import main
|
||||
from app.main.forms import CreateKeyForm, Whitelist
|
||||
@@ -68,13 +68,22 @@ def create_api_key(service_id):
|
||||
key['name'] for key in api_key_api_client.get_api_keys(service_id=service_id)['apiKeys']
|
||||
]
|
||||
form = CreateKeyForm(key_names)
|
||||
form.key_type.choices = filter(None, [
|
||||
(KEY_TYPE_NORMAL, 'Send messages to anyone')
|
||||
if not current_service['restricted'] else None,
|
||||
(KEY_TYPE_TEST, 'Simulate sending messages to anyone'),
|
||||
(KEY_TYPE_TEAM, 'Only send messages to your team or whitelist')
|
||||
])
|
||||
form.key_type.choices = [
|
||||
(KEY_TYPE_NORMAL, 'Send messages to anyone'),
|
||||
(KEY_TYPE_TEAM, 'Send messages to anyone on my whitelist'),
|
||||
(KEY_TYPE_TEST, 'Pretend to send messages to anyone'),
|
||||
]
|
||||
if current_service['restricted']:
|
||||
disabled_options = [KEY_TYPE_NORMAL]
|
||||
option_hints = {KEY_TYPE_NORMAL: Markup(
|
||||
'This option is not available because your service is in '
|
||||
'<a href="{}">trial mode</a>'.format(url_for(".trial_mode"))
|
||||
)}
|
||||
else:
|
||||
disabled_options, option_hints = [], {}
|
||||
if form.validate_on_submit():
|
||||
if form.key_type.data in disabled_options:
|
||||
abort(400)
|
||||
secret = api_key_api_client.create_api_key(
|
||||
service_id=service_id,
|
||||
key_name=form.key_name.data,
|
||||
@@ -88,7 +97,9 @@ def create_api_key(service_id):
|
||||
)
|
||||
return render_template(
|
||||
'views/api/keys/create.html',
|
||||
form=form
|
||||
form=form,
|
||||
disabled_options=disabled_options,
|
||||
option_hints=option_hints
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from app.utils import user_has_permissions
|
||||
|
||||
@main.route("/services/<service_id>/letters")
|
||||
@login_required
|
||||
@user_has_permissions('manage_templates', 'send_letters', admin_override=True, any_=True)
|
||||
def letters(service_id):
|
||||
if not current_service['can_send_letters']:
|
||||
abort(403)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from flask import render_template
|
||||
from flask_login import login_required
|
||||
|
||||
@@ -63,13 +61,10 @@ def create_global_stats(services):
|
||||
|
||||
def format_stats_by_service(services):
|
||||
for service in services:
|
||||
stats = service['statistics'].values()
|
||||
yield {
|
||||
'id': service['id'],
|
||||
'name': service['name'],
|
||||
'sending': sum((stat['requested'] - stat['delivered'] - stat['failed']) for stat in stats),
|
||||
'delivered': sum(stat['delivered'] for stat in stats),
|
||||
'failed': sum(stat['failed'] for stat in stats),
|
||||
'stats': service['statistics'],
|
||||
'restricted': service['restricted'],
|
||||
'research_mode': service['research_mode'],
|
||||
'created_at': service['created_at'],
|
||||
|
||||
@@ -21,7 +21,7 @@ from notifications_utils.template import Template
|
||||
from notifications_utils.recipients import RecipientCSV, first_column_heading, validate_and_format_phone_number
|
||||
|
||||
from app.main import main
|
||||
from app.main.forms import CsvUploadForm, ChooseTimeForm
|
||||
from app.main.forms import CsvUploadForm, ChooseTimeForm, get_next_days_until, get_furthest_possible_scheduled_time
|
||||
from app.main.uploader import (
|
||||
s3upload,
|
||||
s3download
|
||||
|
||||
@@ -112,11 +112,13 @@ def service_request_to_go_live(service_id):
|
||||
'subject': 'Request to go live',
|
||||
'message': (
|
||||
'On behalf of {} ({})\n\nExpected usage\n---'
|
||||
'\nMOU in place: {}'
|
||||
'\nChannel: {}\nStart date: {}\nStart volume: {}'
|
||||
'\nPeak volume: {}\nUpload or API: {}'
|
||||
).format(
|
||||
current_service['name'],
|
||||
url_for('main.service_dashboard', service_id=current_service['id'], _external=True),
|
||||
form.mou.data,
|
||||
form.channel.data,
|
||||
form.start_date.data,
|
||||
form.start_volume.data,
|
||||
|
||||
@@ -42,7 +42,8 @@ def view_template(service_id, template_id):
|
||||
'views/templates/template.html',
|
||||
template=Template(
|
||||
service_api_client.get_service_template(service_id, template_id)['data'],
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
)
|
||||
|
||||
@@ -63,7 +64,8 @@ def view_template_version(service_id, template_id, version):
|
||||
'views/templates/template_history.html',
|
||||
template=Template(
|
||||
service_api_client.get_service_template(service_id, template_id, version)['data'],
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
)
|
||||
|
||||
@@ -231,7 +233,8 @@ def view_template_versions(service_id, template_id):
|
||||
versions=[
|
||||
Template(
|
||||
template,
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
) for template in service_api_client.get_service_template_versions(service_id, template_id)['data']
|
||||
]
|
||||
)
|
||||
|
||||
@@ -47,8 +47,8 @@ def user_profile_name():
|
||||
form = ChangeNameForm(new_name=current_user.name)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_user.name = form.new_name.data
|
||||
user_api_client.update_user(current_user)
|
||||
user_api_client.update_user_attribute(current_user.id,
|
||||
name=form.new_name.data)
|
||||
return redirect(url_for('.user_profile'))
|
||||
|
||||
return render_template(
|
||||
@@ -107,7 +107,6 @@ def user_profile_email_authenticate():
|
||||
@main.route("/user-profile/email/confirm/<token>", methods=['GET'])
|
||||
@login_required
|
||||
def user_profile_email_confirm(token):
|
||||
|
||||
token_data = check_token(token,
|
||||
current_app.config['SECRET_KEY'],
|
||||
current_app.config['DANGEROUS_SALT'],
|
||||
@@ -115,9 +114,8 @@ def user_profile_email_confirm(token):
|
||||
token_data = json.loads(token_data)
|
||||
user_id = token_data['user_id']
|
||||
new_email = token_data['email']
|
||||
user = user_api_client.get_user(user_id)
|
||||
user.email_address = new_email
|
||||
user_api_client.update_user(user)
|
||||
user_api_client.update_user_attribute(user_id,
|
||||
email_address=new_email)
|
||||
session.pop(NEW_EMAIL, None)
|
||||
|
||||
return redirect(url_for('.user_profile'))
|
||||
@@ -179,10 +177,11 @@ def user_profile_mobile_number_confirm():
|
||||
form = ConfirmMobileNumberForm(_check_code)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_user.mobile_number = session[NEW_MOBILE]
|
||||
mobile_number = session[NEW_MOBILE]
|
||||
del session[NEW_MOBILE]
|
||||
del session[NEW_MOBILE_PASSWORD_CONFIRMED]
|
||||
user_api_client.update_user(current_user)
|
||||
user_api_client.update_user_attribute(current_user.id,
|
||||
mobile_number=mobile_number)
|
||||
return redirect(url_for('.user_profile'))
|
||||
|
||||
return render_template(
|
||||
|
||||
@@ -3,6 +3,12 @@ from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.notify_client.models import User
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
'name',
|
||||
'email_address',
|
||||
'mobile_number'
|
||||
}
|
||||
|
||||
|
||||
class UserApiClient(BaseAPIClient):
|
||||
def __init__(self):
|
||||
@@ -53,6 +59,19 @@ class UserApiClient(BaseAPIClient):
|
||||
user_data = self.put(url, data=data)
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
|
||||
def update_user_attribute(self, user_id, **kwargs):
|
||||
data = dict(kwargs)
|
||||
disallowed_attributes = set(data.keys()) - ALLOWED_ATTRIBUTES
|
||||
if disallowed_attributes:
|
||||
raise TypeError('Not allowed to update user attributes: {}'.format(
|
||||
", ".join(disallowed_attributes)
|
||||
))
|
||||
|
||||
data = dict(**kwargs)
|
||||
url = "/user/{}".format(user_id)
|
||||
user_data = self.post(url, data=data)
|
||||
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
|
||||
|
||||
def verify_password(self, user_id, password):
|
||||
try:
|
||||
url = "/user/{}/verify/password".format(user_id)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{% macro radios(
|
||||
field,
|
||||
hint=None
|
||||
hint=None,
|
||||
disable=[],
|
||||
option_hints={}
|
||||
) %}
|
||||
<div class="form-group {% if field.errors %} error{% endif %}">
|
||||
<fieldset>
|
||||
@@ -14,8 +16,21 @@
|
||||
</legend>
|
||||
{% for option in field %}
|
||||
<label class="block-label" for="{{ option.id }}">
|
||||
{{ option }}
|
||||
<input
|
||||
id="{{ option.id }}" name="{{ option.name }}" type="radio" value="{{ option.data }}"
|
||||
{% if option.data in disable %}
|
||||
disabled
|
||||
{% endif %}
|
||||
{% if option.checked %}
|
||||
checked
|
||||
{% endif %}
|
||||
>
|
||||
{{ option.label.text }}
|
||||
{% if option_hints[option.data] %}
|
||||
<div class="block-label-hint">
|
||||
{{ option_hints[option.data] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
@@ -38,7 +53,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</legend>
|
||||
<div class="radio-select" data-module="radio-select">
|
||||
<div class="radio-select" data-module="radio-select" data-categories="{{ field.categories|join(',') }}">
|
||||
<div class="radio-select-column">
|
||||
{% for option in field %}
|
||||
<label class="block-label" for="{{ option.id }}">
|
||||
|
||||
@@ -49,10 +49,20 @@
|
||||
</tr>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro field(align='left', status='') -%}
|
||||
<td class="table-field{% if align == 'right' %}-right-aligned{% endif %}">
|
||||
<span class="{{ 'table-field-status-' + status if status }}">{{ caller() }}</span>
|
||||
</td>
|
||||
{% macro row_group(id=None) %}
|
||||
<tbody class="table-row-group" {% if id %}id="{{id}}"{% endif %}>
|
||||
{{ caller() }}
|
||||
</tbody>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro field(align='left', status='', border=True) -%}
|
||||
|
||||
{% set field_alignment = 'table-field-right-aligned' if align == 'right' else 'table-field-center-aligned' %}
|
||||
{% set border = '' if border else 'table-field-noborder' %}
|
||||
|
||||
<td class="{{ [field_alignment, border]|join(' ') }}">
|
||||
<span class="{{ 'table-field-status-' + status if status }}">{{ caller() }}</span>
|
||||
</td>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro row_heading() -%}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% if job.job_status == 'scheduled' %}
|
||||
|
||||
<p>
|
||||
Sending will start at {{ job.scheduled_for|format_time }}
|
||||
Sending will start {{ job.scheduled_for|format_datetime_relative }}
|
||||
</p>
|
||||
<div class="page-footer">
|
||||
<form method="post">
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<div class="ajax-block-container">
|
||||
<p class='heading-small bottom-gutter'>
|
||||
Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
|
||||
{% if job.scheduled_for %}
|
||||
{% if job.processing_started %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short }}
|
||||
{% else %}
|
||||
Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<h2 class="heading-medium">
|
||||
Optional content
|
||||
</h2>
|
||||
<p>
|
||||
Use double brackets and ‘??’ to define optional content.
|
||||
</p>
|
||||
<p>
|
||||
For example if you only want to show something to people who are under
|
||||
18:
|
||||
</p>
|
||||
<div class="panel panel-border-wide">
|
||||
<p>
|
||||
((under18??Please get your application signed by a parent or
|
||||
guardian.))
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
For each person you send this message to, specify ‘yes’ or ‘no’ to
|
||||
show or hide this content.
|
||||
</p>
|
||||
@@ -13,15 +13,6 @@
|
||||
API integration
|
||||
</h1>
|
||||
|
||||
{% if current_service.restricted %}
|
||||
{% call banner_wrapper(type='warning') %}
|
||||
<h2 class="heading-medium">Your service is in trial mode</h2>
|
||||
<p>
|
||||
You can only send messages to people in your team or whitelist.
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
<nav class="grid-row bottom-gutter-1-2">
|
||||
<div class="column-one-third">
|
||||
<a class="pill-separate-item" href="{{ url_for('.api_keys', service_id=current_service.id) }}">API keys</a>
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
{% if item.key_type == 'normal' %}
|
||||
<span class="visually-hidden">Normal</span>
|
||||
{% elif item.key_type == 'team' %}
|
||||
Only sends to team members or whitelist
|
||||
Sends to anyone on your whitelist
|
||||
{% elif item.key_type == 'test' %}
|
||||
Simulates sending messages
|
||||
Pretends to send messages
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/radios.html" import radios %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
|
||||
{% block page_title %}
|
||||
Add a new API key – GOV.UK Notify
|
||||
Create an API key – GOV.UK Notify
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -14,8 +15,8 @@
|
||||
</h1>
|
||||
|
||||
<form method="post">
|
||||
{{ radios(form.key_type) }}
|
||||
{{ textbox(form.key_name, label='Name for this key') }}
|
||||
{{ radios(form.key_type, disable=disabled_options, option_hints=option_hints) }}
|
||||
{{ page_footer('Continue') }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
Whitelist
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
You and members of
|
||||
<a href="{{ url_for('main.manage_users', service_id=current_service.id) }}">your team</a>
|
||||
are included in the whitelist automatically.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<div class="grid-row">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% if scheduled_jobs %}
|
||||
<div class='dashboard-table'>
|
||||
<h2 class="heading-medium">
|
||||
In the next 24 hours
|
||||
In the next few days
|
||||
</h2>
|
||||
{% call(item, row_number) list_table(
|
||||
scheduled_jobs,
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="file-list">
|
||||
<a class="file-list-filename" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||
<span class="file-list-hint">
|
||||
Sending at {{ item.scheduled_for|format_time }}
|
||||
Sending {{ item.scheduled_for|format_datetime_relative }}
|
||||
</span>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<aside class="column-whole">
|
||||
{% include "partials/templates/guidance-formatting.html" %}
|
||||
{% include "partials/templates/guidance-personalisation.html" %}
|
||||
{% include "partials/templates/guidance-optional-content.html" %}
|
||||
{% include "partials/templates/guidance-links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
</div>
|
||||
<aside class="column-whole">
|
||||
{% include "partials/templates/guidance-personalisation.html" %}
|
||||
{% include "partials/templates/guidance-optional-content.html" %}
|
||||
{% include "partials/templates/guidance-links.html" %}
|
||||
{% include "partials/templates/guidance-character-count.html" %}
|
||||
</aside>
|
||||
|
||||
@@ -14,39 +14,32 @@ Information security guidelines – GOV.UK Notify
|
||||
Information security for text messages, emails and letters
|
||||
</h1>
|
||||
|
||||
{% call banner_wrapper(type='warning') %}
|
||||
<h2 class="heading-medium">This content is a work in progress</h2>
|
||||
|
||||
<p>It should not be relied upon</p>
|
||||
{% endcall %}
|
||||
|
||||
<p>A more pragmatic approach to information security</p>
|
||||
|
||||
<p class="lede">In the past, government has taken a risk-averse approach to information security. This resulted in services that were unhelpful and hard to use.</p>
|
||||
<p class="lede">Use a practical approach to information security, one that balances a user’s need to be kept informed with being kept safe.</p>
|
||||
|
||||
<p class="lede">We’re switching to a more pragmatic approach to information security – one that balances a user’s needs to be kept informed and kept safe.</p>
|
||||
|
||||
<p>In the past, for example, our blanket no-links policy meant we were telling people to “search ‘the UK government’ and click the first result” rather than just telling them to “visit <a href="https://www.gov.uk">www.gov.uk</a>”. Other services had a blanket policy of not sending any information at all, resulting in obtuse messages like “You have a message in your online account. Sign in to see the message.” (no sign-in link included).</p>
|
||||
|
||||
<section id="contents">
|
||||
<h2 class="heading-medium">Contents</h2>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a href="#start-with-needs">Start with needs – user needs, not government needs</a></li>
|
||||
<li><a href="#start-with-needs">Start with user needs, not government needs</a></li>
|
||||
<li><a href="#understand-the-risks">Understand the risks</a></li>
|
||||
<li><a href="#information-security-guidelines">Information security guidelines</a></li>
|
||||
<li><a href="#information-security-guidelines">Information security principles</a></li>
|
||||
<li><a href="#examples">Examples</a></li>
|
||||
<li><a href="#you-can-do-more">You can do more if you want to</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="start-with-needs">
|
||||
<h2 class="heading-medium">Start with needs – user needs, not government needs</h2>
|
||||
<h2 class="heading-medium">Start with user needs, not government needs</h2>
|
||||
|
||||
<p>Start by writing the message you want to send. Don’t worry about the information security aspect just yet – write the message you want to convey as clearly and directly as possible.</p>
|
||||
|
||||
<p>We have <a href="">design patterns</a> and <a href="">content guidance</a> to help you write clearly and convey the right information at the right time.</p>
|
||||
<p>Use our <a href="https://designpatterns.hackpad.com/Notifications-5vuitmNqIjZ">design patterns</a> along with the <a href="https://www.gov.uk/topic/government-digital-guidance/content-publishing">GOV.UK style guide</a> to help you write clearly and convey the right information at the right time.</p>
|
||||
|
||||
<p>Once you have a message which meets user needs, look at it in relation to the risks below. Use this framework to decide if you need to change the message in order to keep the users safe.</p>
|
||||
<p>Once you have a message which meets user needs, look at it in relation to the risks we outline. Use this to decide if you need to change the message in order to keep the users safe.</p>
|
||||
</section>
|
||||
|
||||
<section id="understand-the-risks">
|
||||
@@ -54,19 +47,19 @@ Information security guidelines – GOV.UK Notify
|
||||
|
||||
<p>There are 3 main risks involved in sending notifications by text message, email or letter:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>Someone accidentally sees the notification</li>
|
||||
<li>An attacker intercepts a message, or gains access to someone’s email inbox, phone messages or paper files</li>
|
||||
<li>An attacker tricks the user by sending a fake notification (phishing)</li>
|
||||
</ul>
|
||||
<ol class="list list-number">
|
||||
<li>Someone accidentally sees the notification.</li>
|
||||
<li>An attacker intercepts a message, or gains access to someone’s email inbox, phone messages or paper files.</li>
|
||||
<li>An attacker tricks the user by sending a fake notification (phishing).</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="heading-small" id="risk-privacy">Someone accidentally sees the notification</h3>
|
||||
|
||||
<p>For some messages, the recipient would be unhappy if someone else accidentally saw the contents – for example, the results of a recent medical test.</p>
|
||||
<p>For some messages, the recipient would be unhappy if someone else accidentally saw the contents, for example, the results of a recent medical test.</p>
|
||||
|
||||
<p>This is a privacy issue – in this case the unintended recipient isn’t trying to steal money or identity information.</p>
|
||||
|
||||
<p>To address this risk, don’t reveal the important information in the subject line or opening sentence, or ask the user to sign in to see the information in full. More about this below.</p>
|
||||
<p>To address this risk, don’t reveal the important information in the subject line or opening sentence, or ask the user to sign in to see the information in full.</p>
|
||||
|
||||
<h3 class="heading-small" id="risk-fraud">An attacker intercepts a message, or gains access to someone’s email inbox, phone messages or paper files</h3>
|
||||
|
||||
@@ -74,7 +67,7 @@ Information security guidelines – GOV.UK Notify
|
||||
|
||||
<p>It’s also possible for a criminal to gain access to someone’s entire email inbox, phone messages or paper files. Email accounts can be hacked, phones and paper files can be stolen, left lying around or picked out of the rubbish.</p>
|
||||
|
||||
<p>In both cases, criminals are looking for information they can use to commit fraud. To address this risk, don’t send payment details, ID numbers or any other information that can be used for fraud. More about this below.</p>
|
||||
<p>In both cases, criminals are looking for information they can use to commit fraud. To address this risk, don’t send payment details, ID numbers or any other information that can be used for fraud.</p>
|
||||
|
||||
<h3 class="heading-small" id="risk-phishing">An attacker tricks the user by sending a fake notification (phishing)</h3>
|
||||
|
||||
@@ -82,26 +75,26 @@ Information security guidelines – GOV.UK Notify
|
||||
|
||||
<p>This is known as a ‘phishing attack’.</p>
|
||||
|
||||
<p>To address this risk, don’t send <strong>requests</strong> for personal information <strong>of any kind</strong>, unless the request is <strong>directly connected with a transaction</strong>. More about this below.</p>
|
||||
<p>To address this risk, don’t send requests for personal information of any kind, unless the request is directly connected with a transaction.</p>
|
||||
</section>
|
||||
|
||||
<section id="information-security-guidelines">
|
||||
<h2 class="heading-medium">Information security guidelines</h2>
|
||||
<h2 class="heading-medium">Information security principles</h2>
|
||||
|
||||
<h3 class="heading-small" id="guideline-privacy">Protect the user’s privacy</h3>
|
||||
|
||||
<p>If you think the recipient might be upset if someone accidentally saw the message contents, either:</p>
|
||||
<p>To avoid someone other than the recipient accidentally seeing a message that has sensitive or confidential information, either:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>use a fairly generic subject line and opening sentence, and only give the information in full within the body of the message, or</li>
|
||||
<li>send a fairly generic message which asks the person to sign in to see the information in full</li>
|
||||
<li>use a generic subject line and opening sentence, and only give the information in full within the body of the message</li>
|
||||
<li>send a generic message which asks the person to sign in to see the information in full</li>
|
||||
</ul>
|
||||
|
||||
<p>Remember that even the sender ID also reveals information. For example, don’t set your sender name as ‘STI clinic’.</p>
|
||||
|
||||
<h3 class="heading-small" id="guideline-fraud">Don’t send information that can be used for fraud</h3>
|
||||
|
||||
<p>To reduce the risk if messages are intercepted, hacked or stolen, don’t send information that can be used for fraud – either now or in the future:</p>
|
||||
<p>To reduce the risk if messages are intercepted, hacked or stolen, don’t send messages with:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>payment details</li>
|
||||
@@ -116,59 +109,82 @@ Information security guidelines – GOV.UK Notify
|
||||
|
||||
<h3 class="heading-small" id="guideline-phishing">Don’t send requests for personal information of any kind, unless the request is directly connected with a transaction</h3>
|
||||
|
||||
<p>To reduce the risk from phishing attacks, don’t send <strong>requests</strong> for personal information <strong>of any kind</strong>, unless the request is <strong>directly connected with a transaction</strong>.</p>
|
||||
<p>To reduce the risk from phishing attacks, don’t send requests for personal information of any kind, unless the request is directly connected with a transaction.</p>
|
||||
|
||||
<p>It’s OK to send a request for personal information if it’s directly connected with a transaction. Here are two examples of where it would be OK:</p>
|
||||
<p>It’s OK to send a request for personal information if it’s directly connected with a transaction. For example it's OK to send a notification with a link asking users to reset their password if they've requested it by clicking on a ‘Forgot your password?’ link.</p>
|
||||
|
||||
|
||||
<h3 class="heading-small" id="guideline-links">It’s OK to include links</h3>
|
||||
|
||||
<p>The same rules apply to links:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>Someone clicks a ‘Forgot your password?’ link – it’s OK to send them a link where they can reset their password</li>
|
||||
<li>Someone’s MOT is about to expire – y</li>
|
||||
<li>Don’t send links that reveal information that can be used for fraud.</li>
|
||||
<li>Don’t send unsolicited messages that include a link requesting personal information of any kind (it’s OK to send a message with a link requesting information if the user has just requested it).</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="heading-small" id="guideline-links">It’s OK to include links – but you need to follow these rules</h3>
|
||||
<p>There are additional rules that apply specifically to links.</p>
|
||||
|
||||
<p>The same 2 rules above apply to links, too:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>Don’t send links that reveal information that can be used for fraud</li>
|
||||
<li>Don’t send unsolicited messages that include a link requesting personal information of any kind (it’s OK to send a message with a link requesting information if the user has just requested it)</li>
|
||||
</ul>
|
||||
|
||||
<p>There are additional rules that apply specifically to links:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>Links must point to a .gov.uk domain – for example, <a href="https://www.gov.uk">https://www.gov.uk</a> or <a href="https://www.armslengthbody.gov.uk">https://www.armslengthbody.gov.uk</a></li>
|
||||
<li>Links must show the URL in full – for example <a href="https://www.gov.uk/vehicle-tax">https://www.gov.uk/vehicle-tax</a>, not <a href="https://www.gov.uk/vehicle-tax">Vehicle tax</a></li>
|
||||
<li>Don’t use redirects or tracking links – disguising the URL makes phishing easier. Just show the URL in full</li>
|
||||
<li>Don’t link directly to a sign-in page – this is a request for personal data. If the user needs to sign in to your service, link to your start page on GOV.UK</li>
|
||||
<li>It’s OK to deep-link into your service, as long as the user doesn’t have to sign in to view the information or take action</li>
|
||||
</ul>
|
||||
<ol class="list list-number">
|
||||
<li>Links must point to a .gov.uk domain – for example, https://www.gov.uk or https://www.armslengthbody.gov.uk.</li>
|
||||
<li>Links must show the URL in full – for example https://www.gov.uk/vehicle-tax, not gov.uk/vehicle-tax.</li>
|
||||
<li>Don’t use redirects or tracking links – disguising the URL makes phishing easier. Just show the URL in full.</li>
|
||||
<li>Don’t link directly to a sign-in page – this is a request for personal data. If the user needs to sign in to your service, link to your start page on GOV.UK.</li>
|
||||
<li>It’s OK to deep-link into your service, as long as the user doesn’t have to sign in to view the information or take action.</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="heading-small" id="guideline-attachments">Don’t send attachments</h3>
|
||||
|
||||
<p>If you want to communicate something, write it in the body of the email. This is more user-friendly. If the information is too sensitive to include in the email body, it’s too sensitive to include in an attachment.</p>
|
||||
|
||||
<p>If you need to send someone a file, make the file available within your service, then link to it. </p>
|
||||
|
||||
<p>Criminals often use attachments to conceal viruses, spyware and other kinds of malware. We want people to be cautious about opening attachments.</p>
|
||||
<p>If you need to send someone a file, make the file available within your service, then link to it.</p>
|
||||
|
||||
|
||||
<h3 class="heading-small" id="guideline-name">Include the user’s name – it makes phishing more difficult</h3>
|
||||
|
||||
<p>Start your message by addressing the user. For example, Hi Alice Smith or Dear Bob Jones. Including this extra piece of information makes phishing more difficult.</p>
|
||||
<p>Start your message by addressing the user. For example, ‘Hi Alice Smith’, or ‘Dear Bob Jones’. Including this extra piece of information makes phishing more difficult.</p>
|
||||
|
||||
<h3 class="heading-small" id="guideline-technical">Use technical approaches to improve privacy and prevent phishing</h3>
|
||||
|
||||
<p>There are several technical approaches to preventing phishing – <a href="https://www.gov.uk/guidance/common-technology-services-cts-secure-email-blueprint">SPF/DKIM, DMARC</a> and <a href="https://en.m.wikipedia.org/wiki/Transport_Layer_Security">TLS</a>. You must use them.</p>
|
||||
<p>There are several technical approaches to preventing phishing. You must use <a href="https://www.gov.uk/guidance/common-technology-services-cts-secure-email-blueprint">SPF/DKIM, DMARC</a> and <a href="https://en.m.wikipedia.org/wiki/Transport_Layer_Security">TLS</a>.</p>
|
||||
|
||||
<p>SPF/DKIM and DMARC make sure your emails get delivered, whilst phishing and spam email gets filtered into junk mail.</p>
|
||||
|
||||
<p>TLS makes sure that no-one can intercept your emails.</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="examples">
|
||||
<h2 class="heading-medium">Examples</h2>
|
||||
|
||||
<h3 class="heading-small">Example of an appointment reminder</h3>
|
||||
<p>“Dear Anne Smith, you’ve got a licence appointment tomorrow at 2:15pm at the Licence Office, 1 Chapel Hill, Heswall, Bournemouth BH1 1AA. To cancel your appointment, visit licensing.service.gov.uk/appointment/12345678/cancel. To change your appointment time, sign in to your account.”</p>
|
||||
<p>This is a good example because:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>the message and link doesn't reveal any sensitive personal data</li>
|
||||
<li>it doesn't ask for personal data, passwords or payment details</li>
|
||||
<li>the reminder addresses the user by their name, making phishing attacks more difficult</li>
|
||||
<li>the link just cancels the appointment which minimises what an attacker can do</li>
|
||||
<li>users have to sign in to change the appointment time, making it harder for an attacker to know what their appointment time is</li>
|
||||
<li>the topic is something the user is familiar with</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h3 class="heading-small">Example to add a photo to an environmental permit</h3>
|
||||
<p>“Dear Andrew Jones, to add a location photo to your environmental permit application, visit environmentalpermit.service.gov.uk/12345678/add-photo. If you didn’t request this link, please ignore this message.”</p>
|
||||
<p>This is a good example because:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>the message and link doesn't reveal any sensitive personal data</li>
|
||||
<li>it doesn't ask for personal data, passwords or payment details</li>
|
||||
<li>the reminder addresses the user by their name, making phishing attacks more difficult</li>
|
||||
<li>the link only lets users add a photo to an environmental permit application – it doesn’t complete the process, which minimises what an attacker can do</li>
|
||||
<li>it shows users what to do if the message doesn't apply to them</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="you-can-do-more">
|
||||
<h2 class="heading-medium">You can do more if you want to</h2>
|
||||
|
||||
<p>These guidelines are the minimum requirement. If you want to take more stringent measures for your service, that’s fine.</p>
|
||||
<p>These guidelines are the minimum requirement. You can take stricter measures for your service if you think it's necessary.</p>
|
||||
|
||||
<p>Just make sure you’re balancing your users’ needs to be kept informed and kept safe.</p>
|
||||
</section>
|
||||
|
||||
@@ -2,53 +2,78 @@
|
||||
{% from "components/big-number.html" import big_number, big_number_with_status %}
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
{% from "components/browse-list.html" import browse_list %}
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, hidden_field_heading, text_field %}
|
||||
{% from "components/table.html" import mapping_table, field, stats_fields, row_group, row, right_aligned_field_heading, hidden_field_heading, text_field %}
|
||||
|
||||
{% macro stats_fields(channel, data) -%}
|
||||
|
||||
{% call field(border=False) %}
|
||||
<span class="heading-medium">{{ channel.title() }}</span>
|
||||
{% endcall %}
|
||||
|
||||
{% call field(align='right', border=False) %}
|
||||
{{ big_number(data[channel]['requested'], smaller=True) }}
|
||||
{% endcall %}
|
||||
|
||||
{% call field(align='right', border=False) %}
|
||||
{{ big_number(data[channel]['delivered'], smaller=True) }}
|
||||
{% endcall %}
|
||||
|
||||
{% call field(align='right', status='error' if data[channel]['failed'], border=False) %}
|
||||
{{ big_number(data[channel]['failed'], smaller=True) }}
|
||||
{% endcall %}
|
||||
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro services_table(services, caption) %}
|
||||
{% call(item, row_number) list_table(
|
||||
services,
|
||||
{% call(item, row_number) mapping_table(
|
||||
caption=caption,
|
||||
caption_visible=True,
|
||||
field_headings=[
|
||||
'Service',
|
||||
hidden_field_heading('Status'),
|
||||
right_aligned_field_heading('Sending'),
|
||||
right_aligned_field_heading('Delivered'),
|
||||
right_aligned_field_heading('Failed')
|
||||
'Service',
|
||||
hidden_field_heading('Type'),
|
||||
right_aligned_field_heading('Sending'),
|
||||
right_aligned_field_heading('Delivered'),
|
||||
right_aligned_field_heading('Failed')
|
||||
],
|
||||
field_headings_visible=True
|
||||
) %}
|
||||
{% call field() %}
|
||||
<div>
|
||||
<a href="{{ url_for('main.service_dashboard', service_id=item['id']) }}" class="browse-list-link">{{ item['name'] }}</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% if not item['active'] %}
|
||||
{% call field(status='default') %}
|
||||
<span class="heading-medium">archived</span>
|
||||
|
||||
{% for service in services %}
|
||||
|
||||
{% call row_group() %}
|
||||
|
||||
{% call row() %}
|
||||
{% call field(border=False) %}
|
||||
<a href="{{ url_for('main.service_dashboard', service_id=service['id']) }}" class="browse-list-link">{{ service['name'] }}</a>
|
||||
{% endcall %}
|
||||
|
||||
{{ stats_fields('email', service['stats']) }}
|
||||
{% endcall %}
|
||||
|
||||
{% call row() %}
|
||||
{% if not service['active'] %}
|
||||
{% call field(status='default') %}
|
||||
<span class="heading-medium">archived</span>
|
||||
{% endcall %}
|
||||
{% elif service['research_mode'] %}
|
||||
{% call field(border=False) %}
|
||||
<span class="research-mode">research mode</span>
|
||||
{% endcall %}
|
||||
{% elif not service['restricted'] %}
|
||||
{% call field(status='error') %}
|
||||
<span class="heading-medium">Live</span>
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{{ text_field('') }}
|
||||
{% endif %}
|
||||
|
||||
{{ stats_fields('sms', service['stats']) }}
|
||||
{% endcall %}
|
||||
|
||||
{% endcall %}
|
||||
{% elif item['research_mode'] %}
|
||||
{% call field() %}
|
||||
<span class="research-mode">research mode</span>
|
||||
{% endcall %}
|
||||
{% elif not item['restricted'] %}
|
||||
{% call field(status='error') %}
|
||||
<span class="heading-medium">
|
||||
Live
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{{ text_field('') }}
|
||||
{% endif %}
|
||||
{% call field(align='right') %}
|
||||
{{ big_number(item['sending'], smaller=True) }}
|
||||
{% endcall %}
|
||||
{% call field(align='right') %}
|
||||
{{ big_number(item['delivered'], smaller=True) }}
|
||||
{% endcall %}
|
||||
{% call field(align='right', status='error' if 0 else '') %}
|
||||
{{ big_number(item['failed'], smaller=True) }}
|
||||
{% endcall %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, index_field_heading %}
|
||||
|
||||
{% block page_title %}
|
||||
Send text messages – GOV.UK Notify
|
||||
{% if request.args['help'] %}
|
||||
Example text message
|
||||
{% else %}
|
||||
Send yourself a test
|
||||
{% endif %} – GOV.UK Notify
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -11,24 +11,7 @@
|
||||
|
||||
<h1 class="heading-large">Settings</h1>
|
||||
|
||||
{% if current_service.restricted %}
|
||||
{% call banner_wrapper(type='warning') %}
|
||||
<h2 class="heading-medium">Your service is in trial mode</h2>
|
||||
|
||||
<ul class='list list-bullet'>
|
||||
<li>you can only send messages to yourself</li>
|
||||
<li>you can add people to your team, then you can send messages to them too</li>
|
||||
<li>you can only send 50 messages per day</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
To remove these restrictions
|
||||
<a href="{{ url_for('.service_request_to_go_live', service_id=current_service.id) }}">request to go live</a>.
|
||||
</p>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-gutter-2">
|
||||
<div class="bottom-gutter-3-2">
|
||||
|
||||
{% call mapping_table(
|
||||
caption='Settings',
|
||||
@@ -57,6 +40,34 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% if current_service.restricted %}
|
||||
<h2 class="heading-medium">Your service is in trial mode</h2>
|
||||
|
||||
<ul class='list list-bullet'>
|
||||
<li>you can only send messages to yourself</li>
|
||||
<li>you can add people to your team, then you can send messages to them too</li>
|
||||
<li>you can only send 50 messages per day</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
To remove these restrictions
|
||||
<a href="{{ url_for('.service_request_to_go_live', service_id=current_service.id) }}">request to go live</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
<h2 class="heading-medium">Your service is live</h2>
|
||||
|
||||
<p>
|
||||
You can send up to
|
||||
{{ "{:,}".format(current_service.message_limit) }} messages
|
||||
per day.
|
||||
</p>
|
||||
<p>
|
||||
Problems or comments?
|
||||
<a href="{{ url_for('main.feedback') }}">Give feedback</a>.
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has_permissions([], admin_override=True) %}
|
||||
|
||||
<h2 class="heading-medium">Platform admin settings</h2>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/radios.html" import radios %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
|
||||
@@ -11,19 +12,10 @@
|
||||
|
||||
<h1 class="heading-large">Request to go live</h1>
|
||||
|
||||
{% call banner_wrapper(type='warning') %}
|
||||
<h2 class="heading-medium">You must accept the GOV.UK Notify data sharing and financial agreement (Memorandum of Understanding) before we can process data for you.</h2>
|
||||
|
||||
<p>
|
||||
<a href="{{ url_for('main.feedback') }}">Contact the Notify team</a> to get a copy of the agreement or to find out if your organisation has already accepted it.
|
||||
</p>
|
||||
{% endcall %}
|
||||
|
||||
<p>
|
||||
Before you request to go live, make sure you’ve:
|
||||
</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>accepted our data sharing and financial agreement</li>
|
||||
<ul class="list list-bullet bottom-gutter">
|
||||
<li>read our <a href="{{ url_for('.terms') }}">terms of use</a></li>
|
||||
<li>added <a href="{{ url_for('main.manage_users', service_id=current_service.id) }}">team members</a> to your account</li>
|
||||
<li>
|
||||
@@ -39,17 +31,26 @@
|
||||
</ul>
|
||||
|
||||
<form method="post">
|
||||
{{ textbox(form.channel, width='1-1') }}
|
||||
{{ textbox(form.start_date, width='1-1') }}
|
||||
{{ textbox(form.start_volume, width='1-1') }}
|
||||
{{ textbox(form.peak_volume, width='1-1') }}
|
||||
{{ textbox(form.upload_or_api, width='1-1') }}
|
||||
<div class="form-group">
|
||||
<p>We need permission to process your data before we can make your service live.</p>
|
||||
{{ radios(form.mou, option_hints={
|
||||
'no': 'We’ll send you a copy',
|
||||
'don’t know': 'We’ll check for you',
|
||||
}) }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ radios(form.channel) }}
|
||||
{{ textbox(form.start_date, width='1-1') }}
|
||||
{{ textbox(form.start_volume, width='1-1', hint='For example, ‘1000 a month’.') }}
|
||||
{{ textbox(form.peak_volume, width='1-1', hint='For example, ‘Messages will increase to 20,000 a month in January’.') }}
|
||||
{{ radios(form.upload_or_api) }}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Once you’ve completed the tasks needed to set up, we’ll make your service live. We’ll do this within one working day.
|
||||
</p>
|
||||
<p>
|
||||
By requesting to go live you are agreeing to our terms of use.
|
||||
By requesting to go live you’re agreeing to our <a href="{{ url_for('.terms') }}">terms of use</a>.
|
||||
</p>
|
||||
|
||||
{{ page_footer('Request to go live') }}
|
||||
|
||||
@@ -23,7 +23,7 @@ Terms of use – GOV.UK Notify
|
||||
|
||||
{% endcall %}
|
||||
|
||||
<p>To accept these terms, you must be the service manager for your service. If you’re not the service manager, you’ll need to invite them.</p>
|
||||
<p>To accept these terms, you must be the service manager for your service.</p>
|
||||
|
||||
<section id="summary">
|
||||
<h2 class="heading-medium">
|
||||
@@ -34,25 +34,20 @@ Terms of use – GOV.UK Notify
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a href="#we-agree-to-send-all-the-messages">send all the messages you pass to us</a></li>
|
||||
<li><a href="#we-agree-to-keep-you-informed">keep you informed about the performance of GOV.UK Notify</a></li>
|
||||
<li><a href="#we-agree-to-keep-your-data-secure">keep your data secure</a></li>
|
||||
<li><a href="#we-agree-to-give-you-three-months-notice-if-we-change-these-terms">give you three months’ notice if we change these terms</a></li>
|
||||
<li><a href="#we-agree-to-give-you-one-months-notice">give you one months’ notice if we change these terms</a></li>
|
||||
</ul>
|
||||
|
||||
<p>You agree:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a href="#you-agree-not-to-compromise-our-security">not to compromise the security of GOV.UK Notify</a></li>
|
||||
<li><a href="#you-agree-not-to-send-marketing">not to use GOV.UK Notify to send marketing messages</a></li>
|
||||
<li><a href="#you-agree-not-to-send-unsolicited">not to use GOV.UK Notify to send unsolicited messages</a></li>
|
||||
<li><a href="#you-agree-to-send-messages-consistent-with-our-guidelines">to send messages consistent with our design patterns, style guide and information security principles</a></li>
|
||||
<li><a href="#you-agree-to-use-delivery-data-to-improve">to use GOV.UK delivery data to continuously improve the quality of your contact data</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Before you can send real messages:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a href="#you-must-tell-us-how-many-text-messages-emails-and-letters-you-plan-to-send">you must tell us approximately how many text messages, emails and letters you plan to send</a></li>
|
||||
<li><a href="#we-will-check-your-templates-before-you-can-go-live">we will check the messages you plan to send to make sure they meet our guidelines</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="our-side">
|
||||
@@ -64,37 +59,37 @@ Terms of use – GOV.UK Notify
|
||||
We agree to send all the messages you pass to us
|
||||
</h3>
|
||||
|
||||
<p>We will send all the messages you pass to us, as long as they meet our guidelines.</p>
|
||||
<p>We’ll send all the messages you pass to us, as long as they meet our guidelines.</p>
|
||||
|
||||
<p>We endeavour to provide continuous uptime for both accepting messages and sending them.</p>
|
||||
<p>We aim to provide a continuous service so you can use GOV.UK Notify 24 hours a day, 365 days a year.</p>
|
||||
|
||||
<p>We’ve made sure that GOV.UK Notify can handle large volumes of messages. For email and text messages we have several delivery providers concurrently integrated. This provides GOV.UK Notify with real-time failover capability.</p>
|
||||
<p>We’ve made sure that GOV.UK Notify can handle large volumes of messages. For text messages we use multiple delivery providers at any one time. If a provider’s service fails, GOV.UK Notify will automatically switch to a different provider.</p>
|
||||
|
||||
<p>GOV.UK Notify is supported 24/7 for high-priority issues. We provide a ticketing system and escalation routes for service teams to address incidents.</p>
|
||||
<h3 class="heading-small" id="we-agree-to-keep-you-informed">
|
||||
We agree to keep you informed about the performance of GOV.UK Notify
|
||||
</h3>
|
||||
|
||||
<p>You’ll be able to see how our service is performing on our <a href="https://status.notifications.service.gov.uk">status page</a>.</p>
|
||||
<p>You’ll be able to see how the service is performing on our <a href="https://status.notifications.service.gov.uk">status page</a>.</p>
|
||||
|
||||
<p>We have a ticketing system and escalation routes to address incidents. We also provide 24 hour support for high-priority issues.</p>
|
||||
|
||||
<p>We also have a <a href="https://ukgovernmentdigital.slack.com/messages/govuk-notify">chat room</a> for talking to the GOV.UK Notify team. We are available to discuss your needs, and to see how Notify is working for you.</p>
|
||||
|
||||
<h3 class="heading-small" id="we-agree-to-keep-your-data-secure">
|
||||
We agree to keep your data secure
|
||||
</h3>
|
||||
|
||||
<p>GOV.UK Notify (as a whole, including subcontractors) currently store personal data for up to 1 year, and non-personal data indefinitely.</p>
|
||||
|
||||
<p>GOV.UK Notify has been through an information assurance process to assess information risks, to determine appropriate treatments for those risks and to obtain risk acceptance from the Cabinet Office Senior Information Risk Officer (SIRO). This work includes the completion of a Privacy Impact Assessment to ensure compliance with the Data Protection Act.</p>
|
||||
|
||||
<p>We do not conduct, or enable, analysis of when the same recipient (mobile number, email or postal address) is contacted by multiple Government organisations. We may do so if required by law enforcement.</p>
|
||||
|
||||
<p>We maintain appropriate technical and organisational measures to protect data. We make sure our subcontractors follow the same procedures.</p>
|
||||
<p>GOV.UK Notify has been through an information assurance process to assess information risks, to determine appropriate treatments for those risks and to obtain risk acceptance from the Cabinet Office Senior Information Risk Officer (SIRO). This work includes the completion of a privacy impact assessment to ensure compliance with the Data Protection Act.</p>
|
||||
|
||||
<p>Cabinet Office act as data processor, as parent organisation of GOV.UK Notify. Your organisation remains the data controller.</p>
|
||||
|
||||
<p>We’ll never transfer or store data on servers outside of the European Economic Area.</p>
|
||||
<p><a href="{{ url_for('main.feedback') }}">Contact us</a> if you want more information about our approach to data protection and information risk management.</p>
|
||||
|
||||
<h3 class="heading-small" id="we-agree-to-give-you-three-months-notice-if-we-change-these-terms">
|
||||
We agree to give you three months’ notice if we change these terms
|
||||
<h3 class="heading-small" id="we-agree-to-give-you-one-months-notice">
|
||||
We agree to give you one months’ notice if we change these terms
|
||||
</h3>
|
||||
|
||||
<p>We’ll email you if you need to change these terms. We’ll tell you clearly what is changing and when the change will come into effect.</p>
|
||||
<p>We’ll email to tell you what is changing and when the change will come into effect.</p>
|
||||
|
||||
<p>This includes when any of our email, text message or postal providers change.</p>
|
||||
|
||||
@@ -115,53 +110,51 @@ Terms of use – GOV.UK Notify
|
||||
|
||||
<p>You must follow industry best practices for keeping your API keys secure.</p>
|
||||
|
||||
<p>You must ensure you have obtained correct levels of consent - both to send messages but also for how data is shared in order to do so.</p>
|
||||
<p>You must ensure you have obtained correct levels of consent - both to send messages but also for how data is shared, stored, and processed in order to do so.</p>
|
||||
|
||||
<p>You must not perform any load testing on GOV.UK Notify, since we’ve already done it.</p>
|
||||
|
||||
<h3 class="heading-small" id="you-agree-not-to-send-marketing">
|
||||
You agree not to use GOV.UK Notify to send marketing messages
|
||||
<h3 class="heading-small" id="you-agree-not-to-send-unsolicited">
|
||||
You agree not to use GOV.UK Notify to send unsolicited messages
|
||||
</h3>
|
||||
|
||||
<p>GOV.UK Notify is for sending transactional messages.</p>
|
||||
<p>GOV.UK Notify is for sending transactional messages and subscription based alerts or reminders.</p>
|
||||
|
||||
<p>Transactional messages relate directly to something the user did. For example:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>The user completed a transaction, you send them a confirmation email</li>
|
||||
<li>The user got an MOT a year ago, you remind them that it’s about to expire</li>
|
||||
<li>The user signed up for email alerts, you send them email alerts</li>
|
||||
<li>they completed a transaction, and you’re sending them a confirmation email</li>
|
||||
<li>they paid for an annual service a year ago, and you're reminding them that it’s about to expire</li>
|
||||
<li>their application has been approved, and you're sending them a text message to let them know</li>
|
||||
</ul>
|
||||
|
||||
<p>You don’t need to ask permission to send messages that directly relate to a transaction. By using a transaction, a user is implicitly agreeing to receive messages about that transaction.</p>
|
||||
<p>You don’t need to ask permission to send messages that directly relate to a transaction. By making a transaction and providing their contact details, a user is implicitly agreeing to receive messages about that transaction.</p>
|
||||
|
||||
<p>Marketing messages don’t relate directly to something the user did. For example:</p>
|
||||
<p>Subscription based messages relate to something a user has explicitly asked to be updated with. For example:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>Telling users about your webinar</li>
|
||||
<li>Sending users government advice</li>
|
||||
<li>Continuing to update someone about a service they no longer use</li>
|
||||
<li>they subscribed to travel advice alerts</li>
|
||||
<li>they asked to be updated when guidance was updated</li>
|
||||
<li>they opted in for information about new procurement frameworks</li>
|
||||
</ul>
|
||||
|
||||
<p>You must agree not to use GOV.UK Notify to send marketing messages.</p>
|
||||
<p>All subscription based messages must, by law, contain a way for users to unsubscribe.</p>
|
||||
|
||||
<p>If you do use GOV.UK Notify to send marketing messages, we may refuse to accept further messages for delivery.</p>
|
||||
<p>If you do use GOV.UK Notify to send unsolicited messages, we may refuse to accept further messages for delivery.</p>
|
||||
|
||||
<h3 class="heading-small" id="you-agree-to-send-messages-consistent-with-our-guidelines">
|
||||
You agree to send messages consistent with our design patterns, style guide and information security guidelines
|
||||
</h3>
|
||||
|
||||
<p>Your messages must follow our <a href="https://designpatterns.hackpad.com/Notifications-5vuitmNqIjZ" rel="external">design patterns</a>, <a href="https://www.gov.uk/topic/government-digital-guidance/content-publishing" rel="external">style guide</a> and <a href="https://docs.google.com/document/d/15-OjaEqDBy31uDU7nLZCpYIQOnzSCJR63-cp3cQI9G8" rel="external">information security guidelines</a>.</p>
|
||||
<p>Your messages must follow our <a href="https://designpatterns.hackpad.com/Notifications-5vuitmNqIjZ" rel="external">design patterns</a>, <a href="https://www.gov.uk/topic/government-digital-guidance/content-publishing" rel="external">style guide</a> and <a href="{{ url_for('.information_security') }}">information security guidelines</a>.</p>
|
||||
|
||||
<p>Your messages must not contain any personal, or otherwise sensitive, information.</p>
|
||||
<p>Your messages must not contain any personally or commercially sensitive information.</p>
|
||||
|
||||
<h3 class="heading-small" id="you-agree-to-use-delivery-data-to-improve">
|
||||
You agree to use GOV.UK Notify delivery data to continuously improve the quality of your contact data
|
||||
</h3>
|
||||
|
||||
<p>When you send messages through GOV.UK Notify, we provide feedback on the status of every text message, email and letter.</p>
|
||||
<p>When you send messages through GOV.UK Notify, we provide feedback on the status of every text message, email and letter you send.</p>
|
||||
|
||||
<p>You agree to use our delivery data to check (and potentially remove) bounced email addresses, mobile numbers and postal addresses from your database.</p>
|
||||
<p>You agree to use our delivery data to check (and potentially remove) bounced email addresses, mobile numbers, and postal addresses from your database.</p>
|
||||
|
||||
<p>You agree to ensure your user’s personal data is kept accurate and up to date, in line with Data Protection Act principles.</p>
|
||||
|
||||
@@ -169,36 +162,6 @@ Terms of use – GOV.UK Notify
|
||||
|
||||
</section>
|
||||
|
||||
<section id="requesting-to-go-live">
|
||||
<h2 class="heading-medium">
|
||||
Requesting to go live
|
||||
</h2>
|
||||
|
||||
<p>Before you can send real messages:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>you must tell us approximately how many text messages, emails and letters you plan to send</li>
|
||||
<li>you must ensure you have obtained consent to both send messages themselves, but also share data in order to do so</li>
|
||||
<li>your organisation needs to have accepted the GOV.UK Notify data sharing and financial agreement (Memorandum of Understanding)</li>
|
||||
<li>if you plan to send more than 250,000 text messages per year or any number of letters, your organisation must agree to pay any costs you run up using GOV.UK Notify</li>
|
||||
<li>we will check the messages you plan to send to make sure they meet our guidelines</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="heading-small" id="you-must-tell-us-how-many-text-messages-emails-and-letters-you-plan-to-send">
|
||||
You must tell us how many text messages, emails and letters you plan to send
|
||||
</h3>
|
||||
|
||||
<p>You must estimate how many text messages, emails and letters you plan to send each year, including any spikes or seasonal variation.</p>
|
||||
|
||||
<p>We will make sure GOV.UK Notify is easily able to handle your estimated sending volume.</p>
|
||||
|
||||
<h3 class="heading-small" id="we-will-check-your-templates-before-you-can-go-live">
|
||||
We’ll check your templates before you can go live
|
||||
</h3>
|
||||
|
||||
<p>We’ll check your templates to make sure they are transactional, not marketing, and follow our <a href="https://designpatterns.hackpad.com/Notifications-5vuitmNqIjZ" rel="external">design patterns</a>, <a href="https://www.gov.uk/topic/government-digital-guidance/content-publishing" rel="external">style guide</a> and <a href="https://docs.google.com/document/d/15-OjaEqDBy31uDU7nLZCpYIQOnzSCJR63-cp3cQI9G8" rel="external">information security guidelines</a>.</p>
|
||||
</section>
|
||||
|
||||
<section id="leaving-gov-uk-notify">
|
||||
<h2 class="heading-medium">
|
||||
Leaving GOV.UK Notify
|
||||
@@ -206,7 +169,7 @@ Terms of use – GOV.UK Notify
|
||||
|
||||
<p>You can remove your service from GOV.UK Notify at any time. <a href="{{ url_for('main.feedback') }}">Contact us</a> and we’ll delete your account.</p>
|
||||
|
||||
<p>Any data that you have processed through GOV.UK Notify will be deleted as part of the existing data deletion processes.</p>
|
||||
<p>Any data that you have already processed through GOV.UK Notify will be deleted as part of the existing data deletion processes and data retention periods.</p>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ RUN \
|
||||
echo "Install global pip packages" \
|
||||
&& pip install \
|
||||
virtualenv \
|
||||
awscli
|
||||
awscli \
|
||||
wheel
|
||||
|
||||
WORKDIR /var/project
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
echo "Install dependencies"
|
||||
cd /home/notify-app/notifications-admin;
|
||||
pip3 install -r /home/notify-app/notifications-admin/requirements.txt
|
||||
pip3 install --find-links=wheelhouse -r /home/notify-app/notifications-admin/requirements.txt
|
||||
|
||||
@@ -9,14 +9,37 @@ def test_form_contains_next_24h(app_):
|
||||
|
||||
choices = ChooseTimeForm().scheduled_for.choices
|
||||
|
||||
# Friday
|
||||
assert choices[0] == ('', 'Now')
|
||||
assert choices[1] == ('2016-01-01T12:00:00.061258', 'Midday')
|
||||
assert choices[23] == ('2016-01-02T10:00:00.061258', '10am')
|
||||
assert choices[1] == ('2016-01-01T12:00:00.061258', 'Today at midday')
|
||||
assert choices[13] == ('2016-01-02T00:00:00.061258', 'Today at midnight')
|
||||
|
||||
# Saturday
|
||||
assert choices[14] == ('2016-01-02T01:00:00.061258', 'Tomorrow at 1am')
|
||||
assert choices[37] == ('2016-01-03T00:00:00.061258', 'Tomorrow at midnight')
|
||||
|
||||
# Sunday
|
||||
assert choices[38] == ('2016-01-03T01:00:00.061258', 'Sunday at 1am')
|
||||
|
||||
# Monday
|
||||
assert choices[84] == ('2016-01-04T23:00:00.061258', 'Monday at 11pm')
|
||||
assert choices[85] == ('2016-01-05T00:00:00.061258', 'Monday at midnight')
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
assert choices[24]
|
||||
assert choices[
|
||||
12 + # hours left in the day
|
||||
(3 * 24) + # 3 days
|
||||
2 # magic number
|
||||
]
|
||||
|
||||
|
||||
@freeze_time("2016-01-01 11:09:00.061258")
|
||||
def test_form_defaults_to_now(app_):
|
||||
assert ChooseTimeForm().scheduled_for.data == ''
|
||||
|
||||
|
||||
@freeze_time("2016-01-01 11:09:00.061258")
|
||||
def test_form_contains_next_three_days(app_):
|
||||
assert ChooseTimeForm().scheduled_for.categories == [
|
||||
'Later today', 'Tomorrow', 'Sunday', 'Monday'
|
||||
]
|
||||
|
||||
@@ -22,7 +22,6 @@ def test_should_show_api_page(
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert page.h1.string.strip() == 'API integration'
|
||||
assert 'Your service is in trial mode' in page.find('div', {'class': 'banner-warning'}).text
|
||||
rows = page.find_all('details')
|
||||
assert len(rows) == 5
|
||||
for index, row in enumerate(rows):
|
||||
@@ -202,10 +201,7 @@ def test_cant_create_normal_api_key_in_trial_mode(
|
||||
'key_type': 'normal'
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert page.find('span', {'class': 'error-message'}).text.strip() == 'Not a valid choice'
|
||||
|
||||
assert response.status_code == 400
|
||||
mock_post.assert_not_called()
|
||||
|
||||
|
||||
|
||||
@@ -194,10 +194,10 @@ def test_should_show_upcoming_jobs_on_dashboard(
|
||||
assert len(table_rows) == 2
|
||||
|
||||
assert 'send_me_later.csv' in table_rows[0].find_all('th')[0].text
|
||||
assert 'Sending at 11:09am' in table_rows[0].find_all('th')[0].text
|
||||
assert 'Sending today at 11:09am' in table_rows[0].find_all('th')[0].text
|
||||
assert table_rows[0].find_all('td')[0].text.strip() == '1'
|
||||
assert 'even_later.csv' in table_rows[1].find_all('th')[0].text
|
||||
assert 'Sending at 11:09pm' in table_rows[1].find_all('th')[0].text
|
||||
assert 'Sending today at 11:09pm' in table_rows[1].find_all('th')[0].text
|
||||
assert table_rows[1].find_all('td')[0].text.strip() == '1'
|
||||
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ def test_should_show_job_in_progress(
|
||||
assert page.find('p', {'class': 'hint'}).text.strip() == 'Report is 50% complete…'
|
||||
|
||||
|
||||
@freeze_time("2016-01-01T00:00:00.061258")
|
||||
def test_should_show_scheduled_job(
|
||||
app_,
|
||||
service_one,
|
||||
@@ -162,7 +163,7 @@ def test_should_show_scheduled_job(
|
||||
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert page.find('main').find_all('p')[2].text.strip() == 'Sending will start at midnight'
|
||||
assert page.find('main').find_all('p')[2].text.strip() == 'Sending will start today at midnight'
|
||||
assert page.find('input', {'type': 'submit', 'value': 'Cancel sending'})
|
||||
|
||||
|
||||
@@ -266,7 +267,7 @@ def test_should_show_updates_for_one_job_as_json(
|
||||
assert 'Status' in content['notifications']
|
||||
assert 'Delivered' in content['notifications']
|
||||
assert '12:01am' in content['notifications']
|
||||
assert 'Uploaded by Test User on 1 January at midnight' in content['status']
|
||||
assert 'Sent by Test User on 1 January at midnight' in content['status']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -17,30 +17,7 @@ def test_letters_access_restricted(logged_in_client, mocker, can_send_letters, r
|
||||
assert response.status_code == response_code
|
||||
|
||||
|
||||
@pytest.mark.parametrize('permission', [
|
||||
'send_letters',
|
||||
'manage_templates'
|
||||
])
|
||||
def test_letters_lets_in_with_permissions(
|
||||
client,
|
||||
mocker,
|
||||
mock_login,
|
||||
mock_has_permissions,
|
||||
api_user_active,
|
||||
permission,
|
||||
):
|
||||
service = service_json(can_send_letters=True)
|
||||
mocker.patch('app.service_api_client.get_service', return_value={"data": service})
|
||||
|
||||
api_user_active._permissions[str(service['id'])] = [permission]
|
||||
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.letters', service_id=service['id']))
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_letters_rejects_without_permissions(
|
||||
def test_letters_lets_in_without_permission(
|
||||
client,
|
||||
mocker,
|
||||
mock_login,
|
||||
@@ -53,4 +30,5 @@ def test_letters_rejects_without_permissions(
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.letters', service_id=service['id']))
|
||||
|
||||
assert api_user_active.permissions == {}
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -24,3 +24,21 @@ def test_cant_see_letters_if_not_allowed(logged_in_client, mocker):
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert 'Letter templates' not in page.find('nav', class_='navigation').text
|
||||
|
||||
|
||||
def test_can_see_letters_without_permissions(
|
||||
client,
|
||||
mocker,
|
||||
mock_login,
|
||||
mock_has_permissions,
|
||||
api_user_active
|
||||
):
|
||||
service = service_json(can_send_letters=True)
|
||||
mocker.patch('app.service_api_client.get_service', return_value={"data": service})
|
||||
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.service_settings', service_id=service['id']))
|
||||
|
||||
assert response.status_code == 200
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert 'Letter templates' in page.find('nav', class_='navigation').text
|
||||
|
||||
@@ -60,8 +60,10 @@ def test_should_show_research_and_restricted_mode(
|
||||
assert response.status_code == 200
|
||||
mock_get_detailed_services.assert_called_once_with({'detailed': True})
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
# get second column, which contains flags as text.
|
||||
assert page.find_all('tbody')[table_index].find_all('td')[1].text.strip() == displayed
|
||||
# get first column in second row, which contains flags as text.
|
||||
table_body = page.find_all('table')[table_index].find_all('tbody')[0]
|
||||
service_mode = table_body.find_all('tbody')[0].find_all('tr')[1].find_all('td')[0].text.strip()
|
||||
assert service_mode == displayed
|
||||
|
||||
|
||||
def test_should_render_platform_admin_page(
|
||||
@@ -141,7 +143,7 @@ def create_stats(
|
||||
}
|
||||
|
||||
|
||||
def test_format_stats_by_service_sums_values_for_sending(fake_uuid):
|
||||
def test_format_stats_by_service_returns_correct_values(fake_uuid):
|
||||
services = [service_json(fake_uuid, 'a', [])]
|
||||
services[0]['statistics'] = create_stats(
|
||||
emails_requested=10,
|
||||
@@ -153,11 +155,65 @@ def test_format_stats_by_service_sums_values_for_sending(fake_uuid):
|
||||
)
|
||||
|
||||
ret = list(format_stats_by_service(services))
|
||||
|
||||
assert len(ret) == 1
|
||||
assert ret[0]['sending'] == 34
|
||||
assert ret[0]['delivered'] == 10
|
||||
assert ret[0]['failed'] == 16
|
||||
assert ret[0]['stats']['email']['requested'] == 10
|
||||
assert ret[0]['stats']['email']['delivered'] == 3
|
||||
assert ret[0]['stats']['email']['failed'] == 5
|
||||
|
||||
assert ret[0]['stats']['sms']['requested'] == 50
|
||||
assert ret[0]['stats']['sms']['delivered'] == 7
|
||||
assert ret[0]['stats']['sms']['failed'] == 11
|
||||
|
||||
|
||||
@pytest.mark.parametrize('restricted, table_index, research_mode', [
|
||||
(True, 1, False),
|
||||
(False, 0, False)
|
||||
])
|
||||
def test_should_show_email_and_sms_stats_for_all_service_types(
|
||||
restricted,
|
||||
table_index,
|
||||
research_mode,
|
||||
app_,
|
||||
platform_admin_user,
|
||||
mocker,
|
||||
mock_get_detailed_services,
|
||||
fake_uuid
|
||||
):
|
||||
services = [service_json(fake_uuid, 'My Service', [], restricted=restricted, research_mode=research_mode)]
|
||||
services[0]['statistics'] = create_stats(
|
||||
emails_requested=10,
|
||||
emails_delivered=3,
|
||||
emails_failed=5,
|
||||
sms_requested=50,
|
||||
sms_delivered=7,
|
||||
sms_failed=11
|
||||
)
|
||||
|
||||
mock_get_detailed_services.return_value = {'data': services}
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
mock_get_user(mocker, user=platform_admin_user)
|
||||
client.login(platform_admin_user)
|
||||
response = client.get(url_for('main.platform_admin'))
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_get_detailed_services.assert_called_once_with({'detailed': True})
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
|
||||
table_body = page.find_all('table')[table_index].find_all('tbody')[0]
|
||||
service_row_group = table_body.find_all('tbody')[0].find_all('tr')
|
||||
email_stats = service_row_group[0].find_all('td')[2:]
|
||||
sms_stats = service_row_group[1].find_all('td')[2:]
|
||||
|
||||
email_sending, email_delivered, email_failed = [int(stat.text.split()[0]) for stat in email_stats]
|
||||
sms_sending, sms_delivered, sms_failed = [int(stat.text.split()[0]) for stat in sms_stats]
|
||||
|
||||
assert email_sending == 10
|
||||
assert email_delivered == 3
|
||||
assert email_failed == 5
|
||||
assert sms_sending == 50
|
||||
assert sms_delivered == 7
|
||||
assert sms_failed == 11
|
||||
|
||||
|
||||
@pytest.mark.parametrize('restricted, table_index', [
|
||||
|
||||
@@ -315,11 +315,12 @@ def test_should_redirect_after_request_to_go_live(
|
||||
response = client.post(
|
||||
url_for('main.service_request_to_go_live', service_id='6ce466d0-fd6a-11e5-82f5-e0accb9d11a6'),
|
||||
data={
|
||||
'channel': 'Email',
|
||||
'mou': 'yes',
|
||||
'channel': 'emails',
|
||||
'start_date': '01/01/2017',
|
||||
'start_volume': '100,000',
|
||||
'peak_volume': '2,000,000',
|
||||
'upload_or_api': 'api'
|
||||
'upload_or_api': 'API'
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
@@ -338,11 +339,11 @@ def test_should_redirect_after_request_to_go_live(
|
||||
)
|
||||
|
||||
returned_message = mock_post.call_args[1]['data']['message']
|
||||
assert 'Email' in returned_message
|
||||
assert 'emails' in returned_message
|
||||
assert '01/01/2017' in returned_message
|
||||
assert '100,000' in returned_message
|
||||
assert '2,000,000' in returned_message
|
||||
assert 'api' in returned_message
|
||||
assert 'API' in returned_message
|
||||
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
flash_banner = page.find('div', class_='banner-default').string.strip()
|
||||
@@ -377,11 +378,12 @@ def test_log_error_on_request_to_go_live(
|
||||
resp = client.post(
|
||||
url_for('main.service_request_to_go_live', service_id='6ce466d0-fd6a-11e5-82f5-e0accb9d11a6'),
|
||||
data={
|
||||
'channel': 'channel',
|
||||
'mou': 'yes',
|
||||
'channel': 'emails',
|
||||
'start_date': 'start_date',
|
||||
'start_volume': 'start_volume',
|
||||
'peak_volume': 'peak_volume',
|
||||
'upload_or_api': 'upload_or_api'
|
||||
'upload_or_api': 'API'
|
||||
}
|
||||
)
|
||||
mock_logger.assert_called_with(
|
||||
|
||||
@@ -32,8 +32,8 @@ def test_should_show_name_page(app_,
|
||||
def test_should_redirect_after_name_change(app_,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
mock_update_user,
|
||||
mock_get_user):
|
||||
mock_get_user,
|
||||
mock_update_user_attribute):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -46,7 +46,7 @@ def test_should_redirect_after_name_change(app_,
|
||||
assert response.location == url_for(
|
||||
'main.user_profile', _external=True)
|
||||
api_user_active.name = new_name
|
||||
assert mock_update_user.called
|
||||
assert mock_update_user_attribute.called
|
||||
|
||||
|
||||
def test_should_show_email_page(app_,
|
||||
@@ -116,7 +116,8 @@ def test_should_render_change_email_continue_after_authenticate_email(app_,
|
||||
|
||||
def test_should_redirect_to_user_profile_when_user_confirms_email_link(app_,
|
||||
api_user_active,
|
||||
mock_login
|
||||
mock_login,
|
||||
mock_update_user_attribute
|
||||
):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
@@ -218,6 +219,7 @@ def test_should_redirect_after_mobile_number_confirm(app_,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
mock_get_user,
|
||||
mock_update_user_attribute,
|
||||
mock_check_verify_code):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from app.notify_client.user_api_client import UserApiClient
|
||||
|
||||
|
||||
@@ -13,3 +15,10 @@ def test_client_uses_correct_find_by_email(mocker, api_user_active):
|
||||
client.get_user_by_email(api_user_active.email_address)
|
||||
|
||||
mock_get.assert_called_once_with(expected_url, params=expected_params)
|
||||
|
||||
|
||||
def test_client_only_updates_allowed_attributes(mocker):
|
||||
mocker.patch('app.notify_client.current_user', id='1')
|
||||
with pytest.raises(TypeError) as error:
|
||||
UserApiClient().update_user_attribute('user_id', id='1')
|
||||
assert str(error.value) == 'Not allowed to update user attributes: id'
|
||||
|
||||
@@ -744,13 +744,21 @@ def mock_verify_password(mocker):
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_update_user(mocker):
|
||||
def _update(user):
|
||||
return user
|
||||
def mock_update_user(mocker, api_user_active):
|
||||
def _update(user_id, **kwargs):
|
||||
return api_user_active
|
||||
|
||||
return mocker.patch('app.user_api_client.update_user', side_effect=_update)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_update_user_attribute(mocker, api_user_active):
|
||||
def _update(user_id, **kwargs):
|
||||
return api_user_active
|
||||
|
||||
return mocker.patch('app.user_api_client.update_user_attribute', side_effect=_update)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_is_email_unique(mocker):
|
||||
return mocker.patch('app.user_api_client.is_email_unique', return_value=True)
|
||||
@@ -894,7 +902,7 @@ def mock_get_scheduled_job(mocker, api_user_active):
|
||||
api_user_active,
|
||||
job_id=job_id,
|
||||
job_status='scheduled',
|
||||
scheduled_for='2016-01-01T00:00:00.061258'
|
||||
scheduled_for='2016-01-02T00:00:00.061258'
|
||||
)}
|
||||
|
||||
return mocker.patch('app.job_api_client.get_job', side_effect=_get_job)
|
||||
|
||||
Reference in New Issue
Block a user