From b303e06ca09a85979522d5125dcdd4cb8dfa7b92 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Mon, 11 Jan 2016 11:13:06 +0000
Subject: [PATCH 01/10] Add a styleguide page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
There are now quite a few frontend components in the app.
It’s good to have a reference for them to:
- document for developers what patterns are available and how they are used
- check for regressions
- when working on one variation of a pattern you can check that your changes
don’t break other variations of it
- when changing the arguments to a pattern you can check that this doesn’t
change the expected arguments already in use
This commit adds a single page (`/_styleguide`) which has examples of all the
patterns.
---
app/main/__init__.py | 2 +-
app/main/views/styleguide.py | 7 +
app/templates/views/styleguide.html | 162 ++++++++++++++++++++++++
tests/app/main/views/test_styleguide.py | 4 +
4 files changed, 174 insertions(+), 1 deletion(-)
create mode 100644 app/main/views/styleguide.py
create mode 100644 app/templates/views/styleguide.html
create mode 100644 tests/app/main/views/test_styleguide.py
diff --git a/app/main/__init__.py b/app/main/__init__.py
index 3d3ab5cde..578ea5e92 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -4,5 +4,5 @@ main = Blueprint('main', __name__)
from app.main.views import (
index, sign_in, sign_out, register, two_factor, verify, sms, add_service,
- code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password
+ code_not_received, jobs, dashboard, templates, service_settings, forgot_password, new_password, styleguide
)
diff --git a/app/main/views/styleguide.py b/app/main/views/styleguide.py
new file mode 100644
index 000000000..516a39b6c
--- /dev/null
+++ b/app/main/views/styleguide.py
@@ -0,0 +1,7 @@
+from flask import render_template
+from app.main import main
+
+
+@main.route('/_styleguide')
+def styleguide():
+ return render_template('views/styleguide.html')
diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html
new file mode 100644
index 000000000..9ad7ba9a3
--- /dev/null
+++ b/app/templates/views/styleguide.html
@@ -0,0 +1,162 @@
+{% extends "admin_template.html" %}
+
+{% from "components/banner.html" import banner %}
+{% from "components/big-number.html" import big_number %}
+{% from "components/browse-list.html" import browse_list %}
+{% from "components/page-footer.html" import page_footer %}
+{% from "components/sms-message.html" import sms_message, message_status %}
+{% from "components/table.html" import mapping_table, list_table, row, field %}
+{% from "components/textbox.html" import textbox %}
+
+{% block page_title %}
+ Styleguide – GOV.UK Notify
+{% endblock %}
+
+{% block fullwidth_content %}
+
+
+ Styleguide
+
+
+ Banner
+ Used to show the result of a user’s action.
+ {{ banner("This is a banner") }}
+
+ Big number
+
+ Used to show some important statistics.
+
+
+
+ {{ big_number("567") }}
+
+
+ {{ big_number("2", "Messages delivered") }}
+
+
+
+ Browse list
+
+ Used to navigate to child pages.
+
+ {{ browse_list([
+ {
+ 'title': 'Change your username',
+ 'link': 'http://example.com',
+ },
+ {
+ 'title': 'Change your password',
+ 'link': 'http://example.com',
+ 'hint': 'Your password is used to log in'
+ },
+ {
+ 'title': 'Delete everything',
+ 'link': 'http://example.com',
+ 'hint': 'You can’t undo this',
+ 'destructive': True
+ }
+ ]) }}
+
+ Page footer
+
+
+ Used to submit forms and optionally provide a link to go back to the
+ previous page.
+
+
+ Must be used inside a form.
+
+
+ Adds a hidden CSRF token to the page.
+
+
+ {{ page_footer(
+ button_text='Save and continue'
+ ) }}
+
+ {{ page_footer(
+ button_text='Delete', destructive=True
+ ) }}
+
+ {{ page_footer(
+ button_text='Send', back_link='http://example.com', back_link_text="Back to dashboard"
+ ) }}
+
+ SMS message
+
+ Used to show or preview an SMS message.
+
+
+
+ {{ sms_message("Your vehicle tax for LC12 BFL is due on 1 March 2016. Renew online at www.gov.uk/vehicle-tax") }}
+ {{ sms_message("Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax") }}
+ {{ sms_message(
+ "Your vehicle tax for registration number is due on date. Renew online at www.gov.uk/vehicle-tax",
+ "+44 7700 900 306"
+ ) }}
+
+
+
+ Message status
+
+
+ {{ message_status("Delivered 10:20") }}
+
+
+
+ Tables
+
+ {% call mapping_table(
+ caption='Account settings',
+ field_headings=['Label', 'Value'],
+ field_headings_visible=True,
+ caption_visible=True
+ ) %}
+ {% call row() %}
+ {% call field() %}
+ Username
+ {% endcall %}
+ {% call field() %}
+ admin
+ {% endcall %}
+ {% endcall %}
+ {% endcall %}
+
+ {% call(item) list_table(
+ [
+ {
+ 'file': 'dispatch_20151114.csv', 'status': 'Queued'
+ },
+ {
+ 'file': 'dispatch_20151117.csv', 'status': 'Delivered'
+ },
+ {
+ 'file': 'remdinder_monday.csv', 'status': 'Delivered'
+ }
+ ],
+ caption='Messages',
+ field_headings=['File', 'Status'],
+ field_headings_visible=False,
+ caption_visible=True
+ ) %}
+ {% call field() %}
+ {{ item.file }}
+ {% endcall %}
+ {% call field() %}
+ {{ item.status }}
+ {% endcall %}
+ {% endcall %}
+
+ Textbox
+
+ {{ textbox('name', 'Username') }}
+ {{ textbox('password', 'Password', password=True) }}
+ {{ textbox(
+ 'message',
+ "Message",
+ value="Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax",
+ small=False,
+ highlight_tags=True
+ ) }}
+
+{% endblock %}
diff --git a/tests/app/main/views/test_styleguide.py b/tests/app/main/views/test_styleguide.py
new file mode 100644
index 000000000..10004b847
--- /dev/null
+++ b/tests/app/main/views/test_styleguide.py
@@ -0,0 +1,4 @@
+def test_styleguide_can_render(notifications_admin):
+ response = notifications_admin.test_client().get('/_styleguide')
+
+ assert response.status_code == 200
From 85b0b4af215fb9c757dd3b2b7588ff79ec311a40 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Mon, 11 Jan 2016 11:27:42 +0000
Subject: [PATCH 02/10] Replace `message_status` component with `banner`
Message status was almost identical to banner, visually and semantically.
This consolidates the two into one component.
This means adding an extra parameter which controls whether or not the banner
has a tick (with and without a tick are the only two variations currently).
---
app/assets/stylesheets/components/banner.scss | 10 +++++++-
.../stylesheets/components/sms-message.scss | 23 -------------------
app/templates/components/banner.html | 6 +++--
app/templates/components/sms-message.html | 8 -------
app/templates/views/job.html | 2 +-
app/templates/views/notification.html | 5 +++-
app/templates/views/styleguide.html | 16 ++++++-------
7 files changed, 25 insertions(+), 45 deletions(-)
diff --git a/app/assets/stylesheets/components/banner.scss b/app/assets/stylesheets/components/banner.scss
index a801b2ec7..70555d2d1 100644
--- a/app/assets/stylesheets/components/banner.scss
+++ b/app/assets/stylesheets/components/banner.scss
@@ -1,14 +1,22 @@
+%banner,
.banner {
@include core-19;
background: $turquoise;
color: $white;
display: block;
- padding: $gutter-half $gutter;
+ padding: $gutter-half;
margin: 0 0 $gutter 0;
text-align: center;
position: relative;
+}
+
+.banner-with-tick {
+
+ @extend %banner;
+ padding: $gutter-half $gutter;
+
&:before {
@include core-24;
content: '✔';
diff --git a/app/assets/stylesheets/components/sms-message.scss b/app/assets/stylesheets/components/sms-message.scss
index 4fa727388..abdd5f9bf 100644
--- a/app/assets/stylesheets/components/sms-message.scss
+++ b/app/assets/stylesheets/components/sms-message.scss
@@ -45,27 +45,4 @@
margin: -$gutter-half 0 $gutter 0;
}
- &-history {
-
- background: $turquoise;
- color: $white;
- padding: $gutter-half;
- @include bold-19;
- margin: 0 0 $gutter 0;
-
- &-heading {
-
- @include bold-19;
- margin: 0;
-
- &-time {
- @include inline-block;
- margin-left: 10px;
- font-weight: normal;
- }
-
- }
-
- }
-
}
diff --git a/app/templates/components/banner.html b/app/templates/components/banner.html
index 2dab12c5d..a03d2cb0a 100644
--- a/app/templates/components/banner.html
+++ b/app/templates/components/banner.html
@@ -1,3 +1,5 @@
-{% macro banner(body) %}
- {{ body }}
+{% macro banner(body, with_tick=False) %}
+
+ {{ body }}
+
{% endmacro %}
diff --git a/app/templates/components/sms-message.html b/app/templates/components/sms-message.html
index 38d30b877..d0d0f92ce 100644
--- a/app/templates/components/sms-message.html
+++ b/app/templates/components/sms-message.html
@@ -10,11 +10,3 @@
{% endif %}
{% endmacro %}
-
-{% macro message_status(status, time) %}
-
-
- {{ status }} {{ time }}
-
-
-{% endmacro %}
diff --git a/app/templates/views/job.html b/app/templates/views/job.html
index 29ae57ba7..b733a5e61 100644
--- a/app/templates/views/job.html
+++ b/app/templates/views/job.html
@@ -14,7 +14,7 @@ GOV.UK Notify | Notifications activity
- {{ banner(flash_message) }}
+ {{ banner(flash_message, with_tick=True) }}
diff --git a/app/templates/views/notification.html b/app/templates/views/notification.html
index 1e771062a..4f21b6a96 100644
--- a/app/templates/views/notification.html
+++ b/app/templates/views/notification.html
@@ -1,5 +1,6 @@
{% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message, message_status %}
+{% from "components/banner.html" import banner %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
@@ -18,7 +19,9 @@ GOV.UK Notify | Notifications activity
{{ sms_message(message.message, message.phone) }}
- {{ message_status(message.status, delivered_at) }}
+ {{ banner(
+ "{} {}".format(message.status, delivered_at)
+ ) }}
diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html
index 9ad7ba9a3..882679452 100644
--- a/app/templates/views/styleguide.html
+++ b/app/templates/views/styleguide.html
@@ -4,7 +4,7 @@
{% from "components/big-number.html" import big_number %}
{% from "components/browse-list.html" import browse_list %}
{% from "components/page-footer.html" import page_footer %}
-{% from "components/sms-message.html" import sms_message, message_status %}
+{% from "components/sms-message.html" import sms_message %}
{% from "components/table.html" import mapping_table, list_table, row, field %}
{% from "components/textbox.html" import textbox %}
@@ -20,7 +20,12 @@
Banner
Used to show the result of a user’s action.
- {{ banner("This is a banner") }}
+ {{ banner("This is a banner", with_tick=True) }}
+
+
+ {{ banner("Delivered 10:20") }}
+
+
Big number
@@ -97,13 +102,6 @@
- Message status
-
-
- {{ message_status("Delivered 10:20") }}
-
-
-
Tables
{% call mapping_table(
From 928299e6fede94c5304e3341f308c7a927e99a4d Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Mon, 11 Jan 2016 11:37:05 +0000
Subject: [PATCH 03/10] Add 'View source' link
---
app/templates/views/styleguide.html | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/templates/views/styleguide.html b/app/templates/views/styleguide.html
index 882679452..fadbf0a4c 100644
--- a/app/templates/views/styleguide.html
+++ b/app/templates/views/styleguide.html
@@ -18,6 +18,8 @@
Styleguide
+ View source
+
Banner
Used to show the result of a user’s action.
{{ banner("This is a banner", with_tick=True) }}
From 05695a1c03d3e2b8fe93134c9152c2af31935b85 Mon Sep 17 00:00:00 2001
From: Rebecca Law
Date: Mon, 11 Jan 2016 15:17:00 +0000
Subject: [PATCH 04/10] Updated generate_token to use encrypt the entire url.
Created notify_client.sender to hold the methods to send notifications.
---
app/main/notifications/__init__.py | 0
app/main/views/__init__.py | 54 --------------------
app/main/views/code_not_received.py | 4 +-
app/main/views/forgot_password.py | 4 +-
app/main/views/new_password.py | 4 +-
app/main/views/register.py | 4 +-
app/main/views/sign_in.py | 4 +-
app/notify_client/sender.py | 52 +++++++++++++++++++
config.py | 2 +-
tests/app/main/notify_client/__init__.py | 0
tests/app/main/notify_client/test_sender.py | 35 +++++++++++++
tests/app/main/views/test_forgot_password.py | 39 +++++---------
tests/app/main/views/test_new_password.py | 4 +-
13 files changed, 113 insertions(+), 93 deletions(-)
create mode 100644 app/main/notifications/__init__.py
create mode 100644 app/notify_client/sender.py
create mode 100644 tests/app/main/notify_client/__init__.py
create mode 100644 tests/app/main/notify_client/test_sender.py
diff --git a/app/main/notifications/__init__.py b/app/main/notifications/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/main/views/__init__.py b/app/main/views/__init__.py
index a76b4feb4..e69de29bb 100644
--- a/app/main/views/__init__.py
+++ b/app/main/views/__init__.py
@@ -1,54 +0,0 @@
-from random import randint
-
-from flask import url_for, current_app
-
-from app import admin_api_client
-from app.main.dao import verify_codes_dao
-
-
-def create_verify_code():
- return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)])
-
-
-def send_sms_code(user_id, mobile_number):
- sms_code = create_verify_code()
- verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms')
- admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token)
-
- return sms_code
-
-
-def send_email_code(user_id, email):
- email_code = create_verify_code()
- verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email')
- admin_api_client.send_email(email_address=email,
- from_str='notify@digital.cabinet-office.gov.uk',
- message=email_code,
- subject='Verification code',
- token=admin_api_client.auth_token)
- return email_code
-
-
-def send_change_password_email(email):
- link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True)
- admin_api_client.send_email(email_address=email,
- from_str='notify@digital.cabinet-office.gov.uk',
- message=link_to_change_password,
- subject='Reset password for GOV.UK Notify',
- token=admin_api_client.auth_token)
-
-
-def generate_token(email):
- from itsdangerous import TimestampSigner
- signer = TimestampSigner(current_app.config['SECRET_KEY'])
- return signer.sign(email).decode('utf8')
-
-
-def check_token(token):
- from itsdangerous import TimestampSigner, SignatureExpired
- signer = TimestampSigner(current_app.config['SECRET_KEY'])
- try:
- email = signer.unsign(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS'])
- return email
- except SignatureExpired as e:
- current_app.logger.info('token expired %s' % e)
diff --git a/app/main/views/code_not_received.py b/app/main/views/code_not_received.py
index 4f0962653..56d68025e 100644
--- a/app/main/views/code_not_received.py
+++ b/app/main/views/code_not_received.py
@@ -1,10 +1,10 @@
from flask import (
- render_template, redirect, jsonify, session, url_for)
+ render_template, redirect, session, url_for)
from app.main import main
from app.main.dao import users_dao
from app.main.forms import EmailNotReceivedForm, TextNotReceivedForm
-from app.main.views import send_sms_code, send_email_code
+from app.notify_client.sender import send_sms_code, send_email_code
@main.route('/email-not-received', methods=['GET', 'POST'])
diff --git a/app/main/views/forgot_password.py b/app/main/views/forgot_password.py
index 0e5c81bda..21d7d8f1c 100644
--- a/app/main/views/forgot_password.py
+++ b/app/main/views/forgot_password.py
@@ -1,8 +1,8 @@
-from flask import render_template, flash, current_app
+from flask import render_template, current_app
from app.main import main
from app.main.dao import users_dao
from app.main.forms import ForgotPasswordForm
-from app.main.views import send_change_password_email
+from app.notify_client.sender import send_change_password_email
@main.route('/forgot-password', methods=['GET', 'POST'])
diff --git a/app/main/views/new_password.py b/app/main/views/new_password.py
index c1b0e1e04..7a46f5c18 100644
--- a/app/main/views/new_password.py
+++ b/app/main/views/new_password.py
@@ -3,7 +3,7 @@ from flask import (render_template, url_for, redirect, flash)
from app.main import main
from app.main.dao import users_dao
from app.main.forms import NewPasswordForm
-from app.main.views import send_sms_code, check_token
+from app.notify_client.sender import check_token, send_sms_code
@main.route('/new-password/', methods=['GET', 'POST'])
@@ -13,7 +13,7 @@ def new_password(token):
flash('The link in the email we sent you has expired. Enter your email address to resend.')
return redirect(url_for('.forgot_password'))
- user = users_dao.get_user_by_email(email_address=email_address.decode('utf-8'))
+ user = users_dao.get_user_by_email(email_address=email_address)
if user and user.state != 'request_password_reset':
flash('The link in the email we sent you has already been used.')
return redirect(url_for('.index'))
diff --git a/app/main/views/register.py b/app/main/views/register.py
index 9eef37a4a..2ca7d8faa 100644
--- a/app/main/views/register.py
+++ b/app/main/views/register.py
@@ -5,12 +5,14 @@ from flask import render_template, redirect, session
from app.main import main
from app.main.dao import users_dao
from app.main.forms import RegisterUserForm
-from app.main.views import send_sms_code, send_email_code
from app.models import User
# TODO how do we handle duplicate unverifed email addresses?
# malicious or otherwise.
+from app.notify_client.sender import send_sms_code, send_email_code
+
+
@main.route('/register', methods=['GET', 'POST'])
def register():
form = RegisterUserForm(users_dao.get_user_by_email)
diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py
index 0f3be0c2d..7c0e28a26 100644
--- a/app/main/views/sign_in.py
+++ b/app/main/views/sign_in.py
@@ -1,12 +1,12 @@
from flask import (
- render_template, redirect, jsonify, url_for)
+ render_template, redirect, url_for)
from flask import session
from app.main import main
from app.main.dao import users_dao
from app.main.encryption import check_hash
from app.main.forms import LoginForm
-from app.main.views import send_sms_code
+from app.notify_client.sender import send_sms_code
@main.route('/sign-in', methods=(['GET', 'POST']))
diff --git a/app/notify_client/sender.py b/app/notify_client/sender.py
new file mode 100644
index 000000000..1a509a820
--- /dev/null
+++ b/app/notify_client/sender.py
@@ -0,0 +1,52 @@
+from random import randint
+from flask import url_for, current_app
+from itsdangerous import URLSafeTimedSerializer, SignatureExpired
+from app import admin_api_client
+from app.main.dao import verify_codes_dao
+
+
+def create_verify_code():
+ return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)])
+
+
+def send_sms_code(user_id, mobile_number):
+ sms_code = create_verify_code()
+ verify_codes_dao.add_code(user_id=user_id, code=sms_code, code_type='sms')
+ admin_api_client.send_sms(mobile_number=mobile_number, message=sms_code, token=admin_api_client.auth_token)
+
+ return sms_code
+
+
+def send_email_code(user_id, email):
+ email_code = create_verify_code()
+ verify_codes_dao.add_code(user_id=user_id, code=email_code, code_type='email')
+ admin_api_client.send_email(email_address=email,
+ from_str='notify@digital.cabinet-office.gov.uk',
+ message=email_code,
+ subject='Verification code',
+ token=admin_api_client.auth_token)
+ return email_code
+
+
+def send_change_password_email(email):
+ link_to_change_password = url_for('.new_password', token=generate_token(email), _external=True)
+ admin_api_client.send_email(email_address=email,
+ from_str='notify@digital.cabinet-office.gov.uk',
+ message=link_to_change_password,
+ subject='Reset password for GOV.UK Notify',
+ token=admin_api_client.auth_token)
+
+
+def generate_token(email):
+ ser = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
+ return ser.dumps(email, current_app.config.get('DANGEROUS_SALT'))
+
+
+def check_token(token):
+ ser = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
+ try:
+ email = ser.loads(token, max_age=current_app.config['TOKEN_MAX_AGE_SECONDS'],
+ salt=current_app.config.get('DANGEROUS_SALT'))
+ return email
+ except SignatureExpired as e:
+ current_app.logger.info('token expired %s' % e)
diff --git a/config.py b/config.py
index 0f03f3792..4893ee410 100644
--- a/config.py
+++ b/config.py
@@ -29,7 +29,7 @@ class Config(object):
SECRET_KEY = 'secret-key'
HTTP_PROTOCOL = 'http'
DANGEROUS_SALT = 'itsdangeroussalt'
- TOKEN_MAX_AGE_SECONDS = 120000
+ TOKEN_MAX_AGE_SECONDS = 3600
class Development(Config):
diff --git a/tests/app/main/notify_client/__init__.py b/tests/app/main/notify_client/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/app/main/notify_client/test_sender.py b/tests/app/main/notify_client/test_sender.py
new file mode 100644
index 000000000..129b1f3b1
--- /dev/null
+++ b/tests/app/main/notify_client/test_sender.py
@@ -0,0 +1,35 @@
+from itsdangerous import BadSignature
+from pytest import fail
+
+from app.notify_client.sender import generate_token, check_token
+
+
+def test_should_return_email_from_signed_token(notifications_admin,
+ notifications_admin_db,
+ notify_db_session):
+ email = 'email@something.com'
+ token = generate_token(email)
+ assert email == check_token(token)
+
+
+def test_should_throw_exception_when_token_is_tampered_with(notifications_admin,
+ notifications_admin_db,
+ notify_db_session):
+ email = 'email@something.com'
+ token = generate_token(email)
+ try:
+ check_token(token + 'qerqwer')
+ fail()
+ except BadSignature:
+ pass
+
+
+def test_return_none_when_token_is_expired(notifications_admin,
+ notifications_admin_db,
+ notify_db_session):
+ with notifications_admin.test_request_context():
+ notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000
+ email = 'email@something.com'
+ token = generate_token(email)
+ assert check_token(token) is None
+ notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 120000
diff --git a/tests/app/main/views/test_forgot_password.py b/tests/app/main/views/test_forgot_password.py
index c7dac0177..9b4e82a81 100644
--- a/tests/app/main/views/test_forgot_password.py
+++ b/tests/app/main/views/test_forgot_password.py
@@ -1,15 +1,14 @@
from flask import url_for
-
from app.main.dao import users_dao
-from app.main.views import generate_token
from tests.app.main import create_test_user
def test_should_render_forgot_password(notifications_admin, notifications_admin_db, notify_db_session):
- response = notifications_admin.test_client().get('/forgot-password')
- assert response.status_code == 200
- assert 'If you have forgotten your password, we can send you an email to create a new password.' \
- in response.get_data(as_text=True)
+ with notifications_admin.test_request_context():
+ response = notifications_admin.test_client().get(url_for('.forgot_password'))
+ assert response.status_code == 200
+ assert 'If you have forgotten your password, we can send you an email to create a new password.' \
+ in response.get_data(as_text=True)
def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_admin,
@@ -17,25 +16,11 @@ def test_should_redirect_to_password_reset_sent_and_state_updated(notifications_
mocker,
notify_db_session):
mocker.patch("app.admin_api_client.send_email")
- user = create_test_user('active')
- response = notifications_admin.test_client().post('/forgot-password',
- data={'email_address': user.email_address})
- assert response.status_code == 200
- assert 'You have been sent an email containing a link to reset your password.' in response.get_data(
- as_text=True)
- assert users_dao.get_user_by_id(user.id).state == 'request_password_reset'
-
-
-def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_expired(notifications_admin,
- notifications_admin_db,
- notify_db_session):
with notifications_admin.test_request_context():
- with notifications_admin.test_client() as client:
- notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = -1000
- user = create_test_user('active')
- token = generate_token(user.email_address)
- response = client.post('/new-password/{}'.format(token),
- data={'new_password': 'a-new_password'})
- assert response.status_code == 302
- assert response.location == url_for('.forgot_password', _external=True)
- notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400
+ user = create_test_user('active')
+ response = notifications_admin.test_client().post(url_for('.forgot_password'),
+ data={'email_address': user.email_address})
+ assert response.status_code == 200
+ assert 'You have been sent an email containing a link to reset your password.' in response.get_data(
+ as_text=True)
+ assert users_dao.get_user_by_id(user.id).state == 'request_password_reset'
diff --git a/tests/app/main/views/test_new_password.py b/tests/app/main/views/test_new_password.py
index 4f1aeba6b..ca89da594 100644
--- a/tests/app/main/views/test_new_password.py
+++ b/tests/app/main/views/test_new_password.py
@@ -2,7 +2,7 @@ from flask import url_for
from app.main.dao import users_dao
from app.main.encryption import check_hash
-from app.main.views import generate_token
+from app.notify_client.sender import generate_token
from tests.app.main import create_test_user
@@ -56,7 +56,7 @@ def test_should_redirect_to_forgot_password_with_flash_message_when_token_is_exp
response = client.post(url_for('.new_password', token=token), data={'new_password': 'a-new_password'})
assert response.status_code == 302
assert response.location == url_for('.forgot_password', _external=True)
- notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 86400
+ notifications_admin.config['TOKEN_MAX_AGE_SECONDS'] = 3600
def test_should_redirect_to_forgot_password_when_user_is_active_should_be_request_password_reset(notifications_admin,
From e8fe8c50ba5e805b20eb8ef3109ccab799388fbe Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Mon, 11 Jan 2016 13:15:10 +0000
Subject: [PATCH 05/10] Add a WTForms-compatible textbox macro
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This macro:
- accepts a WTForm form field as a parameter
- renders a form field which follows the GOV.UK Elements patterns, both visually
and in markup terms
It then changes any page which uses either:
- the old, non-WTForms macro or
- the old, WTFforms `render_field` macro
…to use this new macro and removes both of the old ones.
It also adds the option to display hint text above the textbox.
---
app/assets/stylesheets/main.scss | 1 +
app/main/forms.py | 15 ++++++++-
app/main/views/service_settings.py | 26 ++++++++++++---
app/main/views/styleguide.py | 19 ++++++++++-
app/main/views/templates.py | 18 ++++++++---
app/templates/admin_template.html | 1 -
app/templates/components/form-field.html | 12 -------
app/templates/components/textbox.html | 32 +++++++++++--------
app/templates/views/add-service.html | 7 ++--
app/templates/views/edit-template.html | 4 +--
app/templates/views/email-not-received.html | 12 +++----
app/templates/views/forgot-password.html | 12 +++----
app/templates/views/new-password.html | 16 +++-------
app/templates/views/register.html | 11 +++----
.../views/service-settings/confirm.html | 2 +-
.../views/service-settings/name.html | 11 ++++---
app/templates/views/signin.html | 13 +++-----
app/templates/views/styleguide.html | 13 ++------
app/templates/views/text-not-received.html | 6 ++--
app/templates/views/two-factor.html | 6 ++--
app/templates/views/verify.html | 8 ++---
21 files changed, 140 insertions(+), 105 deletions(-)
delete mode 100644 app/templates/components/form-field.html
diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss
index 7a0a95feb..bcd47c542 100644
--- a/app/assets/stylesheets/main.scss
+++ b/app/assets/stylesheets/main.scss
@@ -22,6 +22,7 @@
@import '../govuk_elements/public/sass/elements/forms';
@import '../govuk_elements/public/sass/elements/forms/form-validation';
@import '../govuk_elements/public/sass/elements/forms/form-block-labels';
+@import '../govuk_elements/public/sass/elements/forms/form-validation';
@import '../govuk_elements/public/sass/elements/icons';
@import '../govuk_elements/public/sass/elements/layout';
@import '../govuk_elements/public/sass/elements/lists';
diff --git a/app/main/forms.py b/app/main/forms.py
index e06595ba4..a6315928d 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -1,5 +1,5 @@
from flask_wtf import Form
-from wtforms import StringField, PasswordField, ValidationError
+from wtforms import StringField, PasswordField, ValidationError, TextAreaField
from wtforms.validators import DataRequired, Email, Length, Regexp
from app.main.validators import Blacklist, ValidateUserCodes
@@ -121,6 +121,19 @@ class AddServiceForm(Form):
raise ValidationError('Service name already exists')
+class ServiceNameForm(Form):
+ service_name = StringField(u'New name')
+
+
+class ConfirmPasswordForm(Form):
+ password = PasswordField(u'Enter password')
+
+
+class TemplateForm(Form):
+ template_name = StringField(u'Template name')
+ template_body = TextAreaField(u'Message')
+
+
class ForgotPasswordForm(Form):
email_address = email_address()
diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py
index 6553a876e..b031fd447 100644
--- a/app/main/views/service_settings.py
+++ b/app/main/views/service_settings.py
@@ -2,6 +2,7 @@ from flask import render_template, redirect, request, url_for, abort
from flask_login import login_required
from app.main import main
+from app.main.forms import ConfirmPasswordForm, ServiceNameForm
service = {
'name': 'Service name',
@@ -20,10 +21,15 @@ def service_settings():
@main.route("/service-settings/name", methods=['GET', 'POST'])
def name():
+
+ form = ServiceNameForm()
+ form.service_name.data = 'Service name'
+
if request.method == 'GET':
return render_template(
'views/service-settings/name.html',
- service=service
+ service=service,
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.confirm_name_change'))
@@ -31,10 +37,14 @@ def name():
@main.route("/service-settings/name/confirm", methods=['GET', 'POST'])
def confirm_name_change():
+
+ form = ConfirmPasswordForm()
+
if request.method == 'GET':
return render_template(
'views/service-settings/confirm.html',
- heading='Change your service name'
+ heading='Change your service name',
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.service_settings'))
@@ -64,11 +74,15 @@ def status():
@main.route("/service-settings/status/confirm", methods=['GET', 'POST'])
def confirm_status_change():
+
+ form = ConfirmPasswordForm()
+
if request.method == 'GET':
return render_template(
'views/service-settings/confirm.html',
heading='Turn off all outgoing notifications',
- destructive=True
+ destructive=True,
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.service_settings'))
@@ -87,11 +101,15 @@ def delete():
@main.route("/service-settings/delete/confirm", methods=['GET', 'POST'])
def confirm_delete():
+
+ form = ConfirmPasswordForm()
+
if request.method == 'GET':
return render_template(
'views/service-settings/confirm.html',
heading='Delete this service from Notify',
- destructive=True
+ destructive=True,
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.dashboard'))
diff --git a/app/main/views/styleguide.py b/app/main/views/styleguide.py
index 516a39b6c..f778f9dcf 100644
--- a/app/main/views/styleguide.py
+++ b/app/main/views/styleguide.py
@@ -1,7 +1,24 @@
from flask import render_template
+from flask_wtf import Form
+from wtforms import StringField, PasswordField, TextAreaField, validators
from app.main import main
@main.route('/_styleguide')
def styleguide():
- return render_template('views/styleguide.html')
+
+ class FormExamples(Form):
+ username = StringField(u'Username')
+ password = PasswordField(u'Password', [validators.required()])
+ message = TextAreaField(u'Message')
+
+ form = FormExamples()
+
+ form.message.data = "Your vehicle tax for ((registration number)) is due on ((date)). Renew online at www.gov.uk/vehicle-tax" # noqa
+
+ form.validate()
+
+ return render_template(
+ 'views/styleguide.html',
+ form=form
+ )
diff --git a/app/main/views/templates.py b/app/main/views/templates.py
index ff80cb353..2d7d38e85 100644
--- a/app/main/views/templates.py
+++ b/app/main/views/templates.py
@@ -1,6 +1,7 @@
from flask import request, render_template, redirect, url_for
from app.main import main
+from app.main.forms import TemplateForm
@main.route("/templates")
@@ -10,12 +11,17 @@ def manage_templates():
@main.route("/templates/template", methods=['GET', 'POST'])
def add_template():
+
+ form = TemplateForm()
+
+ form.template_name.data = 'Reminder'
+ form.template_body.data = 'Vehicle tax: Your vehicle tax for ((registration number)) expires on ((date)). Tax your vehicle at www.gov.uk/vehicle-tax' # noqa
+
if request.method == 'GET':
return render_template(
'views/edit-template.html',
- template_name='Reminder',
- template_body='Vehicle tax: Your vehicle tax for ((registration number)) expires on ((date)). Tax your vehicle at www.gov.uk/vehicle-tax', # noqa
- h1='Edit template'
+ h1='Edit template',
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.manage_templates'))
@@ -23,10 +29,14 @@ def add_template():
@main.route("/templates/template/add", methods=['GET', 'POST'])
def edit_template():
+
+ form = TemplateForm()
+
if request.method == 'GET':
return render_template(
'views/edit-template.html',
- h1='Add template'
+ h1='Add template',
+ form=form
)
elif request.method == 'POST':
return redirect(url_for('.manage_templates'))
diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html
index a03e47612..c127da3f7 100644
--- a/app/templates/admin_template.html
+++ b/app/templates/admin_template.html
@@ -1,4 +1,3 @@
-{%- from "components/form-field.html" import render_field %}
{% extends "govuk_template.html" %}
{% block head %}
diff --git a/app/templates/components/form-field.html b/app/templates/components/form-field.html
deleted file mode 100644
index 691b4c579..000000000
--- a/app/templates/components/form-field.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% macro render_field(field) %}
- - {{ field.label }}
-
- {{ field(**kwargs)|safe }}
- {% if field.errors %}
-
- {% for error in field.errors %}
- - {{ error }}
- {% endfor %}
-
- {% endif %}
-
-{% endmacro %}
diff --git a/app/templates/components/textbox.html b/app/templates/components/textbox.html
index 7a74f0985..918fc33d7 100644
--- a/app/templates/components/textbox.html
+++ b/app/templates/components/textbox.html
@@ -1,15 +1,21 @@
-{% macro textbox(name, label, value='', small=True, highlight_tags=False, password=False) %}
-
diff --git a/app/templates/views/forgot-password.html b/app/templates/views/forgot-password.html
index 4d36f2a86..c4062e6b9 100644
--- a/app/templates/views/forgot-password.html
+++ b/app/templates/views/forgot-password.html
@@ -1,4 +1,6 @@
{% extends "admin_template.html" %}
+{% from "components/textbox.html" import textbox %}
+{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
GOV.UK Notify
@@ -12,13 +14,11 @@ GOV.UK Notify
If you have forgotten your password, we can send you an email to create a new password.
-
+
diff --git a/app/templates/views/new-password.html b/app/templates/views/new-password.html
index 580325410..16c098cb9 100644
--- a/app/templates/views/new-password.html
+++ b/app/templates/views/new-password.html
@@ -1,4 +1,6 @@
{% extends "admin_template.html" %}
+{% from "components/textbox.html" import textbox %}
+{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
GOV.UK Notify
@@ -13,17 +15,9 @@ GOV.UK Notify
You can now create a new password for your account.
-
{% else %}
Message about email address does not exist. Some one needs to figure out the words here.
diff --git a/app/templates/views/register.html b/app/templates/views/register.html
index f640ac116..9bb45a3a8 100644
--- a/app/templates/views/register.html
+++ b/app/templates/views/register.html
@@ -1,4 +1,5 @@
{% extends "admin_template.html" %}
+{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
@@ -16,12 +17,10 @@ GOV.UK Notify | Create an account
diff --git a/app/templates/views/service-settings/confirm.html b/app/templates/views/service-settings/confirm.html
index fe0cd05c9..20206724c 100644
--- a/app/templates/views/service-settings/confirm.html
+++ b/app/templates/views/service-settings/confirm.html
@@ -14,7 +14,7 @@ GOV.UK Notify | Service settings
diff --git a/app/templates/views/two-factor.html b/app/templates/views/two-factor.html
index 46724c617..44cd98739 100644
--- a/app/templates/views/two-factor.html
+++ b/app/templates/views/two-factor.html
@@ -1,4 +1,5 @@
{% extends "admin_template.html" %}
+{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
@@ -14,9 +15,8 @@ GOV.UK Notify | Text verification
We've sent you a text message with a verification code.
-
diff --git a/app/templates/views/verify.html b/app/templates/views/verify.html
index 94e594d1a..7c4c41180 100644
--- a/app/templates/views/verify.html
+++ b/app/templates/views/verify.html
@@ -1,4 +1,5 @@
{% extends "admin_template.html" %}
+{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
@@ -13,11 +14,10 @@ GOV.UK Notify | Confirm email address and mobile number
We've sent you confirmation codes by email and text message. You need to enter both codes here.
-
From 362b2aeba7d288e00ed5efac8bc1335a89a07ce4 Mon Sep 17 00:00:00 2001
From: Martyn Inglis
Date: Mon, 11 Jan 2016 17:00:04 +0000
Subject: [PATCH 06/10] New script to run db updates so can be done by ubuntu
user
---
appspec.yml | 4 ++++
scripts/aws_db_updates.sh | 3 +++
2 files changed, 7 insertions(+)
create mode 100755 scripts/aws_db_updates.sh
diff --git a/appspec.yml b/appspec.yml
index 56488949b..f5ac38fd8 100644
--- a/appspec.yml
+++ b/appspec.yml
@@ -13,6 +13,10 @@ hooks:
location: scripts/aws_change_ownership.sh
runas: root
timeout: 300
+ -
+ location: scripts/aws_db_updates.sh
+ runas: ubuntu
+ timeout: 300
ApplicationStart:
-
location: scripts/aws_start_app.sh
diff --git a/scripts/aws_db_updates.sh b/scripts/aws_db_updates.sh
new file mode 100755
index 000000000..e45ecc843
--- /dev/null
+++ b/scripts/aws_db_updates.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+python3 db.py db upgrade
From 35b4f8f3a77f6dba00d2b1c2605c936d681ef88c Mon Sep 17 00:00:00 2001
From: Martyn Inglis
Date: Mon, 11 Jan 2016 17:23:10 +0000
Subject: [PATCH 07/10] Removed the db upgrades
---
scripts/aws_install_dependencies.sh | 1 -
1 file changed, 1 deletion(-)
diff --git a/scripts/aws_install_dependencies.sh b/scripts/aws_install_dependencies.sh
index e36fae522..a74b4ccea 100755
--- a/scripts/aws_install_dependencies.sh
+++ b/scripts/aws_install_dependencies.sh
@@ -4,4 +4,3 @@ echo "Install dependencies"
cd /home/ubuntu/notifications-admin;
export FLASK_CONFIG=/home/ubuntu/config.cfg
pip3 install -r /home/ubuntu/notifications-admin/requirements.txt
-python3 db.py db upgrade
From e075145945da2e888943cd395a44ad9bb98c61f5 Mon Sep 17 00:00:00 2001
From: Martyn Inglis
Date: Mon, 11 Jan 2016 17:32:24 +0000
Subject: [PATCH 08/10] Putting this back in the dependencies file for now
- this fails on the instance and not sure why, putting back till I can figure it out
---
appspec.yml | 6 +-----
scripts/aws_db_updates.sh | 3 ---
scripts/aws_install_dependencies.sh | 1 +
3 files changed, 2 insertions(+), 8 deletions(-)
delete mode 100755 scripts/aws_db_updates.sh
diff --git a/appspec.yml b/appspec.yml
index f5ac38fd8..c6a713089 100644
--- a/appspec.yml
+++ b/appspec.yml
@@ -13,11 +13,7 @@ hooks:
location: scripts/aws_change_ownership.sh
runas: root
timeout: 300
- -
- location: scripts/aws_db_updates.sh
- runas: ubuntu
- timeout: 300
- ApplicationStart:
+ ApplicationStart:
-
location: scripts/aws_start_app.sh
runas: root
diff --git a/scripts/aws_db_updates.sh b/scripts/aws_db_updates.sh
deleted file mode 100755
index e45ecc843..000000000
--- a/scripts/aws_db_updates.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-python3 db.py db upgrade
diff --git a/scripts/aws_install_dependencies.sh b/scripts/aws_install_dependencies.sh
index a74b4ccea..e36fae522 100755
--- a/scripts/aws_install_dependencies.sh
+++ b/scripts/aws_install_dependencies.sh
@@ -4,3 +4,4 @@ echo "Install dependencies"
cd /home/ubuntu/notifications-admin;
export FLASK_CONFIG=/home/ubuntu/config.cfg
pip3 install -r /home/ubuntu/notifications-admin/requirements.txt
+python3 db.py db upgrade
From 840185017c48efa5005acbb717e93bb8cd6df7f9 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Tue, 12 Jan 2016 09:38:55 +0000
Subject: [PATCH 09/10] Add badges for requirements and code coverage
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 984842c8e..214b2fca5 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-[](https://api.travis-ci.org/alphagov/notifications-admin.svg?branch=master)
+[](https://travis-ci.org/alphagov/notifications-admin)
+[](https://coveralls.io/github/alphagov/notifications-admin?branch=master)
+[](https://requires.io/github/alphagov/notifications-admin/requirements/?branch=master)
# notifications-admin
From 003c7e097a0435f415631969fecbcbe08ea2526e Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Tue, 12 Jan 2016 09:47:35 +0000
Subject: [PATCH 10/10] Remove badge for code coverage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
It needs more integration, which I’m not sure we want at the moment.
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 214b2fca5..0f963d9c5 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
[](https://travis-ci.org/alphagov/notifications-admin)
-[](https://coveralls.io/github/alphagov/notifications-admin?branch=master)
[](https://requires.io/github/alphagov/notifications-admin/requirements/?branch=master)