diff --git a/.travis.yml b/.travis.yml
index 50eecec26..030da3e0c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/app.py b/app.py
index 445f95cd5..1710069ca 100644
--- a/app.py
+++ b/app.py
@@ -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))
diff --git a/app/__init__.py b/app/__init__.py
index 48ba1ed19..c355f065f 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -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)
diff --git a/app/assets/images/tick-black.png b/app/assets/images/tick-black.png
new file mode 100644
index 000000000..dd050c6c7
Binary files /dev/null and b/app/assets/images/tick-black.png differ
diff --git a/app/assets/javascripts/highlightTags.js b/app/assets/javascripts/highlightTags.js
index a1ecf2f37..ca4359111 100644
--- a/app/assets/javascripts/highlightTags.js
+++ b/app/assets/javascripts/highlightTags.js
@@ -20,8 +20,9 @@
`))
- .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(
$('
').text(this.$textbox.val()).html().replace(
tagPattern, match => `${match} `
)
);
- this.maintainScrollParity = () => this.$backgroundMaskForeground.scrollTop(
- this.$textbox.scrollTop()
+ this.update = () => (
+ this.replacePlaceholders() && this.resize()
);
};
diff --git a/app/assets/stylesheets/_grids.scss b/app/assets/stylesheets/_grids.scss
index 490f4d01d..f0008d51e 100644
--- a/app/assets/stylesheets/_grids.scss
+++ b/app/assets/stylesheets/_grids.scss
@@ -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;
+ }
+}
diff --git a/app/assets/stylesheets/app.scss b/app/assets/stylesheets/app.scss
index d7eeacb62..d2d7fb838 100644
--- a/app/assets/stylesheets/app.scss
+++ b/app/assets/stylesheets/app.scss
@@ -80,3 +80,11 @@ a {
}
}
+
+td {
+ vertical-align: top;
+}
+
+.heading-xlarge {
+ margin-bottom: 20px;
+}
diff --git a/app/assets/stylesheets/components/_previous-next-navigation.scss b/app/assets/stylesheets/components/_previous-next-navigation.scss
new file mode 100644
index 000000000..a1cf5e0ea
--- /dev/null
+++ b/app/assets/stylesheets/components/_previous-next-navigation.scss
@@ -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;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss
index 884523bdd..540ce1e38 100644
--- a/app/assets/stylesheets/components/banner.scss
+++ b/app/assets/stylesheets/components/banner.scss
@@ -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;
+ }
+
+ }
+
+}
diff --git a/app/assets/stylesheets/components/big-number.scss b/app/assets/stylesheets/components/big-number.scss
index 0bce53b25..a14f30d9b 100644
--- a/app/assets/stylesheets/components/big-number.scss
+++ b/app/assets/stylesheets/components/big-number.scss
@@ -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;
+ }
+
}
}
diff --git a/app/assets/stylesheets/components/textbox.scss b/app/assets/stylesheets/components/textbox.scss
index efb692c27..703b3b8cf 100644
--- a/app/assets/stylesheets/components/textbox.scss
+++ b/app/assets/stylesheets/components/textbox.scss
@@ -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 {
diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss
index e9c36ad7a..74db12573 100644
--- a/app/assets/stylesheets/main.scss
+++ b/app/assets/stylesheets/main.scss
@@ -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';
diff --git a/app/main/dao/users_dao.py b/app/main/dao/users_dao.py
index 9b27d5839..2f1841cb3 100644
--- a/app/main/dao/users_dao.py
+++ b/app/main/dao/users_dao.py
@@ -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):
diff --git a/app/main/forms.py b/app/main/forms.py
index 640e993de..34bf5139f 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -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()
diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py
index 4ab30ed55..9a30f7782 100644
--- a/app/main/views/add_service.py
+++ b/app/main/views/add_service.py
@@ -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)
diff --git a/app/main/views/code_not_received.py b/app/main/views/code_not_received.py
index 41d071754..36fcfc8dc 100644
--- a/app/main/views/code_not_received.py
+++ b/app/main/views/code_not_received.py
@@ -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'))
diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py
index f681faa45..6fc17c49c 100644
--- a/app/main/views/dashboard.py
+++ b/app/main/views/dashboard.py
@@ -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//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
diff --git a/app/main/views/index.py b/app/main/views/index.py
index 5f7d700df..47780a1e7 100644
--- a/app/main/views/index.py
+++ b/app/main/views/index.py
@@ -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')
diff --git a/app/main/views/invites.py b/app/main/views/invites.py
index 251416fd0..e5bd43de9 100644
--- a/app/main/views/invites.py
+++ b/app/main/views/invites.py
@@ -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)
diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py
index 7bfe5247a..48ef1875c 100644
--- a/app/main/views/jobs.py
+++ b/app/main/views/jobs.py
@@ -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//jobs")
@@ -85,6 +91,34 @@ def view_job_updates(service_id, job_id):
})
+@main.route('/services//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//jobs//notification/")
@login_required
def view_notification(service_id, job_id, notification_id):
diff --git a/app/main/views/manage_users.py b/app/main/views/manage_users.py
index 19c908d2e..cb77fb625 100644
--- a/app/main/views/manage_users.py
+++ b/app/main/views/manage_users.py
@@ -3,7 +3,8 @@ from flask import (
render_template,
redirect,
url_for,
- flash)
+ flash
+)
from flask_login import (
login_required,
diff --git a/app/main/views/register.py b/app/main/views/register.py
index 2893fe8b6..9a0f4c816 100644
--- a/app/main/views/register.py
+++ b/app/main/views/register.py
@@ -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')
diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py
index 9121b3b9b..7cad45b81 100644
--- a/app/main/views/sign_in.py
+++ b/app/main/views/sign_in.py
@@ -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
diff --git a/app/main/views/user_profile.py b/app/main/views/user_profile.py
index 394baf775..beb57e2e4 100644
--- a/app/main/views/user_profile.py
+++ b/app/main/views/user_profile.py
@@ -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)
diff --git a/app/main/views/verify.py b/app/main/views/verify.py
index 6bc4c3e2c..df0996960 100644
--- a/app/main/views/verify.py
+++ b/app/main/views/verify.py
@@ -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/')
+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'))
diff --git a/app/notify_client/statistics_api_client.py b/app/notify_client/statistics_api_client.py
new file mode 100644
index 000000000..9ef40bbd6
--- /dev/null
+++ b/app/notify_client/statistics_api_client.py
@@ -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),
+ )
diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py
index 4ffc8456c..2cb970fba 100644
--- a/app/notify_client/user_api_client.py
+++ b/app/notify_client/user_api_client.py
@@ -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)
diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html
index bd1eba398..666add0bb 100644
--- a/app/templates/admin_template.html
+++ b/app/templates/admin_template.html
@@ -24,6 +24,10 @@
{% endblock %}
{% block cookie_message %}
+
+ GOV.UK Notify uses cookies to make the site simpler.
+ Find out more about cookies
+
{% endblock %}
{% block inside_header %}
@@ -68,6 +72,15 @@
{% endblock %}
+{% block footer_support_links %}
+
+{% endblock %}
+
{% block body_end %}
{% endblock %}
diff --git a/app/templates/components/banner.html b/app/templates/components/banner.html
index 0c0ef7abc..d008b5b57 100644
--- a/app/templates/components/banner.html
+++ b/app/templates/components/banner.html
@@ -1,19 +1,9 @@
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None) %}
- {% if subhead %}
-
-
- {{ subhead }}
-
-
- {% endif %}
-
+ {% if subhead -%}
+ {{ subhead }}
+ {%- endif -%}
{{ body }}
-
- {% if subhead %}
-
-
- {% endif %}
{% if delete_button %}
{% endmacro %}
+
+
+{% macro big_number_with_status(number, label, status_number, status_label, percentage_bad=0) %}
+
+ {{ big_number(number, label) }}
+
+
+ {{ big_number(status_number, status_label) }}
+
+
+{% endmacro %}
diff --git a/app/templates/components/previous-next-navigation.html b/app/templates/components/previous-next-navigation.html
new file mode 100644
index 000000000..9704ea939
--- /dev/null
+++ b/app/templates/components/previous-next-navigation.html
@@ -0,0 +1,22 @@
+{% macro previous_next_navigation(previous_page, next_page) %}
+
+
+ {% if previous_page %}
+
+
+
+
+
+
+ {% endif %}
+ {% if next_page %}
+
+
+
+
+
+
+ {% endif %}
+
+
+{% endmacro %}
\ No newline at end of file
diff --git a/app/templates/components/sms-message.html b/app/templates/components/sms-message.html
index edd64887f..04d334d97 100644
--- a/app/templates/components/sms-message.html
+++ b/app/templates/components/sms-message.html
@@ -4,7 +4,7 @@
{% if name %}
{% if edit_link %}
- {{ name }}
+ {{ name }}
{% else %}
{{ name }}
{% endif %}
diff --git a/app/templates/main_nav.html b/app/templates/main_nav.html
index e5be21e9e..dbfdca040 100644
--- a/app/templates/main_nav.html
+++ b/app/templates/main_nav.html
@@ -2,29 +2,34 @@
- {% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']) %}
+ {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
- {% elif current_user.has_permissions(permissions=['manage_templates'], admin_override=True) %}
+ {% elif current_user.has_permissions(['manage_templates']) %}
{% endif %}
- {% if current_user.has_permissions(permissions=['manage_users', 'manage_settings'], admin_override=True) %}
+ {% if current_user.has_permissions(['manage_users', 'manage_settings']) %}
{% endif %}
- {% if current_user.has_permissions(permissions=['manage_api_keys', 'access_developer_docs']) %}
+ {% if current_user.has_permissions(['manage_api_keys', 'access_developer_docs']) %}
{% endif %}
+
+
{% if current_user.has_permissions(admin_override=True) %}
List all services
diff --git a/app/templates/views/choose-template.html b/app/templates/views/choose-template.html
index 4a1f3b946..9cb126643 100644
--- a/app/templates/views/choose-template.html
+++ b/app/templates/views/choose-template.html
@@ -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 %}
diff --git a/app/templates/views/cookies.html b/app/templates/views/cookies.html
new file mode 100644
index 000000000..6337c1f77
--- /dev/null
+++ b/app/templates/views/cookies.html
@@ -0,0 +1,81 @@
+{% extends "withoutnav_template.html" %}
+
+{% block page_title %}
+ Cookies – GOV.UK Notify
+{% endblock %}
+
+{% block maincolumn_content %}
+
+
+
+
Cookies
+
+ GOV.UK Notify puts small files (known as ‘cookies’)
+ on to your computer.
+
+
These cookies are used to remember you once you’ve logged in
+
+ Find out how to manage cookies .
+
+
+
Session cookies
+
+ We store session cookies on your computer to help keep your information
+ secure while you use the service.
+
+
+
+
+ Name
+ Purpose
+ Expires
+
+
+
+
+
+ notify_admin_session
+
+
+ Used to keep you logged in
+
+
+ 1 hour
+
+
+
+
+
+
Introductory message cookie
+
+ When you first use the service, you may see a pop-up ‘welcome’ message.
+ Once you’ve seen the message, we store a cookie on your computer so it
+ knows not to show it again.
+
+
+
+
+ Name
+ Purpose
+ Expires
+
+
+
+
+
+ seen_cookie_message
+
+
+ Saves a message to let us know that you have seen our cookie
+ message
+
+
+ 1 month
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html
new file mode 100644
index 000000000..67fb31a53
--- /dev/null
+++ b/app/templates/views/dashboard/dashboard.html
@@ -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 %}
diff --git a/app/templates/views/dashboard/get-started.html b/app/templates/views/dashboard/get-started.html
new file mode 100644
index 000000000..d1b8adc01
--- /dev/null
+++ b/app/templates/views/dashboard/get-started.html
@@ -0,0 +1,19 @@
+{% from "components/banner.html" import banner_wrapper %}
+
+Get started
+
+ {% if current_user.has_permissions(['manage_templates']) %}
+
+ {% call banner_wrapper(type="tip", subhead='1.' if not templates else None, with_tick=templates|length) %}
+ Add a template
+ {% endcall %}
+
+ {% endif %}
+ {% if current_user.has_permissions(['send_texts', 'send_emails', 'send_letters']) %}
+
+ {% call banner_wrapper(type="tip", subhead='2.') %}
+ Send yourself a message
+ {% endcall %}
+
+ {% endif %}
+
diff --git a/app/templates/views/dashboard/jobs.html b/app/templates/views/dashboard/jobs.html
new file mode 100644
index 000000000..941bb51cc
--- /dev/null
+++ b/app/templates/views/dashboard/jobs.html
@@ -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 haven’t sent any text messages yet',
+ field_headings=['File', 'Started', right_aligned_field_heading('Rows'), right_aligned_field_heading('Sent')]
+) %}
+ {% call field() %}
+ {{ item.original_file_name }}
+ {% 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']) %}
+
+ See all sent text messages
+
+ {% endif %}
+{% endif %}
diff --git a/app/templates/views/dashboard/today.html b/app/templates/views/dashboard/today.html
new file mode 100644
index 000000000..bc1d13abf
--- /dev/null
+++ b/app/templates/views/dashboard/today.html
@@ -0,0 +1,25 @@
+{% from "components/big-number.html" import big_number_with_status %}
+
+
+ Sent today
+
+
+
+ {{ 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)
+ ) }}
+
+
+ {{ 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)
+ ) }}
+
+
diff --git a/app/templates/views/dashboard/trial-mode-banner.html b/app/templates/views/dashboard/trial-mode-banner.html
new file mode 100644
index 000000000..3a3c34829
--- /dev/null
+++ b/app/templates/views/dashboard/trial-mode-banner.html
@@ -0,0 +1,25 @@
+{% from "components/banner.html" import banner_wrapper %}
+{% from "components/big-number.html" import big_number %}
+
+{% call banner_wrapper(type="mode") %}
+
+ Trial mode
+
+
+
+ We’ll only deliver messages to you and members of your team
+
+ Find out more
+
+
+
+
+
+
+ {{ big_number(
+ service.limit - statistics.get('emails_requested', 0) - statistics.get('sms_requested', 0),
+ 'messages left today'
+ ) }}
+
+
+{% endcall %}
diff --git a/app/templates/views/email-not-received.html b/app/templates/views/email-not-received.html
deleted file mode 100644
index 5b98d330a..000000000
--- a/app/templates/views/email-not-received.html
+++ /dev/null
@@ -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 %}
-
-
-
-
Check your email address
-
-
Check your email address is correct and then resend the confirmation code.
-
-
-
-
-
-{% endblock %}
diff --git a/app/templates/views/help.html b/app/templates/views/help.html
new file mode 100644
index 000000000..72ef617b3
--- /dev/null
+++ b/app/templates/views/help.html
@@ -0,0 +1,79 @@
+{% extends "withoutnav_template.html" %}
+
+{% block page_title %}
+ Cookies – GOV.UK Notify
+{% endblock %}
+
+{% block maincolumn_content %}
+
+
+
+
Help
+
+
Trial mode
+
+
+ When you create a service on GOV.UK Notify, you start off in trial
+ mode. This means:
+
+
+
+ you can only send messages to yourself and your team members
+
+
+ you can only send 50 messages per day
+
+
+
+
+ Anyone working in central government can create an account on
+ GOV.UK Notify and try it out. So it’s important that you start off
+ with some restrictions in place.
+
+
+
To remove these restrictions
+
+
+ You need to request to go live:
+
+
+
+ You’ll need to agree to our terms of use
+
+
+ If you plan to send more than 250,000 text messages per year, you’ll
+ need to agree to pay for what you use – take a look at our pricing
+
+
+ We’ll check your templates to make sure they’re consistent with our
+ design patterns, style guide and information security principles
+
+
+
+
+ All other aspects of GOV.UK Notify are exactly the same
+
+
+
+ You can still:
+
+
+
+ upload a batch of recipients
+
+
+ do an API integration
+
+
+
+
+ We’ll send the messages you have permission to send. The messages you
+ don’t have permission to send will still be listed on your
+ GOV.UK Notify dashboard but won’t actually get sent. This means you
+ can check that everything is fully working without accidentally sending
+ hundreds of text messages or emails.
+
+
+
+
+{% endblock %}
diff --git a/app/templates/views/notifications.html b/app/templates/views/notifications.html
new file mode 100644
index 000000000..4a2ce4708
--- /dev/null
+++ b/app/templates/views/notifications.html
@@ -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 %}
+
+ Notifications activity
+
+ {% call(item) list_table(
+ notifications,
+ caption="Recent activity",
+ caption_visible=False,
+ empty_message='You haven’t sent any notifications yet',
+ field_headings=['Recipient', 'Template', 'Type', 'Job', 'Status', 'Time'])
+ %}
+ {% call field() %}
+ {{ item.to }}
+ {% endcall %}
+ {% call field() %}
+ {{ item.template.name }}
+ {% endcall %}
+ {% call field() %}
+ {{ item.template.template_type }}
+ {% endcall %}
+ {% call field() %}
+ {% if item.job %}
+ {{ item.job.original_file_name }}
+ {% endif %}
+ {% endcall %}
+ {% call field() %}
+ {{ item.status }}
+ {% endcall %}
+ {% call field() %}
+ {{ item.created_at | format_datetime}}
+ {% endcall %}
+ {% endcall %}
+ {{ previous_next_navigation(prev_page, next_page) }}
+{% endblock %}
\ No newline at end of file
diff --git a/app/templates/views/registration-continue.html b/app/templates/views/registration-continue.html
new file mode 100644
index 000000000..ae042f32c
--- /dev/null
+++ b/app/templates/views/registration-continue.html
@@ -0,0 +1,16 @@
+{% extends "withoutnav_template.html" %}
+
+{% block page_title %}
+ Check your email – GOV.UK Notify
+{% endblock %}
+
+{% block maincolumn_content %}
+
+
+
+
Now check your email
+
Click the link in the email we’ve sent you to continue your registration
+
+
+
+{% endblock %}
diff --git a/app/templates/views/resend-email-verification.html b/app/templates/views/resend-email-verification.html
new file mode 100644
index 000000000..28387c233
--- /dev/null
+++ b/app/templates/views/resend-email-verification.html
@@ -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 %}
+
+
+
+
Check your email
+
In order to verify your email address we've sent a new confirmation link to {{email}}
+
+
+
+{% endblock %}
diff --git a/app/templates/views/service_dashboard.html b/app/templates/views/service_dashboard.html
index 440aaa21a..1c235dc91 100644
--- a/app/templates/views/service_dashboard.html
+++ b/app/templates/views/service_dashboard.html
@@ -65,7 +65,7 @@
{% if more_jobs_to_show %}
{% if current_user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']) %}
- See all sent text messages
+ See all sent text messages
{% endif %}
{% endif %}
diff --git a/app/templates/views/verify.html b/app/templates/views/verify.html
deleted file mode 100644
index e34285720..000000000
--- a/app/templates/views/verify.html
+++ /dev/null
@@ -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 we’ve sent you – GOV.UK Notify
- {% else %}
- Enter the code we’ve sent you – GOV.UK Notify
- {% endif %}
-{% endblock %}
-
-{% block maincolumn_content %}
-
-
-
- {% if form.email_code %}
-
Enter the codes we’ve sent you
- {% else %}
-
Enter the code we’ve sent you
- {% endif %}
- {% if form.email_code %}
-
- We’ve sent you confirmation codes by email and text message.
-
- {% else %}
-
- We’ve sent you a confirmation code by text message.
-
- {% endif %}
-
-
-
-
-{% endblock %}
diff --git a/app/utils.py b/app/utils.py
index 2a7bb0b3e..b8c023744 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -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
+ }
diff --git a/config.py b/config.py
index bb520a3ae..1466096f9 100644
--- a/config.py
+++ b/config.py
@@ -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'
}
diff --git a/config_live.py b/config_live.py
new file mode 100644
index 000000000..5a3b64116
--- /dev/null
+++ b/config_live.py
@@ -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')
diff --git a/config_staging.py b/config_staging.py
new file mode 100644
index 000000000..5ce68b304
--- /dev/null
+++ b/config_staging.py
@@ -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')
diff --git a/scripts/run_app.sh b/scripts/run_app.sh
index 96947cc19..9236e8642 100755
--- a/scripts/run_app.sh
+++ b/scripts/run_app.sh
@@ -1,3 +1,4 @@
#!/bin/bash
+export NOTIFY_ADMIN_ENVIRONMENT='config.Development'
python3 app.py runserver
diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh
index af5971aa8..bc80a179e 100755
--- a/scripts/run_tests.sh
+++ b/scripts/run_tests.sh
@@ -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"
diff --git a/tests/__init__.py b/tests/__init__.py
index 68d2afc62..8d250a4c5 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -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
diff --git a/tests/app/main/notify_client/test_statistics_client.py b/tests/app/main/notify_client/test_statistics_client.py
new file mode 100644
index 000000000..3dc24c16a
--- /dev/null
+++ b/tests/app/main/notify_client/test_statistics_client.py
@@ -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)
diff --git a/tests/app/main/test_verify_form.py b/tests/app/main/test_verify_form.py
deleted file mode 100644
index 51bcf4b46..000000000
--- a/tests/app/main/test_verify_form.py
+++ /dev/null
@@ -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)
diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py
index e38f150ec..4e922bf76 100644
--- a/tests/app/main/views/test_accept_invite.py
+++ b/tests/app/main/views/test_accept_invite.py
@@ -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)
diff --git a/tests/app/main/views/test_code_not_received.py b/tests/app/main/views/test_code_not_received.py
index 22297255c..92be74682 100644
--- a/tests/app/main/views/test_code_not_received.py
+++ b/tests/app/main/views/test_code_not_received.py
@@ -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,
diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py
index 1362c08d5..3d1e9e77f 100644
--- a/tests/app/main/views/test_dashboard.py
+++ b/tests/app/main/views/test_dashboard.py
@@ -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']))
diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py
index 2b8d4e794..dcea08bfd 100644
--- a/tests/app/main/views/test_jobs.py
+++ b/tests/app/main/views/test_jobs.py
@@ -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
diff --git a/tests/app/main/views/test_register.py b/tests/app/main/views/test_register.py
index 9849da99f..3e042b6e6 100644
--- a/tests/app/main/views/test_register.py
+++ b/tests/app/main/views/test_register.py
@@ -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():
diff --git a/tests/app/main/views/test_sign_out.py b/tests/app/main/views/test_sign_out.py
index 8717684cf..d3b099f58 100644
--- a/tests/app/main/views/test_sign_out.py
+++ b/tests/app/main/views/test_sign_out.py
@@ -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):
diff --git a/tests/app/main/views/test_user_profile.py b/tests/app/main/views/test_user_profile.py
index 4ba3beb09..e73444b8e 100644
--- a/tests/app/main/views/test_user_profile.py
+++ b/tests/app/main/views/test_user_profile.py
@@ -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)
diff --git a/tests/app/main/views/test_verify.py b/tests/app/main/views/test_verify.py
index d149ba362..fd76e5cd6 100644
--- a/tests/app/main/views/test_verify.py
+++ b/tests/app/main/views/test_verify.py
@@ -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 (
- "We’ve 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
diff --git a/tests/conftest.py b/tests/conftest.py
index d5391c2ff..d93a14ebf 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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)
diff --git a/wsgi.py b/wsgi.py
index fd773b3e0..dd2db3d10 100644
--- a/wsgi.py
+++ b/wsgi.py
@@ -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()