Merge branch 'master' into platform-admin

Conflicts:
	app/__init__.py
	app/main/views/add_service.py
	app/main/views/jobs.py
	app/templates/main_nav.html
	tests/app/main/views/test_dashboard.py
	tests/conftest.py
This commit is contained in:
Rebecca Law
2016-03-18 16:32:10 +00:00
66 changed files with 1356 additions and 597 deletions
+21
View File
@@ -41,6 +41,27 @@ deploy:
deployment_group: notifications_admin_deployment_group
region: eu-west-1
on: *2
- provider: s3
access_key_id: AKIAJ5MKF6G3P2JQP4QQ
secret_access_key: &1
secure: xfjg4kNBvU0B9xhRETr14mB0bCpVonlAKqGnKL2AoqpnF19yihqGNA8sv/pOGUFpeWZO3cW2GA3anyL2gGG1X0K3f81649mneVJkSHaZ2fiG/S1eKtS0Ws5XblSqLmKTPC7H9ndUxT8r+r2wLg62netBE5g8tAxw2QwN/gVz2fK/68owiyeD/jl6gw/iQ47F+mmGdAY/eFe8sUuGR4Oxj2xNAYARaDQOmHpQF/IG3M69FO5uOJtck8fUWnpd0rWxsyWBOVwwIRQHL6cWOyodeIK7YvLmzviCi1GojBPKQwQbjJu89LHfMJJTW1625drj5CNounuENTFte0Pip8zp90bg090VA8OlTXNWcyFjBQD1vNIE59vyQ/hCh90NK1nlTXdnNOwL0VZTMxQ/zYulXoMqwDLfDozhQbmnkXmexJl6BF4/dz+XmwDu7st5A/PI6U5zCK86ST/6g3MklGSseFi5Rkt6kmJrdlRIhiLnoaab8YgI0FPWjzHBC6B98ZtgIUiUk7Ng5ZTM8Sjq1HCC7mUDrDL0c7aerZA5bq2hQiKGhvjBXFU17iHZ0eEDZ8kO+jumeMwmpW6NbjlS0PuDx+lHywMSG7r+YVmkjxq5gwrTVl3evRxhHe8H/lU18y2dOKpIyX5UEZpXRq9kAWuQruCDBwoDe3Y3QP+Rg7HVGBU=
local_dir: dpl_cd_upload
skip_cleanup: true
on: &2
repo: alphagov/notifications-admin
branch: live
bucket: live-notifications-api-codedeploy
region: eu-west-1
- provider: codedeploy
access_key_id: AKIAJ5MKF6G3P2JQP4QQ
secret_access_key: *1
bucket: live-notifications-api-codedeploy
key: notifications-admin-$TRAVIS_BRANCH-$TRAVIS_BUILD_NUMBER-$TRAVIS_COMMIT.zip
bundle_type: zip
application: notifications-admin
deployment_group: live_notifications_admin_deployment_group
region: eu-west-1
on: *2
- provider: s3
access_key_id: AKIAJQPPNM6P6V53SWKA
secret_access_key: &1
+1 -1
View File
@@ -3,7 +3,7 @@ from flask.ext.script import Manager, Server
from app import create_app
application = create_app(os.getenv('NOTIFICATIONS_ADMIN_ENVIRONMENT') or 'development')
application = create_app()
manager = Manager(application)
port = int(os.environ.get('PORT', 6012))
manager.add_command("runserver", Server(host='0.0.0.0', port=port))
+20 -21
View File
@@ -2,7 +2,8 @@ import os
import re
import dateutil
from flask import (Flask, session, Markup, escape, render_template, make_response)
import urllib
from flask import (Flask, session, Markup, escape, render_template, make_response, current_app)
from flask._compat import string_types
from flask_login import LoginManager
from flask_wtf import CsrfProtect
@@ -19,6 +20,7 @@ from app.notify_client.job_api_client import JobApiClient
from app.notify_client.notification_api_client import NotificationApiClient
from app.notify_client.status_api_client import StatusApiClient
from app.notify_client.invite_api_client import InviteApiClient
from app.notify_client.statistics_api_client import StatisticsApiClient
from app.its_dangerous_session import ItsdangerousSessionInterface
from app.asset_fingerprinter import AssetFingerprinter
from utils.recipients import validate_phone_number, InvalidPhoneError
@@ -36,15 +38,16 @@ job_api_client = JobApiClient()
notification_api_client = NotificationApiClient()
status_api_client = StatusApiClient()
invite_api_client = InviteApiClient()
statistics_api_client = StatisticsApiClient()
asset_fingerprinter = AssetFingerprinter()
def create_app(config_name, config_overrides=None):
def create_app():
application = Flask(__name__)
application.config['NOTIFY_ADMIN_ENVIRONMENT'] = config_name
application.config.from_object(configs[config_name])
init_app(application, config_overrides)
application.config.from_object(os.environ['NOTIFY_ADMIN_ENVIRONMENT'])
init_app(application)
logging.init_app(application)
init_csrf(application)
@@ -55,6 +58,7 @@ def create_app(config_name, config_overrides=None):
notification_api_client.init_app(application)
status_api_client.init_app(application)
invite_api_client.init_app(application)
statistics_api_client.init_app(application)
login_manager.init_app(application)
login_manager.login_view = 'main.sign_in'
@@ -75,6 +79,7 @@ def create_app(config_name, config_overrides=None):
application.add_template_filter(format_time)
application.add_template_filter(syntax_highlight_json)
application.add_template_filter(valid_phone_number)
application.add_template_filter(linkable_name)
application.after_request(useful_headers_after_request)
register_errorhandlers(application)
@@ -101,22 +106,12 @@ def init_csrf(application):
abort(400, reason)
def init_app(app, config_overrides):
if config_overrides:
for key in app.config.keys():
if key in config_overrides:
app.config[key] = config_overrides[key]
for key, value in app.config.items():
if key in os.environ:
app.config[key] = convert_to_boolean(os.environ[key])
@app.context_processor
def init_app(application):
@application.context_processor
def inject_global_template_variables():
return {
'asset_path': '/static/',
'header_colour': app.config['HEADER_COLOUR'],
'header_colour': application.config['HEADER_COLOUR'],
'asset_url': asset_fingerprinter.get_url
}
@@ -139,6 +134,10 @@ def nl2br(value):
return Markup(result)
def linkable_name(value):
return urllib.parse.quote_plus(value)
def syntax_highlight_json(code):
return Markup(highlight(code, JavascriptLexer(), HtmlFormatter(noclasses=True)))
@@ -203,7 +202,7 @@ def register_errorhandlers(application):
@application.errorhandler(Exception)
def handle_bad_request(error):
from flask import current_app
if current_app.config.get('DEBUG'):
print(error)
# We want the Flask in browser stacktrace
if current_app.config.get('DEBUG', None):
raise error
return _error_response(500)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+13 -5
View File
@@ -20,8 +20,9 @@
<div class="textbox-highlight-mask" aria-hidden="true" />
<div class="textbox-highlight-foreground" aria-hidden="true" />
`))
.on("input", this.update)
.on("scroll", this.maintainScrollParity);
.on("input", this.update);
this.initialHeight = this.$textbox.height();
this.$backgroundMaskForeground.width(
this.$textbox.width()
@@ -32,14 +33,21 @@
};
this.update = () => this.$backgroundMaskForeground.html(
this.resize = () => this.$textbox.height(
Math.max(
this.initialHeight,
this.$backgroundMaskForeground.outerHeight()
)
);
this.replacePlaceholders = () => this.$backgroundMaskForeground.html(
$('<div/>').text(this.$textbox.val()).html().replace(
tagPattern, match => `<span class='tag'>${match}</span>`
)
);
this.maintainScrollParity = () => this.$backgroundMaskForeground.scrollTop(
this.$textbox.scrollTop()
this.update = () => (
this.replacePlaceholders() && this.resize()
);
};
+21 -1
View File
@@ -2,6 +2,10 @@
@include grid-column(3/4);
}
.column-one-sixth {
@include grid-column(1/6);
}
.column-one-eighth {
@include grid-column(1/8);
}
@@ -16,7 +20,7 @@
}
.bottom-gutter-2-3 {
margin-bottom: $gutter * 2/3;
margin-bottom: $gutter-two-thirds;
}
.align-with-heading {
@@ -26,3 +30,19 @@
padding-left: 2px;
padding-right: 2px;
}
.global-cookie-message {
p {
@extend %site-width-container;
}
}
.footer-nav {
@include copy-16;
margin-bottom: $gutter-two-thirds;
a {
display: inline-block;
margin-right: $gutter-half;
}
}
+8
View File
@@ -80,3 +80,11 @@ a {
}
}
td {
vertical-align: top;
}
.heading-xlarge {
margin-bottom: 20px;
}
@@ -0,0 +1,129 @@
/*
Taken from the GOV.UK component at
https://github.com/alphagov/static/blob/a9d462e71709d2ff6348bcce7e8c625af2b86114/app/assets/stylesheets/govuk-component/_previous-and-next-navigation.scss
and
https://github.com/alphagov/static/blob/da8aeeaa749093eab30286d7fc9f965533b66f47/app/assets/stylesheets/styleguide/_conditionals2.scss
*/
@import "_colours.scss";
@import "_typography.scss";
// Media query helpers. These make producing IE layouts
// super easy.
// These are desktop and down media queries
// There is also a local version of this in Smartanswers.
$is-ie: false !default;
@mixin media-down($size: false, $max-width: false, $min-width: false) {
@if $is-ie == false {
@if $size == mobile {
@media (max-width: 640px){
@content;
}
} @else if $size == tablet {
@media (max-width: 800px){
@content;
}
}
}
}
.govuk-previous-and-next-navigation {
@include media-down(mobile) {
margin: 2em 0 0 0;
}
display: block;
ul {
margin: 0;
padding: 0;
}
li {
@include core-16($line-height: (20 / 16));
float: left;
list-style: none;
text-align: right;
margin: 0;
padding: 0;
width: 49%;
a {
@include ie-lte(7) {
height: 4.5em;
}
display: block;
color: $link-colour;
text-decoration: none;
&:hover,
&:active {
background-color: $canvas-colour;
}
.pagination-part-title {
@include core-27($line-height: (33.75 / 27));
margin-bottom: 0.1em;
display: block;
}
}
&.next-page {
float: right;
text-align: right;
}
&.next-page a:before {
background: transparent file-url("arrow-sprite.png") no-repeat -102px -11px;
margin: -4px -32px 0 0;
display: block;
float: right;
width: 30px;
height: 38px;
content: " ";
}
&.previous-page a:before {
background: transparent file-url("arrow-sprite.png") no-repeat -20px -11px;
margin: -4px 0 0 -32px;
display: block;
float: left;
width: 30px;
height: 38px;
content: " ";
}
&.previous-page {
float: left;
text-align: left;
}
&.previous-page a {
padding: 0.75em 0 0.75em 3em;
}
&.next-page a {
padding: 0.75em 3em 0.75em 0;
}
@include media-down(mobile) {
&.previous-page,
&.next-page {
float: none;
width: 100%;
}
&.next-page a {
text-align: right;
}
}
}
}
+54 -5
View File
@@ -26,9 +26,8 @@
}
.banner-with-tick,
.banner-default-with-tick {
@extend %banner;
%banner-with-tick,
.banner-with-tick {
padding: $gutter-half ($gutter + $gutter-half);
background-image: file-url('tick-white.png');
background-size: 19px;
@@ -37,6 +36,11 @@
font-weight: bold;
}
.banner-default-with-tick {
@extend %banner;
@extend %banner-with-tick;
}
.banner-dangerous {
@extend %banner;
@@ -54,13 +58,15 @@
}
%banner-tip,
.banner-tip {
@extend %banner;
background: $yellow;
@include bold-19;
background-color: $yellow;
color: $text-colour;
text-align: left;
font-weight: bold;
margin-top: 0;
a {
&:link,
@@ -76,6 +82,12 @@
}
.banner-tip-with-tick {
@extend %banner-with-tick;
@extend %banner-tip;
background-image: file-url('tick-black.png');
}
.banner-info,
.banner-important {
@extend %banner;
@@ -91,3 +103,40 @@
.banner-info {
background-image: file-url('icon-information-2x.png');
}
.banner-mode {
@extend %banner;
background: $govuk-blue;
color: $white;
margin-top: $gutter;
.heading-medium {
margin: 0 0 10px 0;
}
a {
&:link,
&:visited {
color: $white;
}
&:hover,
&:active {
color: $light-blue-25;
}
}
.big-number {
margin-top: 10px;
&-label {
padding-bottom: 0;
}
}
}
@@ -1,3 +1,4 @@
%big-number,
.big-number {
@include bold-48($tabular-numbers: true);
@@ -5,6 +6,62 @@
&-label {
@include core-19;
display: block;
padding-bottom: $gutter-half;
}
}
.big-number-with-status {
@extend %big-number;
background: $text-colour;
color: $white;
.big-number {
padding: 15px;
}
.big-number-label {
padding-bottom: 0;
}
.big-number-status {
display: block;
background: $green;
position: relative;
&-error-percentage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $error-colour;
z-index: 1;
}
a {
&:link,
&:visited,
&:active,
&:hover {
color: $white;
}
}
.big-number {
@include bold-19;
position: relative;
z-index: 2;
}
.big-number-label {
display: inline;
}
}
}
@@ -10,6 +10,7 @@
resize: none;
z-index: 20;
background: none;
height: 200px;
}
&-textbox,
@@ -23,7 +24,6 @@
margin: 0;
padding: 4px;
overflow: hidden;
height: 200px;
}
&-background,
@@ -36,6 +36,7 @@
color: transparent;
white-space: pre-wrap;
border: 2px solid transparent;
padding-bottom: $gutter;
}
&-background {
+1
View File
@@ -48,6 +48,7 @@ $path: '/static/images/';
@import 'components/email-message';
@import 'components/api-key';
@import 'components/yes-no';
@import "components/_previous-next-navigation";
@import 'views/job';
@import 'views/edit-template';
+2 -11
View File
@@ -43,20 +43,11 @@ def increment_failed_login_count(id):
def activate_user(user):
user.state = 'active'
return user_api_client.update_user(user)
return user_api_client.activate_user(user)
def is_email_unique(email_address):
try:
if user_api_client.get_user_by_email(email_address):
return False
return True
except HTTPError as ex:
if ex.status_code == 404:
return True
else:
raise ex
return user_api_client.is_email_unique(email_address)
def send_verify_code(user_id, code_type, to):
-43
View File
@@ -150,49 +150,6 @@ class TwoFactorForm(Form):
raise ValidationError(reason)
class VerifySmsForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
validate_code_func -- Validates the code with the API.
'''
self.validate_code_func = validate_code_func
super(VerifySmsForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data, 'sms')
if not is_valid:
raise ValidationError(reason)
class VerifyForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
validate_code_func -- Validates the code with the API.
'''
self.validate_code_func = validate_code_func
super(VerifyForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
email_code = email_code()
def _validate_code(self, cde, code_type):
is_valid, reason = self.validate_code_func(cde, code_type)
if not is_valid:
raise ValidationError(reason)
def validate_email_code(self, field):
if self.sms_code.data:
self._validate_code(field.data, 'email')
def validate_sms_code(self, field):
if self.email_code.data:
self._validate_code(field.data, 'sms')
class EmailNotReceivedForm(Form):
email_address = email_address()
+4 -4
View File
@@ -8,25 +8,25 @@ from flask import (
from flask_login import login_required
from app.main import main
from app.main.dao import services_dao, users_dao
from app.main.dao import services_dao
from app.main.forms import AddServiceForm
from app.notify_client.models import InvitedUser
from app import (
invite_api_client,
user_api_client,
service_api_client)
service_api_client
)
@main.route("/add-service", methods=['GET', 'POST'])
@login_required
def add_service():
invited_user = session.get('invited_user')
if invited_user:
invitation = InvitedUser(**invited_user)
# if invited user add to service and redirect to dashboard
user = users_dao.get_user_by_id(session['user_id'])
user = user_api_client.get_user(session['user_id'])
service_id = invited_user['service']
user_api_client.add_user_to_service(service_id, user.id, invitation.permissions)
invite_api_client.accept_invite(service_id, invitation.id)
+17 -20
View File
@@ -1,35 +1,32 @@
from flask import (
render_template, redirect, session, url_for)
from flask_login import current_user
render_template,
redirect,
session,
url_for
)
from app import user_api_client
from app.main import main
from app.main.dao import users_dao
from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm
from app.main.forms import TextNotReceivedForm
@main.route('/email-not-received', methods=['GET', 'POST'])
def check_and_resend_email_code():
@main.route('/resend-email-verification')
def resend_email_verification():
# TODO there needs to be a way to regenerate a session id
user = users_dao.get_user_by_email(session['user_details']['email'])
form = EmailNotReceivedForm(email_address=user.email_address)
if form.validate_on_submit():
users_dao.send_verify_code(user.id, 'email', to=form.email_address.data)
user.email_address = form.email_address.data
users_dao.update_user(user)
return redirect(url_for('.verify'))
return render_template('views/email-not-received.html', form=form)
user = user_api_client.get_user_by_email(session['user_details']['email'])
user_api_client.send_verify_email(user.id, user.email_address)
return render_template('views/resend-email-verification.html', email=user.email_address)
@main.route('/text-not-received', methods=['GET', 'POST'])
def check_and_resend_text_code():
# TODO there needs to be a way to regenerate a session id
user = users_dao.get_user_by_email(session['user_details']['email'])
user = user_api_client.get_user_by_email(session['user_details']['email'])
form = TextNotReceivedForm(mobile_number=user.mobile_number)
if form.validate_on_submit():
users_dao.send_verify_code(user.id, 'sms', to=form.mobile_number.data)
user_api_client.send_verify_code(user.id, 'sms', to=form.mobile_number.data)
user.mobile_number = form.mobile_number.data
users_dao.update_user(user)
user_api_client.update_user(user)
return redirect(url_for('.verify'))
return render_template('views/text-not-received.html', form=form)
@@ -42,6 +39,6 @@ def verification_code_not_received():
@main.route('/send-new-code', methods=['GET'])
def check_and_resend_verification_code():
# TODO there needs to be a way to generate a new session id
user = users_dao.get_user_by_email(session['user_details']['email'])
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
user = user_api_client.get_user_by_email(session['user_details']['email'])
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
return redirect(url_for('main.two_factor'))
+29 -3
View File
@@ -8,7 +8,7 @@ from flask_login import login_required
from app.main import main
from app.main.dao.services_dao import get_service_by_id
from app.main.dao import templates_dao
from app import job_api_client
from app import job_api_client, statistics_api_client
@main.route("/services/<service_id>/dashboard")
@@ -27,11 +27,37 @@ def service_dashboard(service_id):
message = 'You have successfully accepted your invitation and been added to {}'.format(service_name)
flash(message, 'default_with_tick')
statistics = statistics_api_client.get_statistics_for_service(service_id)['data']
return render_template(
'views/service_dashboard.html',
'views/dashboard/dashboard.html',
jobs=jobs[:5],
more_jobs_to_show=(len(jobs) > 5),
free_text_messages_remaining='250,000',
spent_this_month='0.00',
template_count=len(templates),
service=service['data'],
statistics=expand_statistics(statistics),
templates=templates,
service_id=str(service_id))
def expand_statistics(statistics, danger_zone=25):
if not statistics or not statistics[0]:
return {}
today = statistics[0]
today.update({
'emails_failure_rate':
int(today['emails_error'] / today['emails_requested'] * 100) if today['emails_requested'] else 0,
'sms_failure_rate':
int(today['sms_error'] / today['sms_requested'] * 100) if today['sms_requested'] else 0
})
today.update({
'emails_percentage_of_danger_zone': min(today['emails_failure_rate'] / (danger_zone / 100), 100),
'sms_percentage_of_danger_zone': min(today['sms_failure_rate'] / (danger_zone / 100), 100)
})
return today
+10
View File
@@ -16,3 +16,13 @@ def index():
@login_required
def verify_mobile():
return render_template('views/verify-mobile.html')
@main.route('/cookies')
def cookies():
return render_template('views/cookies.html')
@main.route('/help')
def help():
return render_template('views/help.html')
+10 -2
View File
@@ -3,11 +3,15 @@ from flask import (
url_for,
session,
flash,
render_template)
render_template
)
from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.dao.services_dao import get_service_by_id_or_404
from app import (
invite_api_client,
user_api_client
@@ -32,7 +36,11 @@ def accept_invite(token):
session['invited_user'] = invited_user.serialize()
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
try:
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
except HTTPError as ex:
if ex.status_code == 404:
existing_user = False
service_users = user_api_client.get_users_for_service(invited_user.service)
+35 -1
View File
@@ -4,7 +4,12 @@ import time
from flask import (
render_template,
jsonify
abort,
jsonify,
flash,
redirect,
request,
url_for
)
from flask_login import login_required
from utils.template import Template
@@ -13,6 +18,7 @@ from app import job_api_client, notification_api_client
from app.main import main
from app.main.dao import templates_dao
from app.main.dao import services_dao
from app.utils import (get_page_from_request, generate_previous_next_dict)
@main.route("/services/<service_id>/jobs")
@@ -85,6 +91,34 @@ def view_job_updates(service_id, job_id):
})
@main.route('/services/<service_id>/notifications')
@login_required
def view_notifications(service_id):
# TODO get the api to return count of pages as well.
page = get_page_from_request()
if page is None:
abort(404, "Invalid page argument ({}) reverting to page 1.".format(request.args['page'], None))
notifications = notification_api_client.get_notifications_for_service(service_id=service_id, page=page)
prev_page = None
if notifications['links'].get('prev', None):
prev_page = generate_previous_next_dict(
'main.view_notifications',
{'service_id': service_id}, page - 1, 'Previous page', 'page {}'.format(page - 1))
next_page = None
if notifications['links'].get('next', None):
next_page = generate_previous_next_dict(
'main.view_notifications',
{'service_id': service_id}, page + 1, 'Next page', 'page {}'.format(page + 1))
return render_template(
'views/notifications.html',
service_id=service_id,
notifications=notifications['notifications'],
page=page,
prev_page=prev_page,
next_page=next_page
)
@main.route("/services/<service_id>/jobs/<job_id>/notification/<string:notification_id>")
@login_required
def view_notification(service_id, job_id, notification_id):
+2 -1
View File
@@ -3,7 +3,8 @@ from flask import (
render_template,
redirect,
url_for,
flash)
flash
)
from flask_login import (
login_required,
+19 -9
View File
@@ -1,4 +1,7 @@
from datetime import datetime, timedelta
from datetime import (
datetime,
timedelta
)
from flask import (
render_template,
@@ -12,7 +15,7 @@ from flask import (
from flask.ext.login import current_user
from app.main import main
from app.main.dao import users_dao
from app.main.forms import (
RegisterUserForm,
RegisterUserFromInviteForm
@@ -28,9 +31,9 @@ def register():
form = RegisterUserForm()
if form.validate_on_submit():
registered = _do_registration(form)
registered = _do_registration(form, send_sms=False)
if registered:
return redirect(url_for('main.verify'))
return redirect(url_for('main.registration_continue'))
else:
flash('There was an error registering your account')
return render_template('views/register.html', form=form), 400
@@ -40,7 +43,6 @@ def register():
@main.route('/register-from-invite', methods=['GET', 'POST'])
def register_from_invite():
form = RegisterUserFromInviteForm()
invited_user = session.get('invited_user')
if not invited_user:
@@ -61,8 +63,8 @@ def register_from_invite():
return render_template('views/register-from-invite.html', email_address=invited_user['email_address'], form=form)
def _do_registration(form, service=None, send_email=True):
if users_dao.is_email_unique(form.email_address.data):
def _do_registration(form, service=None, send_sms=True, send_email=True):
if user_api_client.is_email_unique(form.email_address.data):
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data,
@@ -73,11 +75,19 @@ def _do_registration(form, service=None, send_email=True):
# How do we report to the user there is a problem with
# sending codes apart from service unavailable?
# at the moment i believe http 500 is fine.
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
if send_email:
users_dao.send_verify_code(user.id, 'email', user.email_address)
user_api_client.send_verify_email(user.id, user.email_address)
if send_sms:
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
session['user_details'] = {"email": user.email_address, "id": user.id}
return True
else:
return False
@main.route('/registration-continue')
def registration_continue():
return render_template('views/registration-continue.html')
+13 -6
View File
@@ -3,15 +3,22 @@ from flask import (
redirect,
url_for,
session,
abort,
flash,
request
)
from flask.ext.login import (current_user, login_fresh, confirm_login)
from flask.ext.login import (
current_user,
login_fresh,
confirm_login
)
from app.main import main
from app.main.dao import (users_dao, services_dao)
from app.main.dao import services_dao
from app import user_api_client
from app.main.forms import LoginForm
@@ -22,7 +29,7 @@ def sign_in():
form = LoginForm()
if form.validate_on_submit():
user = users_dao.get_user_by_email(form.email_address.data)
user = user_api_client.get_user_by_email(form.email_address.data)
user = _get_and_verify_user(user, form.password.data)
if user:
# Remember me login
@@ -41,7 +48,7 @@ def sign_in():
if user.state == 'pending':
return redirect(url_for('.verify'))
elif user.is_active():
users_dao.send_verify_code(user.id, 'sms', user.mobile_number)
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
if request.args.get('next'):
return redirect(url_for('.two_factor', next=request.args.get('next')))
else:
@@ -62,7 +69,7 @@ def _get_and_verify_user(user, password):
return None
elif user.is_locked():
return None
elif not users_dao.verify_password(user.id, password):
elif not user_api_client.verify_password(user.id, password):
return None
else:
return user
+25 -7
View File
@@ -1,16 +1,35 @@
from flask import (
request, render_template, redirect, url_for, session)
request,
render_template,
redirect,
url_for,
session
)
from flask.ext.login import current_user
from flask_login import login_required
from app.main import main
from app.main.dao.users_dao import (
verify_password, update_user, check_verify_code, is_email_unique,
send_verify_code)
from app.main.forms import (
ChangePasswordForm, ChangeNameForm, ChangeEmailForm, ConfirmEmailForm,
ChangeMobileNumberForm, ConfirmMobileNumberForm, ConfirmPasswordForm
verify_password,
update_user,
check_verify_code,
is_email_unique,
send_verify_code
)
from app.main.forms import (
ChangePasswordForm,
ChangeNameForm,
ChangeEmailForm,
ConfirmEmailForm,
ChangeMobileNumberForm,
ConfirmMobileNumberForm,
ConfirmPasswordForm
)
from app import user_api_client
NEW_EMAIL = 'new-email'
NEW_MOBILE = 'new-mob'
NEW_EMAIL_PASSWORD_CONFIRMED = 'new-email-password-confirmed'
@@ -63,7 +82,6 @@ def user_profile_email():
@main.route("/user-profile/email/authenticate", methods=['GET', 'POST'])
@login_required
def user_profile_email_authenticate():
# Validate password for form
def _check_password(pwd):
return verify_password(current_user.id, pwd)
+46 -16
View File
@@ -1,42 +1,72 @@
import json
from flask import (
render_template,
redirect,
session,
url_for
url_for,
current_app,
flash
)
from itsdangerous import SignatureExpired
from flask_login import login_user
from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.dao import users_dao
from app.main.forms import (
VerifyForm,
VerifySmsForm
)
from app.main.forms import TwoFactorForm
from app import user_api_client
@main.route('/verify', methods=['GET', 'POST'])
def verify():
# TODO there needs to be a way to regenerate a session id
# or handle gracefully.
user_id = session['user_details']['id']
def _check_code(code, code_type):
return users_dao.check_verify_code(user_id, code, code_type)
def _check_code(code):
return user_api_client.check_verify_code(user_id, code, 'sms')
if session.get('invited_user'):
form = VerifySmsForm(_check_code)
else:
form = VerifyForm(_check_code)
form = TwoFactorForm(_check_code)
if form.validate_on_submit():
try:
user = users_dao.get_user_by_id(user_id)
activated_user = users_dao.activate_user(user)
user = user_api_client.get_user(user_id)
activated_user = user_api_client.activate_user(user)
login_user(activated_user)
return redirect(url_for('main.add_service', first='first'))
finally:
session.pop('user_details', None)
return render_template('views/verify.html', form=form)
return render_template('views/two-factor.html', form=form)
@main.route('/verify-email/<token>')
def verify_email(token):
from utils.url_safe_token import check_token
try:
token_data = check_token(token,
current_app.config['SECRET_KEY'],
current_app.config['DANGEROUS_SALT'],
current_app.config['EMAIL_EXPIRY_SECONDS'])
token_data = json.loads(token_data)
verified = user_api_client.check_verify_code(token_data['user_id'], token_data['secret_code'], 'email')
if verified[0]:
user = user_api_client.get_user(token_data['user_id'])
user_api_client.send_verify_code(user.id, 'sms', user.mobile_number)
session['user_details'] = {"email": user.email_address, "id": user.id}
return redirect('verify')
else:
message = "There was a problem verifying your account. Error message: '{}'".format(verified[1])
flash(message)
# TODO could this ask for a resend instead?
return redirect(url_for('main.index'))
except SignatureExpired:
flash('The link in the email we sent you has expired')
return redirect(url_for('main.resend_email_verification'))
@@ -0,0 +1,18 @@
from notifications_python_client.base import BaseAPIClient
class StatisticsApiClient(BaseAPIClient):
def __init__(self, base_url=None, client_id=None, secret=None):
super(self.__class__, self).__init__(base_url=base_url or 'base_url',
client_id=client_id or 'client_id',
secret=secret or 'secret')
def init_app(self, app):
self.base_url = app.config['API_HOST_NAME']
self.client_id = app.config['ADMIN_CLIENT_USER_NAME']
self.secret = app.config['ADMIN_CLIENT_SECRET']
def get_statistics_for_service(self, service_id):
return self.get(
url='/service/{}/notifications-statistics'.format(service_id),
)
+22 -9
View File
@@ -32,14 +32,7 @@ class UserApiClient(BaseAPIClient):
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
def get_user_by_email(self, email_address):
try:
params = {'email': email_address}
user_data = self.get('/user/email', params=params)
except HTTPError as e:
if e.status_code == 404:
return None
else:
raise e
user_data = self.get('/user/email', params={'email': email_address})
return User(user_data['data'], max_failed_login_count=self.max_failed_login_count)
def get_users(self):
@@ -68,7 +61,12 @@ class UserApiClient(BaseAPIClient):
def send_verify_code(self, user_id, code_type, to):
data = {'to': to}
endpoint = '/user/{0}/{1}-code'.format(user_id, code_type)
resp = self.post(endpoint, data=data)
self.post(endpoint, data=data)
def send_verify_email(self, user_id, to):
data = {'to': to}
endpoint = '/user/{0}/email-verification'.format(user_id)
self.post(endpoint, data=data)
def check_verify_code(self, user_id, code, code_type):
data = {'code_type': code_type, 'code': code}
@@ -106,3 +104,18 @@ class UserApiClient(BaseAPIClient):
endpoint = '/user/reset-password'
data = {'email': email_address}
self.post(endpoint, data=data)
def is_email_unique(self, email_address):
try:
if self.get_user_by_email(email_address):
return False
return True
except HTTPError as ex:
if ex.status_code == 404:
return True
else:
raise ex
def activate_user(self, user):
user.state = 'active'
return self.update_user(user)
+13
View File
@@ -24,6 +24,10 @@
{% endblock %}
{% block cookie_message %}
<p>
GOV.UK Notify uses cookies to make the site simpler.
<a href="{{ url_for("main.cookies") }}">Find out more about cookies</a>
</p>
{% endblock %}
{% block inside_header %}
@@ -68,6 +72,15 @@
</div>
{% endblock %}
{% block footer_support_links %}
<nav class="footer-nav">
<a href="{{ url_for("main.help") }}">Help</a>
<a href="https://docs.google.com/forms/d/1AL8U-xJX_HAFEiQiJszGQw0PcEaEUnYATSntEghNDGo/viewform">Support and feedback</a>
<a href="{{ url_for("main.cookies") }}">Cookies</a>
Built by the <a href="https://www.gov.uk/government/organisations/government-digital-service">Government Digital Service</a>
</nav>
{% endblock %}
{% block body_end %}
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}" /></script>
{% endblock %}
+3 -13
View File
@@ -1,19 +1,9 @@
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None) %}
<div class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'>
{% if subhead %}
<div class="grid-row">
<div class="column-one-third">
{{ subhead }}
</div>
<div class="column-two-thirds">
{% endif %}
{% if subhead -%}
{{ subhead }}&ensp;
{%- endif -%}
{{ body }}
{% if subhead %}
</div>
</div>
{% endif %}
{% if delete_button %}
<form method='post'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+11
View File
@@ -4,3 +4,14 @@
<span class="big-number-label">{{ label }}</span>
</div>
{% endmacro %}
{% macro big_number_with_status(number, label, status_number, status_label, percentage_bad=0) %}
<div class="big-number-with-status">
{{ big_number(number, label) }}
<div class="big-number-status">
<div class="big-number-status-error-percentage" style="opacity: {{ percentage_bad / 100 }}"></div>
{{ big_number(status_number, status_label) }}
</div>
</div>
{% endmacro %}
@@ -0,0 +1,22 @@
{% macro previous_next_navigation(previous_page, next_page) %}
<nav class="govuk-previous-and-next-navigation" role="navigation" aria-label="Pagination">
<ul class="group">
{% if previous_page %}
<li class="previous-page">
<a href="{{previous_page['url']}}" rel="previous" >
<span class="pagination-part-title">{{previous_page['title']}}</span>
<span class="pagination-label">{{previous_page['label']}}</span>
</a>
</li>
{% endif %}
{% if next_page %}
<li class="next-page">
<a href="{{next_page['url']}}" rel="next">
<span class="pagination-part-title">{{next_page['title']}}</span>
<span class="pagination-label">{{next_page['label']}}</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endmacro %}
+1 -1
View File
@@ -4,7 +4,7 @@
{% if name %}
<h3 class="sms-message-name">
{% if edit_link %}
<a href="{{ edit_link }}">{{ name }}</a>
<a name="{{ name|linkable_name }}" href="{{ edit_link }}">{{ name }}</a>
{% else %}
{{ name }}
{% endif %}
+9 -4
View File
@@ -2,29 +2,34 @@
<h2 class="navigation-service-name">
<a href="{{ url_for('.service_dashboard', service_id=service_id) }}">{{ session.get('service_name', 'Service') }}</a>
</h2>
{% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']) %}
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
<ul>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li>
</ul>
{% elif current_user.has_permissions(permissions=['manage_templates'], admin_override=True) %}
{% elif current_user.has_permissions(['manage_templates']) %}
<ul>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Text message templates</a></li>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Email templates</a></li>
</ul>
{% endif %}
{% if current_user.has_permissions(permissions=['manage_users', 'manage_settings'], admin_override=True) %}
{% if current_user.has_permissions(['manage_users', 'manage_settings']) %}
<ul>
<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>
{% endif %}
{% if current_user.has_permissions(permissions=['manage_api_keys', 'access_developer_docs']) %}
{% if current_user.has_permissions(['manage_api_keys', 'access_developer_docs']) %}
<ul>
<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>
{% endif %}
<ul>
<li><a href="{{ url_for('.view_notifications', service_id=service_id) }}">Notification history</a></li>
<li><a href="{{ url_for('.view_jobs', service_id=service_id) }}">Job history</a><li>
</ul>
{% if current_user.has_permissions(admin_override=True) %}
<ul>
<li><a href="{{ url_for('.show_all_services') }}"> List all services </a></li>
+2 -2
View File
@@ -37,9 +37,9 @@
{% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters'], or_=True) %}
{{ banner(
"""
Send yourself a test message
Send yourself a test
""",
subhead='Next step',
subhead='Next step:',
type="tip"
)}}
{% endif %}
+81
View File
@@ -0,0 +1,81 @@
{% extends "withoutnav_template.html" %}
{% block page_title %}
Cookies GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Cookies</h1>
<p class="summary">
GOV.UK Notify puts small files (known as cookies)
on to your computer.
</p>
<p>These cookies are used to remember you once youve logged in</p>
<p>
Find out <a href="http://www.aboutcookies.org/">how to manage cookies</a>.
</p>
<h2 class="heading-medium">Session cookies</h2>
<p>
We store session cookies on your computer to help keep your information
secure while you use the service.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Purpose</th>
<th>Expires</th>
</tr>
</thead>
<tbody>
<tr>
<td>
notify_admin_session
</td>
<td width="50%">
Used to keep you logged in
</td>
<td>
1 hour
</td>
</tr>
</tbody>
</table>
<h2 class="heading-medium">Introductory message cookie</h2>
<p>
When you first use the service, you may see a pop-up welcome message.
Once youve seen the message, we store a cookie on your computer so it
knows not to show it again.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Purpose</th>
<th>Expires</th>
</tr>
</thead>
<tbody>
<tr>
<td>
seen_cookie_message
</td>
<td width="50%">
Saves a message to let us know that you have seen our cookie
message
</td>
<td>
1 month
</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends "withnav_template.html" %}
{% block page_title %}
{{ session.get('service_name', 'Dashboard') }} GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
{% if service.restricted %}
{% include 'views/dashboard/trial-mode-banner.html' %}
{% endif %}
{% if not jobs %}
{% include 'views/dashboard/get-started.html' %}
{% else %}
{% include 'views/dashboard/today.html' %}
{% include 'views/dashboard/jobs.html' %}
{% endif %}
{% endblock %}
@@ -0,0 +1,19 @@
{% from "components/banner.html" import banner_wrapper %}
<h2 class="heading-medium">Get started</h2>
<ol class="grid-row">
{% if current_user.has_permissions(['manage_templates']) %}
<li class="column-half">
{% call banner_wrapper(type="tip", subhead='1.' if not templates else None, with_tick=templates|length) %}
<a href='{{ url_for(".add_service_template", service_id=service_id, template_type="sms") }}'>Add a template</a>
{% endcall %}
</li>
{% endif %}
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
<li class="column-half">
{% call banner_wrapper(type="tip", subhead='2.') %}
<a href='{{ url_for(".choose_template", service_id=service_id, template_type="sms") }}'>Send yourself a message</a>
{% endcall %}
</li>
{% endif %}
</ol>
+28
View File
@@ -0,0 +1,28 @@
{% from "components/table.html" import list_table, field, right_aligned_field_heading, hidden_field_heading %}
{% call(item) list_table(
jobs,
caption="Recent batch jobs",
empty_message='You havent sent any text messages yet',
field_headings=['File', 'Started', right_aligned_field_heading('Rows'), right_aligned_field_heading('Sent')]
) %}
{% call field() %}
<a href="{{ url_for('.view_job', service_id=service_id, job_id=item.id) }}">{{ item.original_file_name }}</a>
{% endcall %}
{% call field() %}
{{ item.created_at|format_datetime }}
{% endcall %}
{% call field(align='right') %}
{{ item.notification_count }}
{% endcall %}
{% call field(align='right') %}
{{ item.notifications_sent }}
{% endcall %}
{% endcall %}
{% if more_jobs_to_show %}
{% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
<p class="table-show-more-link">
<a href="{{ url_for('.view_jobs', service_id=service_id) }}">See all sent text messages</a>
</p>
{% endif %}
{% endif %}
+25
View File
@@ -0,0 +1,25 @@
{% from "components/big-number.html" import big_number_with_status %}
<h2 class="heading-medium">
Sent today
</h2>
<div class="grid-row">
<div class="column-half">
{{ big_number_with_status(
statistics.get('emails_requested', 0),
'email' if statistics.get('emails_requested') == 1 else 'emails',
'{}%'.format(statistics.get('emails_failure_rate', 0)),
'failed',
statistics.get('emails_percentage_of_danger_zone', 0)
) }}
</div>
<div class="column-half">
{{ big_number_with_status(
statistics.get('sms_requested', 0),
'text message' if statistics.get('sms_requested') == 1 else 'text messages',
'{}%'.format(statistics.get('sms_failure_rate', 0)),
'failed',
statistics.get('sms_percentage_of_danger_zone', 0)
) }}
</div>
</div>
@@ -0,0 +1,25 @@
{% from "components/banner.html" import banner_wrapper %}
{% from "components/big-number.html" import big_number %}
{% call banner_wrapper(type="mode") %}
<h2 class="heading-medium">Trial mode</h2>
<div class="grid-row">
<div class="column-one-half">
<p>
Well only deliver messages to you and members of your team
<br>
<a href="{{ url_for(".help", _anchor="trial-mode") }}">Find out more</a>
</p>
</div>
<div class="column-one-sixth">
&nbsp;
</div>
<div class="column-one-third">
{{ big_number(
service.limit - statistics.get('emails_requested', 0) - statistics.get('sms_requested', 0),
'messages left today'
) }}
</div>
</div>
{% endcall %}
@@ -1,24 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
Check your email address GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Check your email address</h1>
<p>Check your email address is correct and then resend the confirmation code.</p>
<form autocomplete="off" method="post">
{{ textbox(form.email_address, hint='Your email address must end in .gov.uk') }}
{{ page_footer('Resend confirmation code') }}
</form>
</div>
</div>
{% endblock %}
+79
View File
@@ -0,0 +1,79 @@
{% extends "withoutnav_template.html" %}
{% block page_title %}
Cookies GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-xlarge">Help</h1>
<h2 class="heading-large" id="trial-mode">Trial mode</h2>
<p>
When you create a service on GOV.UK&nbsp;Notify, you start off in trial
mode. This means:
</p>
<ul class="list list-bullet">
<li>
you can only send messages to yourself and your team members
</li>
<li>
you can only send 50 messages per day
</li>
</ul>
<p>
Anyone working in central government can create an account on
GOV.UK&nbsp;Notify and try it out. So its important that you start off
with some restrictions in place.
</p>
<h3 class="heading-medium">To remove these restrictions</h3>
<p>
You need to request to go live:
</p>
<ul class="list list-bullet">
<li>
Youll need to agree to our terms of use
</li>
<li>
If you plan to send more than 250,000 text messages per year, youll
need to agree to pay for what you use take a look at our pricing
</li>
<li>
Well check your templates to make sure theyre consistent with our
design patterns, style guide and information security principles
</li>
</ul>
<h3 class="heading-medium">
All other aspects of GOV.UK&nbsp;Notify are exactly the same
</h2>
<p>
You can still:
</p>
<ul class="list list-bullet">
<li>
upload a batch of recipients
</li>
<li>
do an API integration
</li>
</ul>
<p>
Well send the messages you have permission to send. The messages you
dont have permission to send will still be listed on your
GOV.UK&nbsp;Notify dashboard but wont actually get sent. This means you
can check that everything is fully working without accidentally sending
hundreds of text messages or emails.
</p>
</div>
</div>
{% endblock %}
+42
View File
@@ -0,0 +1,42 @@
{% extends "withnav_template.html" %}
{% from "components/table.html" import list_table, field, right_aligned_field_heading %}
{% from "components/previous-next-navigation.html" import previous_next_navigation %}
{% block page_title %}
Notifications activity GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">Notifications activity</h1>
{% call(item) list_table(
notifications,
caption="Recent activity",
caption_visible=False,
empty_message='You havent sent any notifications yet',
field_headings=['Recipient', 'Template', 'Type', 'Job', 'Status', 'Time'])
%}
{% call field() %}
{{ item.to }}
{% endcall %}
{% call field() %}
<a href="{{ url_for(".choose_template", service_id=service_id, template_type=item.template.template_type, _anchor=item.template.name|linkable_name) }}">{{ item.template.name }}</a>
{% endcall %}
{% call field() %}
{{ item.template.template_type }}
{% endcall %}
{% call field() %}
{% if item.job %}
<a href="{{ url_for(".view_job", service_id=service_id, job_id=item.job.id) }}">{{ item.job.original_file_name }}</a>
{% endif %}
{% endcall %}
{% call field() %}
{{ item.status }}
{% endcall %}
{% call field() %}
{{ item.created_at | format_datetime}}
{% endcall %}
{% endcall %}
{{ previous_next_navigation(prev_page, next_page) }}
{% endblock %}
@@ -0,0 +1,16 @@
{% extends "withoutnav_template.html" %}
{% block page_title %}
Check your email GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Now check your email</h1>
<p>Click the link in the email weve sent you to continue your registration</p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,18 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
Check your email GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Check your email</h1>
<p>In order to verify your email address we've sent a new confirmation link to {{email}}</p>
</div>
</div>
{% endblock %}
+1 -1
View File
@@ -65,7 +65,7 @@
{% if more_jobs_to_show %}
{% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']) %}
<p class="table-show-more-link">
<a href="{{ url_for('.view_jobs', service_id=service_id) }}">See all sent text messages</a>
<a href="{{ url_for('.view_notifications', service_id=service_id) }}">See all sent text messages</a>
</p>
{% endif %}
{% endif %}
-53
View File
@@ -1,53 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
{% if form.email_code %}
Enter the codes weve sent you GOV.UK Notify
{% else %}
Enter the code weve sent you GOV.UK Notify
{% endif %}
{% endblock %}
{% block maincolumn_content %}
<div class="grid-row">
<div class="column-two-thirds">
{% if form.email_code %}
<h1 class="heading-large">Enter the codes weve sent you</h1>
{% else %}
<h1 class="heading-large">Enter the code weve sent you</h1>
{% endif %}
{% if form.email_code %}
<p>
Weve sent you confirmation codes by email and text message.
</p>
{% else %}
<p>
Weve sent you a confirmation code by text message.
</p>
{% endif %}
<form autocomplete="off" method="post">
{% if form.sms_code %}
{{ textbox(
form.sms_code,
width='5em',
help_link=url_for('.check_and_resend_text_code'),
help_link_text='I havent received a text message'
) }}
{% endif %}
{% if form.email_code %}
{{ textbox(
form.email_code,
width='5em',
help_link=url_for('.check_and_resend_email_code'),
help_link_text='I havent received an email'
) }}
{% endif %}
{{ page_footer("Continue") }}
</form>
</div>
</div>
{% endblock %}
+19 -1
View File
@@ -1,7 +1,7 @@
import re
from functools import wraps
from flask import (abort, session)
from flask import (abort, session, request, url_for)
class BrowsableItem(object):
@@ -83,3 +83,21 @@ def get_errors_for_csv(recipients, template_type):
errors.append("fill in {} empty cells".format(number_of_rows_with_missing_data))
return errors
def get_page_from_request():
if 'page' in request.args:
try:
return int(request.args['page'])
except ValueError:
return None
else:
return 1
def generate_previous_next_dict(view, view_dict, page, title, label):
return {
'url': url_for(view, page=page, **view_dict),
'title': title,
'label': label
}
+9 -17
View File
@@ -28,17 +28,16 @@ class Config(object):
REMEMBER_COOKIE_SECURE = True
API_HOST_NAME = os.getenv('API_HOST_NAME')
NOTIFY_API_SECRET = os.getenv('NOTIFY_API_SECRET', "dev-secret")
NOTIFY_API_CLIENT = os.getenv('NOTIFY_API_CLIENT', "admin")
ADMIN_CLIENT_USER_NAME = os.getenv('ADMIN_CLIENT_USER_NAME')
ADMIN_CLIENT_SECRET = os.getenv('ADMIN_CLIENT_SECRET')
WTF_CSRF_ENABLED = True
SECRET_KEY = 'secret-key'
SECRET_KEY = 'dev-notify-secret-key'
HTTP_PROTOCOL = 'http'
DANGEROUS_SALT = 'itsdangeroussalt'
DANGEROUS_SALT = 'dev-notify-salt'
TOKEN_MAX_AGE_SECONDS = 3600
EMAIL_EXPIRY_SECONDS = TOKEN_MAX_AGE_SECONDS * 24 * 7 # one week
DEFAULT_SERVICE_LIMIT = 50
@@ -63,6 +62,7 @@ class Development(Config):
class Test(Development):
DEBUG = True
WTF_CSRF_ENABLED = False
@@ -72,18 +72,10 @@ class Preview(Config):
HEADER_COLOUR = '#F47738' # $orange
class Staging(Preview):
SHOW_STYLEGUIDE = False
class Live(Staging):
HEADER_COLOUR = '#B10E1E' # $red
configs = {
'development': Development,
'test': Test,
'preview': Preview,
'staging': Staging,
'live': Live
'development': 'config.Development',
'test': 'config.Test',
'preview': 'config.Preview',
'staging': 'config_staging.Staging',
'live': 'config_live.Live'
}
+12
View File
@@ -0,0 +1,12 @@
import os
from config import Config
class Live(Config):
SHOW_STYLEGUIDE = False
HEADER_COLOUR = '#B10E1E' # $red
HTTP_PROTOCOL = 'https'
API_HOST_NAME = os.getenv('LIVE_API_HOST_NAME')
ADMIN_CLIENT_SECRET = os.getenv('LIVE_ADMIN_CLIENT_SECRET')
SECRET_KEY = os.getenv('LIVE_SECRET_KEY')
DANGEROUS_SALT = os.getenv('LIVE_DANGEROUS_SALT')
+11
View File
@@ -0,0 +1,11 @@
import os
from config import Config
class Staging(Config):
SHOW_STYLEGUIDE = False
HTTP_PROTOCOL = 'https'
API_HOST_NAME = os.getenv('STAGING_API_HOST_NAME')
ADMIN_CLIENT_SECRET = os.getenv('STAGING_ADMIN_CLIENT_SECRET')
SECRET_KEY = os.getenv('STAGING_SECRET_KEY')
DANGEROUS_SALT = os.getenv('STAGING_DANGEROUS_SALT')
+1
View File
@@ -1,3 +1,4 @@
#!/bin/bash
export NOTIFY_ADMIN_ENVIRONMENT='config.Development'
python3 app.py runserver
+1
View File
@@ -32,5 +32,6 @@ display_result $? 2 "Front end code style check"
#py.test --cov=app tests/
#display_result $? 3 "Code coverage"
export NOTIFY_ADMIN_ENVIRONMENT='config.Test'
py.test -v
display_result $? 4 "Unit tests"
+31 -4
View File
@@ -31,7 +31,7 @@ def service_json(id_, name, users, limit=1000, active=False, restricted=True):
}
def template_json(id_, name, type_, content, service_id):
def template_json(service_id, id_=1, name="sample template", type_="sms", content="template content"):
return {
'id': id_,
'name': name,
@@ -116,13 +116,40 @@ def job_json():
return data
def notification_json():
def notification_json(service_id,
job=None,
template=None,
to='07123456789',
status='sent',
sent_at=None,
created_at=None,
with_links=False):
import datetime
if job is None:
job = job_json()
if template is None:
template = template_json(service_id)
if sent_at is None:
sent_at = str(datetime.datetime.now().time())
if created_at is None:
created_at = str(datetime.datetime.now().time())
links = {}
if with_links:
links = {
'prev': '/service/{}/notifications'.format(service_id),
'next': '/service/{}/notifications'.format(service_id),
'last': '/service/{}/notifications'.format(service_id)
}
data = {
'notifications': [{
'sent_at': str(datetime.datetime.now().time())
'to': to,
'template': {'id': template['id'], 'name': template['name']},
'job': {'id': job['id'], 'original_file_name': job['original_file_name']},
'sent_at': sent_at,
'status': status,
'created_at': created_at
} for i in range(5)],
'links': {}
'links': links
}
return data
@@ -0,0 +1,13 @@
from app.notify_client.statistics_api_client import StatisticsApiClient
def test_client_uses_correct_find_by_email(mocker, api_user_active):
expected_url = '/service/a1b2c3d4/notifications-statistics'
client = StatisticsApiClient()
mock_get = mocker.patch('app.notify_client.statistics_api_client.StatisticsApiClient.get')
client.get_statistics_for_service('a1b2c3d4')
mock_get.assert_called_once_with(url=expected_url)
-91
View File
@@ -1,91 +0,0 @@
from datetime import datetime, timedelta
from app.main.forms import VerifyForm
from app.main.dao import users_dao
from tests import create_test_user
def test_form_should_have_error_when_code_is_not_valid(app_,
mock_check_verify_code):
with app_.test_request_context(method='POST',
data={'sms_code': '12345aa', 'email_code': 'abcde'}) as req:
def _check_code(code, code_type):
return users_dao.check_verify_code('1', code, code_type)
form = VerifyForm(_check_code)
assert form.validate() is False
errors = form.errors
assert len(errors) == 2
expected = {'email_code': ['Code must be 5 digits', 'Code does not match'],
'sms_code': ['Code does not match', 'Code must be 5 digits']}
assert 'sms_code' in errors
assert set(errors) == set(expected)
def test_should_return_errors_when_code_missing(app_,
mock_check_verify_code):
with app_.test_request_context(method='POST',
data={}) as req:
def _check_code(code, code_type):
return users_dao.check_verify_code('1', code, code_type)
form = VerifyForm(_check_code)
assert form.validate() is False
errors = form.errors
expected = {'sms_code': ['SMS code can not be empty'],
'email_code': ['Email code can not be empty']}
assert len(errors) == 2
assert set(errors) == set(expected)
def test_should_return_errors_when_code_is_too_short(app_,
mock_check_verify_code):
with app_.test_request_context(method='POST',
data={'sms_code': '123', 'email_code': '123'}) as req:
def _check_code(code, code_type):
return users_dao.check_verify_code('1', code, code_type)
form = VerifyForm(_check_code)
assert form.validate() is False
errors = form.errors
expected = {'sms_code': ['Code must be 5 digits', 'Code does not match'],
'email_code': ['Code must be 5 digits', 'Code does not match']}
assert len(errors) == 2
assert set(errors) == set(expected)
def test_should_return_errors_when_code_does_not_match(app_,
mock_check_verify_code_code_not_found):
with app_.test_request_context(method='POST',
data={'sms_code': '34567', 'email_code': '34567'}) as req:
def _check_code(code, code_type):
return users_dao.check_verify_code('1', code, code_type)
form = VerifyForm(_check_code)
assert form.validate() is False
errors = form.errors
expected = {'sms_code': ['Code not found'],
'email_code': ['Code not found']}
assert len(errors) == 2
assert set(errors) == set(expected)
def test_should_return_errors_when_code_is_expired(app_,
mock_check_verify_code_code_expired):
with app_.test_request_context(method='POST',
data={'sms_code': '23456',
'email_code': '23456'}) as req:
def _check_code(code, code_type):
return users_dao.check_verify_code('1', code, code_type)
form = VerifyForm(_check_code)
assert form.validate() is False
errors = form.errors
expected = {'sms_code': ['Code has expired'],
'email_code': ['Code has expired']}
assert len(errors) == 2
assert set(errors) == set(expected)
+5 -1
View File
@@ -175,7 +175,7 @@ def test_new_user_accept_invite_calls_api_and_views_registration_page(app_,
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.h1.string.strip() == 'Create an account'
email_in_page = page.find('p')
email_in_page = page.find('main').find('p')
assert email_in_page.text.strip() == 'Your account will be created with this email: invited_user@test.gov.uk' # noqa
form = page.find('form')
@@ -215,6 +215,7 @@ def test_new_user_accept_invite_completes_new_registration_redirects_to_verify(a
api_user_active,
mock_check_invite_token,
mock_dont_get_user_by_email,
mock_is_email_unique,
mock_register_user,
mock_send_verify_code,
mock_get_users_by_service,
@@ -266,6 +267,7 @@ def test_new_invited_user_verifies_and_added_to_service(app_,
api_user_active,
mock_check_invite_token,
mock_dont_get_user_by_email,
mock_is_email_unique,
mock_register_user,
mock_send_verify_code,
mock_check_verify_code,
@@ -275,6 +277,7 @@ def test_new_invited_user_verifies_and_added_to_service(app_,
mock_accept_invite,
mock_get_service,
mock_get_service_templates,
mock_get_service_statistics,
mock_get_jobs):
with app_.test_request_context():
@@ -298,6 +301,7 @@ def test_new_invited_user_verifies_and_added_to_service(app_,
# when they post codes back to admin user should be added to
# service and sent on to dash board
expected_permissions = ['send_messages', 'manage_service', 'manage_api_keys']
with client.session_transaction() as session:
new_user_id = session['user_id']
mock_add_user_to_service.assert_called_with(data['service'], new_user_id, expected_permissions)
+17 -41
View File
@@ -1,39 +1,32 @@
import pytest
from flask import url_for
from bs4 import BeautifulSoup
def test_should_render_email_verification_resent_show_email_address_and_resend_verify_email(app_,
mocker,
api_user_active,
mock_get_user_by_email,
mock_send_verify_email):
def test_should_render_email_code_not_received_template_and_populate_email_address(app_,
api_user_active,
mock_get_user_by_email,
mock_send_verify_code):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active.id,
'email': api_user_active.email_address}
response = client.get(url_for('main.check_and_resend_email_code'))
response = client.get(url_for('main.resend_email_verification'))
assert response.status_code == 200
assert 'Check your email address is correct and then resend the confirmation code' \
in response.get_data(as_text=True)
assert 'value="test@user.gov.uk"' in response.get_data(as_text=True)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
def test_should_check_and_resend_email_code_redirect_to_verify(app_,
api_user_active,
mock_get_user_by_email,
mock_update_user,
mock_send_verify_code):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active.id,
'email': api_user_active.email_address}
response = client.post(url_for('main.check_and_resend_email_code'),
data={'email_address': 'test@user.gov.uk'})
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
assert page.h1.string == 'Check your email'
expected = "In order to verify your email address we've sent a new confirmation link to {}".format(api_user_active.email_address) # noqa
message = page.find_all('p')[1].text
assert message == expected
mock_send_verify_email.assert_called_with(api_user_active.id, api_user_active.email_address)
def test_should_render_text_code_not_received_template(app_,
@@ -70,23 +63,6 @@ def test_should_check_and_redirect_to_verify(app_,
assert response.location == url_for('main.verify', _external=True)
def test_should_update_email_address_resend_code(app_,
api_user_active,
mock_get_user_by_email,
mock_update_user,
mock_send_verify_code):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active.id,
'email': api_user_active.email_address}
response = client.post(url_for('main.check_and_resend_email_code'),
data={'email_address': 'new@address.gov.uk'})
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
def test_should_update_mobile_number_resend_code(app_,
api_user_active,
mock_get_user_by_email,
+2
View File
@@ -5,6 +5,7 @@ def test_should_show_recent_jobs_on_dashboard(app_,
api_user_active,
mock_get_service,
mock_get_service_templates,
mock_get_service_statistics,
mock_get_user,
mock_get_user_by_email,
mock_login,
@@ -33,6 +34,7 @@ def _test_dashboard_menu(mocker, app_, usr, service, permissions):
mocker.patch('app.user_api_client.get_user', return_value=usr)
mocker.patch('app.user_api_client.get_user_by_email', return_value=usr)
mocker.patch('app.service_api_client.get_service', return_value={'data': service})
mocker.patch('app.statistics_api_client.get_statistics_for_service', return_value={'data': [{}]})
client.login(usr)
return client.get(url_for('main.service_dashboard', service_id=service['id']))
+42
View File
@@ -80,3 +80,45 @@ def test_should_show_updates_for_one_job_as_json(
assert 'Recipient' in content['notifications']
assert 'Status' in content['notifications']
assert 'Started' in content['status']
def test_should_show_notifications_for_a_service(app_,
service_one,
api_user_active,
mock_login,
mock_get_user,
mock_get_user_by_email,
mock_get_service,
mock_get_notifications):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
response = client.get(url_for('main.view_notifications', service_id=service_one['id']))
assert response.status_code == 200
content = response.get_data(as_text=True)
notifications = mock_get_notifications(service_one['id'])
notification = notifications['notifications'][0]
assert notification['to'] in content
assert notification['status'] in content
assert notification['template']['name'] in content
assert '.csv' in content
def test_should_show_notifications_for_a_service_with_next_previous(app_,
service_one,
api_user_active,
mock_login,
mock_get_user,
mock_get_user_by_email,
mock_get_service,
mock_get_notifications_with_previous_next):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
response = client.get(url_for('main.view_notifications', service_id=service_one['id'], page=2))
assert response.status_code == 200
content = response.get_data(as_text=True)
assert url_for('main.view_notifications', service_id=service_one['id'], page=3) in content
assert url_for('main.view_notifications', service_id=service_one['id'], page=1) in content
assert 'Previous page' in content
assert 'Next page' in content
+34 -20
View File
@@ -1,4 +1,8 @@
from flask import url_for
from flask import (
url_for,
session
)
from bs4 import BeautifulSoup
@@ -24,24 +28,31 @@ def test_logged_in_user_redirects_to_choose_service(app_,
assert response.location == url_for('main.choose_service', _external=True)
def test_process_register_creates_new_user(app_,
mock_send_verify_code,
mock_register_user,
mock_get_user_by_email_not_found,
mock_login):
user_data = {
'name': 'Some One Valid',
'email_address': 'notfound@example.gov.uk',
'mobile_number': '+4407700900460',
'password': 'validPassword!'
}
def test_register_creates_new_user_and_redirects_to_continue_page(app_,
mock_send_verify_code,
mock_register_user,
mock_get_user_by_email_not_found,
mock_is_email_unique,
mock_send_verify_email,
mock_login):
user_data = {'name': 'Some One Valid',
'email_address': 'notfound@example.gov.uk',
'mobile_number': '+4407700900460',
'password': 'validPassword!'
}
with app_.test_request_context():
response = app_.test_client().post(url_for('main.register'),
data=user_data)
response = app_.test_client().post(url_for('main.register'), data=user_data)
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
assert mock_register_user.called
assert response.location == url_for('main.registration_continue', _external=True)
from unittest.mock import ANY
mock_send_verify_email.assert_called_with(ANY, user_data['email_address'])
mock_register_user.assert_called_with(user_data['name'],
user_data['email_address'],
user_data['mobile_number'],
user_data['password'])
def test_process_register_returns_200_when_mobile_number_is_invalid(app_,
@@ -59,7 +70,7 @@ def test_process_register_returns_200_when_mobile_number_is_invalid(app_,
assert 'Must not contain letters or symbols' in response.get_data(as_text=True)
def test_should_return_400_when_email_is_not_gov_uk(app_,
def test_should_return_200_when_email_is_not_gov_uk(app_,
mock_send_verify_code,
mock_get_user_by_email,
mock_login):
@@ -74,11 +85,13 @@ def test_should_return_400_when_email_is_not_gov_uk(app_,
assert 'Enter a gov.uk email address' in response.get_data(as_text=True)
def test_should_add_verify_codes_on_session(app_,
def test_should_add_user_details_to_session(app_,
mock_send_verify_code,
mock_register_user,
mock_get_user,
mock_get_user_by_email_not_found,
mock_is_email_unique,
mock_send_verify_email,
mock_login):
user_data = {
'name': 'Test Codes',
@@ -91,11 +104,12 @@ def test_should_add_verify_codes_on_session(app_,
with app_.test_client() as client:
response = client.post(url_for('main.register'),
data=user_data)
assert response.status_code == 302
assert 'notify_admin_session' in response.headers.get('Set-Cookie')
assert session['user_details']['email'] == user_data['email_address']
def test_should_return_400_if_password_is_blacklisted(app_,
def test_should_return_200_if_password_is_blacklisted(app_,
mock_get_user_by_email,
mock_login):
with app_.test_request_context():
+1
View File
@@ -16,6 +16,7 @@ def test_sign_out_user(app_,
mock_get_user,
mock_get_user_by_email,
mock_get_service_templates,
mock_get_service_statistics,
mock_login,
mock_get_jobs,
mock_has_permissions):
+82 -78
View File
@@ -2,105 +2,109 @@ import json
from flask import url_for
def test_should_show_overview_page(app_,
api_user_active,
mock_login,
mock_get_user):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
response = client.get(url_for('main.user_profile'))
# def test_should_show_overview_page(app_,
# api_user_active,
# mock_login,
# mock_get_user):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# response = client.get(url_for('main.user_profile'))
assert 'Your profile' in response.get_data(as_text=True)
assert response.status_code == 200
# assert 'Your profile' in response.get_data(as_text=True)
# assert response.status_code == 200
def test_should_show_name_page(app_,
api_user_active,
mock_login,
mock_get_user):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
response = client.get(url_for('main.user_profile_name'))
# def test_should_show_name_page(app_,
# api_user_active,
# mock_login,
# mock_get_user):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# response = client.get(url_for('main.user_profile_name'))
assert 'Change your name' in response.get_data(as_text=True)
assert response.status_code == 200
# assert 'Change your name' in response.get_data(as_text=True)
# assert response.status_code == 200
def test_should_redirect_after_name_change(app_,
api_user_active,
mock_login,
mock_update_user,
mock_get_user):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
new_name = 'New Name'
data = {'new_name': new_name}
response = client.post(url_for(
'main.user_profile_name'), data=data)
# def test_should_redirect_after_name_change(app_,
# api_user_active,
# mock_login,
# mock_update_user,
# mock_get_user):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# new_name = 'New Name'
# data = {'new_name': new_name}
# response = client.post(url_for(
# 'main.user_profile_name'), data=data)
assert response.status_code == 302
assert response.location == url_for(
'main.user_profile', _external=True)
api_user_active.name = new_name
assert mock_update_user.called
# assert response.status_code == 302
# assert response.location == url_for(
# 'main.user_profile', _external=True)
# api_user_active.name = new_name
# assert mock_update_user.called
def test_should_show_email_page(app_,
api_user_active,
mock_login,
mock_get_user):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
response = client.get(url_for(
'main.user_profile_email'))
# def test_should_show_email_page(app_,
# api_user_active,
# mock_login,
# mock_get_user):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# response = client.get(url_for(
# 'main.user_profile_email'))
assert 'Change your email address' in response.get_data(as_text=True)
assert response.status_code == 200
# assert 'Change your email address' in response.get_data(as_text=True)
# assert response.status_code == 200
def test_should_redirect_after_email_change(app_,
api_user_active,
mock_login,
mock_get_user,
mock_get_user_by_email_not_found):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
data = {'email_address': 'new_notify@notify.gov.uk'}
response = client.post(
url_for('main.user_profile_email'),
data=data)
# def test_should_redirect_after_email_change(app_,
# api_user_active,
# mock_login,
# mock_get_user,
# mock_get_user_by_email_not_found,
# mock_is_email_unique):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# data = {'email_address': 'new_notify@notify.gov.uk'}
# response = client.post(
# url_for('main.user_profile_email'),
# data=data)
assert response.status_code == 302
assert response.location == url_for(
'main.user_profile_email_authenticate', _external=True)
# assert response.status_code == 302
# assert response.location == url_for(
# 'main.user_profile_email_authenticate', _external=True)
def test_should_show_authenticate_after_email_change(app_,
api_user_active,
mock_login,
mock_get_user,
mock_verify_password):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
with client.session_transaction() as session:
session['new-email'] = 'new_notify@notify.gov.uk'
response = client.get(url_for('main.user_profile_email_authenticate'))
# def test_should_show_authenticate_after_email_change(app_,
# api_user_active,
# mock_login,
# mock_get_user,
# mock_verify_password):
# with app_.test_request_context():
# with app_.test_client() as client:
# client.login(api_user_active)
# with client.session_transaction() as session:
# session['new-email'] = 'new_notify@notify.gov.uk'
# response = client.get(url_for('main.user_profile_email_authenticate'))
assert 'Change your email address' in response.get_data(as_text=True)
assert 'Confirm' in response.get_data(as_text=True)
assert response.status_code == 200
# assert 'Change your email address' in response.get_data(as_text=True)
# assert 'Confirm' in response.get_data(as_text=True)
# assert response.status_code == 200
def test_should_redirect_after_email_change_confirm(app_,
api_user_active,
mock_login,
mock_get_user):
mock_get_user,
mock_verify_password,
mock_send_verify_code,
mock_is_email_unique):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
+22 -42
View File
@@ -1,5 +1,7 @@
from flask import url_for
from bs4 import BeautifulSoup
def test_should_return_verify_template(app_,
api_user_active,
@@ -12,26 +14,29 @@ def test_should_return_verify_template(app_,
session['user_details'] = {'email_address': api_user_active.email_address, 'id': api_user_active.id}
response = client.get(url_for('main.verify'))
assert response.status_code == 200
assert (
"Weve sent you confirmation codes by email and text message."
) in response.get_data(as_text=True)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.h1.text == 'Text verification'
message = page.find_all('p')[1].text
assert message == "We've sent you a text message with a verification code."
def test_should_redirect_to_add_service_when_code_are_correct(app_,
api_user_active,
mock_get_user,
mock_update_user,
mock_check_verify_code):
def test_should_redirect_to_add_service_when_sms_code_is_correct(app_,
api_user_active,
mock_get_user,
mock_update_user,
mock_check_verify_code):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {'email_address': api_user_active.email_address, 'id': api_user_active.id}
response = client.post(url_for('main.verify'),
data={'sms_code': '12345',
'email_code': '23456'})
data={'sms_code': '12345'})
assert response.status_code == 302
assert response.location == url_for('main.add_service', first='first', _external=True)
mock_check_verify_code.assert_called_once_with(api_user_active.id, '12345', 'sms')
def test_should_activate_user_after_verify(app_,
api_user_active,
@@ -44,45 +49,20 @@ def test_should_activate_user_after_verify(app_,
with client.session_transaction() as session:
session['user_details'] = {'email_address': api_user_active.email_address, 'id': api_user_active.id}
client.post(url_for('main.verify'),
data={'sms_code': '12345',
'email_code': '23456'})
data={'sms_code': '12345'})
assert mock_update_user.called
def test_should_return_200_when_codes_are_wrong(app_,
api_user_active,
mock_get_user,
mock_check_verify_code_code_not_found):
def test_should_return_200_when_sms_code_is_wrong(app_,
api_user_active,
mock_get_user,
mock_check_verify_code_code_not_found):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {'email_address': api_user_active.email_address, 'id': api_user_active.id}
response = client.post(url_for('main.verify'),
data={'sms_code': '12345',
'email_code': '23456'})
data={'sms_code': '12345'})
assert response.status_code == 200
resp_data = response.get_data(as_text=True)
assert resp_data.count('Code not found') == 2
def test_should_only_check_codes_in_validation_if_both_are_present(app_,
api_user_active,
mock_get_user,
mock_update_user,
mock_check_verify_code):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {'email_address': api_user_active.email_address, 'id': api_user_active.id}
response = client.post(url_for('main.verify'), data={'sms_code': '12345'})
assert response.status_code == 200
assert not mock_check_verify_code.called
response = client.post(url_for('main.verify'), data={'email_code': '12345'})
assert response.status_code == 200
assert not mock_check_verify_code.called
response = client.post(url_for('main.verify'), data={'sms_code': '12345', 'email_code': '12345'})
assert response.status_code == 302
assert mock_check_verify_code.called
assert mock_check_verify_code.call_count == 2
assert resp_data.count('Code not found') == 1
+47 -32
View File
@@ -2,7 +2,9 @@ import uuid
from datetime import date, datetime, timedelta
from unittest.mock import Mock
import pytest
from app import create_app
from . import (
service_json,
TestClient,
@@ -16,12 +18,13 @@ from app.notify_client.models import (
User,
InvitedUser
)
from notifications_python_client.errors import HTTPError
@pytest.fixture(scope='session')
def app_(request):
app = create_app('test')
app = create_app()
ctx = app.app_context()
ctx.push()
@@ -144,11 +147,20 @@ def mock_delete_service(mocker, mock_get_service):
'app.service_api_client.delete_service', side_effect=_delete)
@pytest.fixture(scope='function')
def mock_get_service_statistics(mocker):
def _create(service_id):
return {'data': [{}]}
return mocker.patch(
'app.statistics_api_client.get_statistics_for_service', side_effect=_create)
@pytest.fixture(scope='function')
def mock_get_service_template(mocker):
def _create(service_id, template_id):
template = template_json(
template_id, "Two week reminder", "sms", "Your vehicle tax is about to expire", service_id)
service_id, template_id, "Two week reminder", "sms", "Your vehicle tax is about to expire")
return {'data': template}
return mocker.patch(
@@ -159,7 +171,7 @@ def mock_get_service_template(mocker):
def mock_get_service_email_template(mocker):
def _create(service_id, template_id):
template = template_json(
template_id, "Two week reminder", "email", "Your vehicle tax is about to expire", service_id)
service_id, template_id, "Two week reminder", "email", "Your vehicle tax is about to expire")
return {'data': template}
return mocker.patch(
@@ -195,16 +207,16 @@ def mock_get_service_templates(mocker):
def _create(service_id):
return {'data': [
template_json(
1, "sms_template_one", "sms", "sms template one content", service_id
service_id, 1, "sms_template_one", "sms", "sms template one content"
),
template_json(
2, "sms_template_two", "sms", "sms template two content", service_id
service_id, 2, "sms_template_two", "sms", "sms template two content"
),
template_json(
3, "email_template_one", "email", "email template one content", service_id
service_id, 3, "email_template_one", "email", "email template one content"
),
template_json(
4, "email_template_two", "email", "email template two content", service_id
service_id, 4, "email_template_two", "email", "email template two content"
)
]}
@@ -217,8 +229,7 @@ def mock_get_service_templates(mocker):
def mock_delete_service_template(mocker):
def _delete(service_id, template_id):
template = template_json(
template_id, "Template to delete",
"sms", "content to be deleted", service_id)
service_id, template_id, "Template to delete", "sms", "content to be deleted")
return {'data': template}
return mocker.patch(
@@ -366,7 +377,6 @@ def mock_get_user(mocker, api_user_active):
def _get_user(id):
api_user_active.id = id
return api_user_active
return mocker.patch(
'app.user_api_client.get_user', side_effect=_get_user)
@@ -385,10 +395,10 @@ def mock_get_user_pending(mocker, api_user_pending):
@pytest.fixture(scope='function')
def mock_get_user_by_email(mocker, api_user_active):
def _get_user(email_address):
api_user_active._email_address = email_address
return api_user_active
return mocker.patch('app.user_api_client.get_user_by_email', side_effect=_get_user)
@@ -397,16 +407,15 @@ def mock_get_user_with_permissions(mocker, api_user_active):
def _get_user(id):
api_user_active._permissions[''] = ['manage_users', 'manage_templates', 'manage_settings']
return api_user_active
return mocker.patch(
'app.user_api_client.get_user', side_effect=_get_user)
@pytest.fixture(scope='function')
def mock_dont_get_user_by_email(mocker):
def _get_user(email_address):
return None
return mocker.patch(
'app.user_api_client.get_user_by_email',
side_effect=_get_user,
@@ -455,7 +464,6 @@ def mock_get_user_by_email_not_found(mocker):
def mock_verify_password(mocker):
def _verify_password(user, password):
return True
return mocker.patch(
'app.user_api_client.verify_password',
side_effect=_verify_password)
@@ -463,15 +471,20 @@ def mock_verify_password(mocker):
@pytest.fixture(scope='function')
def mock_update_user(mocker):
def _update(user):
return user
return mocker.patch('app.user_api_client.update_user', side_effect=_update)
@pytest.fixture(scope='function')
def mock_is_email_unique(mocker):
return mocker.patch('app.user_api_client.get_user_by_email', return_value=None)
return mocker.patch('app.user_api_client.is_email_unique', return_value=True)
@pytest.fixture(scope='function')
def mock_is_email_not_unique(mocker):
return mocker.patch('app.user_api_client.is_email_unique', return_value=False)
@pytest.fixture(scope='function')
@@ -481,6 +494,7 @@ def mock_get_all_users_from_api(mocker):
@pytest.fixture(scope='function')
def mock_create_api_key(mocker):
def _create(service_id, key_name):
import uuid
return {'data': str(uuid.uuid4())}
@@ -542,11 +556,15 @@ def mock_send_verify_code(mocker):
return mocker.patch('app.user_api_client.send_verify_code')
@pytest.fixture(scope='function')
def mock_send_verify_email(mocker):
return mocker.patch('app.user_api_client.send_verify_email')
@pytest.fixture(scope='function')
def mock_check_verify_code(mocker):
def _verify(user_id, code, code_type):
return True, ''
return mocker.patch(
'app.user_api_client.check_verify_code',
side_effect=_verify)
@@ -556,7 +574,6 @@ def mock_check_verify_code(mocker):
def mock_check_verify_code_code_not_found(mocker):
def _verify(user_id, code, code_type):
return False, 'Code not found'
return mocker.patch(
'app.user_api_client.check_verify_code',
side_effect=_verify)
@@ -566,7 +583,6 @@ def mock_check_verify_code_code_not_found(mocker):
def mock_check_verify_code_code_expired(mocker):
def _verify(user_id, code, code_type):
return False, 'Code has expired'
return mocker.patch(
'app.user_api_client.check_verify_code',
side_effect=_verify)
@@ -588,7 +604,6 @@ def mock_create_job(mocker, job_data):
job_data['file_name'] = '{}.csv'.format(job_id)
job_data['notification_count'] = notification_count
return job_data
return mocker.patch('app.job_api_client.create_job', side_effect=_create)
@@ -598,7 +613,6 @@ def mock_get_job(mocker, job_data):
job_data['id'] = job_id
job_data['service'] = service_id
return {"data": job_data}
return mocker.patch('app.job_api_client.get_job', side_effect=_get_job)
@@ -613,15 +627,23 @@ def mock_get_jobs(mocker):
job_data['service'] = service_id
data.append(job_data)
return {"data": data}
return mocker.patch('app.job_api_client.get_job', side_effect=_get_jobs)
@pytest.fixture(scope='function')
def mock_get_notifications(mocker):
def _get_notifications(service_id, job_id):
return notification_json()
def _get_notifications(service_id, job_id=None, page=1):
return notification_json(service_id)
return mocker.patch(
'app.notification_api_client.get_notifications_for_service',
side_effect=_get_notifications
)
@pytest.fixture(scope='function')
def mock_get_notifications_with_previous_next(mocker):
def _get_notifications(service_id, job_id=None, page=1):
return notification_json(service_id, with_links=True)
return mocker.patch(
'app.notification_api_client.get_notifications_for_service',
side_effect=_get_notifications
@@ -632,7 +654,6 @@ def mock_get_notifications(mocker):
def mock_has_permissions(mocker):
def _has_permission(permissions=None, or_=False, admin_override=False):
return True
return mocker.patch(
'app.notify_client.user_api_client.User.has_permissions',
side_effect=_has_permission)
@@ -658,7 +679,6 @@ def mock_get_users_by_service(mocker):
'email_address': 'notify@digital.cabinet-office.gov.uk',
'failed_login_count': 0}]
return [User(data[0])]
return mocker.patch('app.user_api_client.get_users_for_service', side_effect=_get_users_for_service, autospec=True)
@@ -666,7 +686,6 @@ def mock_get_users_by_service(mocker):
def mock_s3_upload(mocker):
def _upload(upload_id, service_id, filedata, region):
pass
return mocker.patch('app.main.views.send.s3upload', side_effect=_upload)
@@ -689,6 +708,7 @@ def sample_invited_user(mocker, sample_invite):
@pytest.fixture(scope='function')
def mock_create_invite(mocker, sample_invite):
def _create_invite(from_user, service_id, email_address, permissions):
sample_invite['from_user'] = from_user
sample_invite['service'] = service_id
@@ -696,7 +716,6 @@ def mock_create_invite(mocker, sample_invite):
sample_invite['status'] = 'pending'
sample_invite['permissions'] = permissions
return InvitedUser(**sample_invite)
return mocker.patch('app.invite_api_client.create_invite', side_effect=_create_invite)
@@ -711,7 +730,6 @@ def mock_get_invites_for_service(mocker, service_one, sample_invite):
invite['email_address'] = 'user_{}@testnotify.gov.uk'.format(i)
data.append(InvitedUser(**invite))
return data
return mocker.patch('app.invite_api_client.get_invites_for_service', side_effect=_get_invites)
@@ -719,7 +737,6 @@ def mock_get_invites_for_service(mocker, service_one, sample_invite):
def mock_check_invite_token(mocker, sample_invite):
def _check_token(token):
return InvitedUser(**sample_invite)
return mocker.patch('app.invite_api_client.check_token', side_effect=_check_token)
@@ -727,7 +744,6 @@ def mock_check_invite_token(mocker, sample_invite):
def mock_accept_invite(mocker, sample_invite):
def _accept(service_id, invite_id):
return InvitedUser(**sample_invite)
return mocker.patch('app.invite_api_client.accept_invite', side_effect=_accept)
@@ -735,7 +751,6 @@ def mock_accept_invite(mocker, sample_invite):
def mock_add_user_to_service(mocker, service_one, api_user_active):
def _add_user(service_id, user_id, permissions):
return api_user_active
return mocker.patch('app.user_api_client.add_user_to_service', side_effect=_add_user)
+13 -6
View File
@@ -1,17 +1,24 @@
from app import create_app
from credstash import getAllSecrets
import os
config = 'live'
default_env_file = '/home/ubuntu/environment'
environment = 'live'
if os.path.isfile(default_env_file):
environment = open(default_env_file, 'r')
config = environment.readline().strip()
with open(default_env_file, 'r') as environment_file:
environment = environment_file.readline().strip()
secrets = getAllSecrets(region="eu-west-1")
application = create_app(config, secrets)
# on aws get secrets and export to env
os.environ.update(getAllSecrets(region="eu-west-1"))
from config import configs
os.environ['NOTIFY_ADMIN_ENVIRONMENT'] = configs[environment]
from app import create_app
application = create_app()
if __name__ == "__main__":
application.run()