Merge branch 'master' into celery-jobs

This commit is contained in:
Martyn Inglis
2016-02-25 10:34:20 +00:00
40 changed files with 535 additions and 257 deletions

View File

@@ -1,7 +1,7 @@
sudo: false sudo: false
language: python language: python
python: python:
- '3.4' - '3.5'
env: env:
secure: jT9BIioqBMkOdLZhU+WJNdnRJ+06G7qUx4QqEVldp96dJwmWpPEvA0XbitdnQt/WXYkpMlDbgSApvvGj2ZNvdpowRRe5HFX8D2Udhi2g9+cXgKrQxH6zv0evJyQLOjCINW6KtgMCJ5wkYR3qQ4BQawlDt6ecpmeboKTmvs2W8jZ09aV4IKKvdd7BwFon10QVPF5ny10G83unLtKnKgRMjSSLnaEiA78pE/LSUkekK4mhmtl+yfQf60cIuQGcN9NCYIt5PrdYYyMkbUaht9ykwL2C11sp5JYPClI9k6lrlpGJCdL9wbJwejGhR/pEqwJ4tKK8Zv+mngmkbzE6fd5ehuRMnIUAifG4t3p6WbhKwY5pJsdVyPgWcRSPXOJA7yEcAeTAvWcC++6mCIFBeMxt/yQNw02jkFHeNKRh2twTRvr4xWZHq9FsVxTEVz89OOuue3IkkyDNmVusGJ9+AVRIn9Oa+U/r3bDnrs7jz+meSwb82GZUBzFpUe2pe8qeBE572Ay7yHB73VHUgp/2A1qkZ4SnTjTpMbnS5RdXTgwtMkOs5MLZgteCVxFL3sHcr9e/B3UIUnzKUSPXXOjHyDxBwrABWo81V9Vp2IPV7P9Ofv8zroudjQxK5MOcbmiPQF+eEB9L4DvkUBNsGxtJ/nmPp6tmN0Xjo0xXVdZCEVj29Og= secure: jT9BIioqBMkOdLZhU+WJNdnRJ+06G7qUx4QqEVldp96dJwmWpPEvA0XbitdnQt/WXYkpMlDbgSApvvGj2ZNvdpowRRe5HFX8D2Udhi2g9+cXgKrQxH6zv0evJyQLOjCINW6KtgMCJ5wkYR3qQ4BQawlDt6ecpmeboKTmvs2W8jZ09aV4IKKvdd7BwFon10QVPF5ny10G83unLtKnKgRMjSSLnaEiA78pE/LSUkekK4mhmtl+yfQf60cIuQGcN9NCYIt5PrdYYyMkbUaht9ykwL2C11sp5JYPClI9k6lrlpGJCdL9wbJwejGhR/pEqwJ4tKK8Zv+mngmkbzE6fd5ehuRMnIUAifG4t3p6WbhKwY5pJsdVyPgWcRSPXOJA7yEcAeTAvWcC++6mCIFBeMxt/yQNw02jkFHeNKRh2twTRvr4xWZHq9FsVxTEVz89OOuue3IkkyDNmVusGJ9+AVRIn9Oa+U/r3bDnrs7jz+meSwb82GZUBzFpUe2pe8qeBE572Ay7yHB73VHUgp/2A1qkZ4SnTjTpMbnS5RdXTgwtMkOs5MLZgteCVxFL3sHcr9e/B3UIUnzKUSPXXOjHyDxBwrABWo81V9Vp2IPV7P9Ofv8zroudjQxK5MOcbmiPQF+eEB9L4DvkUBNsGxtJ/nmPp6tmN0Xjo0xXVdZCEVj29Og=
before_install: before_install:

View File

@@ -71,7 +71,7 @@
color: $text-colour; color: $text-colour;
background-image: file-url('icon-important-2x.png'); background-image: file-url('icon-important-2x.png');
background-size: 34px 34px; background-size: 34px 34px;
background-position: 0 0px; background-position: 0 0;
background-repeat: no-repeat; background-repeat: no-repeat;
padding: 7px 0 5px 50px; padding: 7px 0 5px 50px;
} }

View File

@@ -7,9 +7,26 @@
margin: 20px 0 10px 0; margin: 20px 0 10px 0;
} }
&-subject, &-meta {
&-from {
margin: 10px 0; @include core-19;
margin: 0;
td,
th {
@include core-19;
border-bottom: 0;
border-top: 1px solid $border-colour;
}
th {
color: $secondary-text-colour;
}
td {
width: 99%;
}
} }
&-from { &-from {

View File

@@ -40,7 +40,7 @@
.sms-message-use-links { .sms-message-use-links {
@include copy-19; @include copy-19;
margin-top: 55px; margin-top: 52px;
a { a {

View File

@@ -3,7 +3,7 @@ from flask import Blueprint
main = Blueprint('main', __name__) main = Blueprint('main', __name__)
from app.main.views import ( from app.main.views import (
index, sign_in, sign_out, register, two_factor, verify, sms, add_service, index, sign_in, sign_out, register, two_factor, verify, send, add_service,
code_not_received, jobs, dashboard, templates, service_settings, forgot_password, code_not_received, jobs, dashboard, templates, service_settings, forgot_password,
new_password, styleguide, user_profile, choose_service, api_keys, manage_users new_password, styleguide, user_profile, choose_service, api_keys, manage_users
) )

View File

@@ -4,14 +4,14 @@ from app.utils import BrowsableItem
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
def insert_service_template(name, content, service_id): def insert_service_template(name, type_, content, service_id, subject=None):
return notifications_api_client.create_service_template( return notifications_api_client.create_service_template(
name, 'sms', content, service_id) name, type_, content, service_id, subject)
def update_service_template(id_, name, content, service_id): def update_service_template(id_, name, type_, content, service_id, subject=None):
return notifications_api_client.update_service_template( return notifications_api_client.update_service_template(
id_, name, 'sms', content, service_id) id_, name, type_, content, service_id)
def get_service_templates(service_id): def get_service_templates(service_id):

View File

@@ -7,7 +7,8 @@ from wtforms import (
ValidationError, ValidationError,
TextAreaField, TextAreaField,
FileField, FileField,
RadioField RadioField,
BooleanField
) )
from wtforms.fields.html5 import EmailField, TelField from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import DataRequired, Email, Length, Regexp from wtforms.validators import DataRequired, Email, Length, Regexp
@@ -110,6 +111,7 @@ class TwoFactorForm(Form):
super(TwoFactorForm, self).__init__(*args, **kwargs) super(TwoFactorForm, self).__init__(*args, **kwargs)
sms_code = sms_code() sms_code = sms_code()
remember_me = BooleanField("Remember me")
def validate_sms_code(self, field): def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data) is_valid, reason = self.validate_code_func(field.data)
@@ -188,7 +190,7 @@ class ConfirmPasswordForm(Form):
raise ValidationError('Invalid password') raise ValidationError('Invalid password')
class TemplateForm(Form): class SMSTemplateForm(Form):
name = StringField( name = StringField(
u'Template name', u'Template name',
validators=[DataRequired(message="Template name cannot be empty")]) validators=[DataRequired(message="Template name cannot be empty")])
@@ -198,6 +200,13 @@ class TemplateForm(Form):
validators=[DataRequired(message="Template content cannot be empty")]) validators=[DataRequired(message="Template content cannot be empty")])
class EmailTemplateForm(SMSTemplateForm):
subject = StringField(
u'Subject',
validators=[DataRequired(message="Subject cannot be empty")])
class ForgotPasswordForm(Form): class ForgotPasswordForm(Form):
email_address = email_address() email_address = email_address()

0
app/main/views/email.py Normal file
View File

View File

@@ -22,15 +22,3 @@ def register_from_invite():
@login_required @login_required
def verify_mobile(): def verify_mobile():
return render_template('views/verify-mobile.html') return render_template('views/verify-mobile.html')
@main.route("/services/<service_id>/send-email")
@login_required
def send_email(service_id):
return render_template('views/send-email.html', service_id=service_id)
@main.route("/services/<service_id>/check-email")
@login_required
def check_email(service_id):
return render_template('views/check-email.html')

View File

@@ -29,10 +29,11 @@ fake_users = [
@main.route("/services/<service_id>/users") @main.route("/services/<service_id>/users")
@login_required @login_required
def manage_users(service_id): def manage_users(service_id):
users = user_api_client.get_users_for_service(service_id=service_id)
return render_template( return render_template(
'views/manage-users.html', 'views/manage-users.html',
service_id=service_id, service_id=service_id,
users=fake_users, users=users,
current_user=current_user, current_user=current_user,
invited_users=[] invited_users=[]
) )

View File

@@ -28,15 +28,23 @@ from app.main.uploader import (
s3download s3download
) )
from app.main.dao import templates_dao from app.main.dao import templates_dao
from app.main.dao import services_dao
from app import job_api_client from app import job_api_client
from app.utils import ( from app.utils import validate_recipient, InvalidPhoneError, InvalidEmailError
validate_phone_number,
InvalidPhoneError first_column_header = {
) 'email': 'email',
'sms': 'phone'
}
@main.route("/services/<service_id>/sms/send", methods=['GET']) @main.route("/services/<service_id>/send/<template_type>", methods=['GET'])
def choose_sms_template(service_id): def choose_template(service_id, template_type):
services_dao.get_service_by_id_or_404(service_id)
if template_type not in ['email', 'sms']:
abort(404)
try: try:
jobs = job_api_client.get_job(service_id)['data'] jobs = job_api_client.get_job(service_id)['data']
except HTTPError as e: except HTTPError as e:
@@ -44,23 +52,20 @@ def choose_sms_template(service_id):
abort(404) abort(404)
else: else:
raise e raise e
print("="*80)
print(jobs)
print(len(jobs))
print(bool(len(jobs)))
return render_template( return render_template(
'views/choose-sms-template.html', 'views/choose-{}-template.html'.format(template_type),
templates=[ templates=[
Template(template) for template in templates_dao.get_service_templates(service_id)['data'] Template(template) for template in templates_dao.get_service_templates(service_id)['data']
if template['template_type'] == template_type
], ],
has_jobs=len(jobs), has_jobs=len(jobs),
service_id=service_id service_id=service_id
) )
@main.route("/services/<service_id>/sms/send/<template_id>", methods=['GET', 'POST']) @main.route("/services/<service_id>/send/<int:template_id>", methods=['GET', 'POST'])
@login_required @login_required
def send_sms(service_id, template_id): def send_messages(service_id, template_id):
form = CsvUploadForm() form = CsvUploadForm()
if form.validate_on_submit(): if form.validate_on_submit():
@@ -70,48 +75,50 @@ def send_sms(service_id, template_id):
upload_id = str(uuid.uuid4()) upload_id = str(uuid.uuid4())
s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION']) s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION'])
session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']} session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']}
return redirect(url_for('.check_sms', return redirect(url_for('.check_messages',
service_id=service_id, service_id=service_id,
upload_id=upload_id)) upload_id=upload_id))
except ValueError as e: except ValueError as e:
flash('There was a problem uploading: {}'.format(csv_file.filename)) flash('There was a problem uploading: {}'.format(csv_file.filename))
flash(str(e)) flash(str(e))
return redirect(url_for('.send_sms', service_id=service_id, template_id=template_id)) return redirect(url_for('.send_messages', service_id=service_id, template_id=template_id))
service = services_dao.get_service_by_id_or_404(service_id)
template = Template( template = Template(
templates_dao.get_service_template_or_404(service_id, template_id)['data'] templates_dao.get_service_template_or_404(service_id, template_id)['data']
) )
return render_template( return render_template(
'views/send-sms.html', 'views/send.html',
template=template, template=template,
column_headers=['phone'] + template.placeholders_as_markup, column_headers=[first_column_header[template.template_type]] + template.placeholders_as_markup,
form=form, form=form,
service=service,
service_id=service_id service_id=service_id
) )
@main.route("/services/<service_id>/sms/send/<template_id>.csv", methods=['GET']) @main.route("/services/<service_id>/send/<template_id>.csv", methods=['GET'])
@login_required @login_required
def get_example_csv(service_id, template_id): def get_example_csv(service_id, template_id):
template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] template = templates_dao.get_service_template_or_404(service_id, template_id)['data']
placeholders = list(Template(template).placeholders) placeholders = list(Template(template).placeholders)
output = io.StringIO() output = io.StringIO()
writer = csv.writer(output) writer = csv.writer(output)
writer.writerow(['phone'] + placeholders) writer.writerow([first_column_header[template['template_type']]] + placeholders)
writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders]) writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders])
return(output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}) return(output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'})
@main.route("/services/<service_id>/sms/send/<template_id>/to-self", methods=['GET']) @main.route("/services/<service_id>/send/<template_id>/to-self", methods=['GET'])
@login_required @login_required
def send_sms_to_self(service_id, template_id): def send_message_to_self(service_id, template_id):
template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] template = templates_dao.get_service_template_or_404(service_id, template_id)['data']
placeholders = list(Template(template).placeholders) placeholders = list(Template(template).placeholders)
output = io.StringIO() output = io.StringIO()
writer = csv.writer(output) writer = csv.writer(output)
writer.writerow(['phone'] + placeholders) writer.writerow([first_column_header[template['template_type']]] + placeholders)
writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders]) writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders])
filedata = { filedata = {
'file_name': 'Test run', 'file_name': 'Test run',
@@ -121,35 +128,37 @@ def send_sms_to_self(service_id, template_id):
s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION']) s3upload(upload_id, service_id, filedata, current_app.config['AWS_REGION'])
session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']} session['upload_data'] = {"template_id": template_id, "original_file_name": filedata['file_name']}
return redirect(url_for('.check_sms', return redirect(url_for('.check_messages',
service_id=service_id, service_id=service_id,
upload_id=upload_id)) upload_id=upload_id))
@main.route("/services/<service_id>/sms/check/<upload_id>", @main.route("/services/<service_id>/check/<upload_id>",
methods=['GET', 'POST']) methods=['GET', 'POST'])
@login_required @login_required
def check_sms(service_id, upload_id): def check_messages(service_id, upload_id):
upload_data = session['upload_data']
template_id = upload_data.get('template_id')
if request.method == 'GET': if request.method == 'GET':
contents = s3download(service_id, upload_id) contents = s3download(service_id, upload_id)
if not contents: if not contents:
flash('There was a problem reading your upload file') flash('There was a problem reading your upload file')
upload_data = session['upload_data']
template_id = upload_data.get('template_id')
raw_template = templates_dao.get_service_template_or_404(service_id, template_id)['data'] raw_template = templates_dao.get_service_template_or_404(service_id, template_id)['data']
recipient_type = first_column_header[raw_template['template_type']]
upload_result = _get_rows(contents, raw_template) upload_result = _get_rows(contents, raw_template)
session['upload_data']['notification_count'] = len(upload_result['rows']) session['upload_data']['notification_count'] = len(upload_result['rows'])
template = Template( template = Template(
raw_template, raw_template,
values=upload_result['rows'][0] if upload_result['valid'] else {}, values=upload_result['rows'][0] if upload_result['valid'] else {},
drop_values={'phone'} drop_values={recipient_type}
) )
return render_template( return render_template(
'views/check-sms.html', 'views/check-sms.html',
upload_result=upload_result, upload_result=upload_result,
template=template, template=template,
column_headers=['phone number'] + list( column_headers=[recipient_type] + list(
template.placeholders if upload_result['valid'] else template.placeholders_as_markup template.placeholders if upload_result['valid'] else template.placeholders_as_markup
), ),
original_file_name=upload_data.get('original_file_name'), original_file_name=upload_data.get('original_file_name'),
@@ -157,9 +166,7 @@ def check_sms(service_id, upload_id):
form=CsvUploadForm() form=CsvUploadForm()
) )
elif request.method == 'POST': elif request.method == 'POST':
upload_data = session['upload_data']
original_file_name = upload_data.get('original_file_name') original_file_name = upload_data.get('original_file_name')
template_id = upload_data.get('template_id')
notification_count = upload_data.get('notification_count') notification_count = upload_data.get('notification_count')
session.pop('upload_data') session.pop('upload_data')
try: try:
@@ -171,9 +178,9 @@ def check_sms(service_id, upload_id):
raise e raise e
flash('Weve started sending your messages', 'default_with_tick') flash('Weve started sending your messages', 'default_with_tick')
return redirect(url_for('main.view_job', return redirect(
service_id=service_id, url_for('main.view_job', service_id=service_id, job_id=upload_id)
job_id=upload_id)) )
def _get_filedata(file): def _get_filedata(file):
@@ -196,8 +203,12 @@ def _get_rows(contents, raw_template):
for row in reader: for row in reader:
rows.append(row) rows.append(row)
try: try:
validate_phone_number(row['phone']) recipient_column = first_column_header[raw_template['template_type']]
Template(raw_template, values=row, drop_values={'phone'}).replaced validate_recipient(
except (InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError): row[recipient_column],
template_type=raw_template['template_type']
)
Template(raw_template, values=row, drop_values={recipient_column}).replaced
except (InvalidEmailError, InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError):
valid = False valid = False
return {"valid": valid, "rows": rows} return {"valid": valid, "rows": rows}

View File

@@ -7,10 +7,10 @@ from flask import (
flash flash
) )
from flask.ext.login import current_user from flask.ext.login import (current_user, login_fresh, confirm_login)
from app.main import main from app.main import main
from app.main.dao import users_dao from app.main.dao import (users_dao, services_dao)
from app.main.forms import LoginForm from app.main.forms import LoginForm
@@ -18,11 +18,24 @@ from app.main.forms import LoginForm
def sign_in(): def sign_in():
if current_user and current_user.is_authenticated(): if current_user and current_user.is_authenticated():
return redirect(url_for('main.choose_service')) return redirect(url_for('main.choose_service'))
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = users_dao.get_user_by_email(form.email_address.data) user = users_dao.get_user_by_email(form.email_address.data)
user = _get_and_verify_user(user, form.password.data) user = _get_and_verify_user(user, form.password.data)
if user: if user:
# Remember me login
if not login_fresh() and \
not current_user.is_anonymous() and \
current_user.id == user.id and \
user.is_active():
confirm_login()
services = services_dao.get_services(user.id).get('data', [])
if (len(services) == 1):
return redirect(url_for('main.service_dashboard', service_id=services[0]['id']))
else:
return redirect(url_for('main.choose_service'))
session['user_details'] = {"email": user.email_address, "id": user.id} session['user_details'] = {"email": user.email_address, "id": user.id}
if user.state == 'pending': if user.state == 'pending':
return redirect(url_for('.verify')) return redirect(url_for('.verify'))

View File

@@ -6,8 +6,7 @@ from app.main import main
@main.route('/sign-out', methods=(['GET'])) @main.route('/sign-out', methods=(['GET']))
@login_required
def sign_out(): def sign_out():
session.clear() session.clear()
logout_user() logout_user()
return redirect(url_for('main.index')) return redirect(url_for('main.sign_in'))

View File

@@ -5,45 +5,44 @@ from notifications_python_client.errors import HTTPError
from utils.template import Template from utils.template import Template
from app.main import main from app.main import main
from app.main.forms import TemplateForm from app.main.forms import SMSTemplateForm, EmailTemplateForm
from app import job_api_client from app import job_api_client
from app.main.dao.services_dao import get_service_by_id from app.main.dao.services_dao import get_service_by_id_or_404
from app.main.dao import templates_dao as tdao from app.main.dao import templates_dao as tdao
from app.main.dao import services_dao as sdao from app.main.dao import services_dao as sdao
@main.route("/services/<service_id>/templates") form_objects = {
'email': EmailTemplateForm,
'sms': SMSTemplateForm
}
@main.route("/services/<service_id>/templates/add-<template_type>", methods=['GET', 'POST'])
@login_required @login_required
def manage_service_templates(service_id): def add_service_template(service_id, template_type):
return redirect(url_for(
'.choose_sms_template',
service_id=service_id
))
service = sdao.get_service_by_id_or_404(service_id)
@main.route("/services/<service_id>/templates/add", methods=['GET', 'POST']) if template_type not in ['sms', 'email']:
@login_required abort(404)
def add_service_template(service_id):
try:
service = sdao.get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
form = TemplateForm() form = form_objects[template_type]()
if form.validate_on_submit(): if form.validate_on_submit():
tdao.insert_service_template( tdao.insert_service_template(
form.name.data, form.template_content.data, service_id) form.name.data, template_type, form.template_content.data, service_id, form.subject.data or None
return redirect(url_for( )
'.choose_sms_template', service_id=service_id)) return redirect(
url_for('.choose_template', service_id=service_id, template_type=template_type)
)
return render_template( return render_template(
'views/edit-template.html', 'views/edit-{}-template.html'.format(template_type),
h1='Add a text message template',
form=form, form=form,
service_id=service_id) template_type=template_type,
service_id=service_id
)
@main.route("/services/<service_id>/templates/<int:template_id>", methods=['GET', 'POST']) @main.route("/services/<service_id>/templates/<int:template_id>", methods=['GET', 'POST'])
@@ -51,20 +50,26 @@ def add_service_template(service_id):
def edit_service_template(service_id, template_id): def edit_service_template(service_id, template_id):
template = tdao.get_service_template_or_404(service_id, template_id)['data'] template = tdao.get_service_template_or_404(service_id, template_id)['data']
template['template_content'] = template['content'] template['template_content'] = template['content']
form = TemplateForm(**template) form = form_objects[template['template_type']](**template)
if form.validate_on_submit(): if form.validate_on_submit():
tdao.update_service_template( tdao.update_service_template(
template_id, form.name.data, template_id, form.name.data, template['template_type'],
form.template_content.data, service_id) form.template_content.data, service_id
return redirect(url_for('.choose_sms_template', service_id=service_id)) )
return redirect(url_for(
'.choose_template',
service_id=service_id,
template_type=template['template_type']
))
return render_template( return render_template(
'views/edit-template.html', 'views/edit-{}-template.html'.format(template['template_type']),
h1='Edit template',
form=form, form=form,
service_id=service_id, service_id=service_id,
template_id=template_id) template_id=template_id,
template_type=template['template_type']
)
@main.route("/services/<service_id>/templates/<int:template_id>/delete", methods=['GET', 'POST']) @main.route("/services/<service_id>/templates/<int:template_id>/delete", methods=['GET', 'POST'])
@@ -74,13 +79,17 @@ def delete_service_template(service_id, template_id):
if request.method == 'POST': if request.method == 'POST':
tdao.delete_service_template(service_id, template_id) tdao.delete_service_template(service_id, template_id)
return redirect(url_for('.manage_service_templates', service_id=service_id)) return redirect(url_for(
'.choose_template',
service_id=service_id,
template_type=template['template_type']
))
template['template_content'] = template['content'] template['template_content'] = template['content']
form = TemplateForm(**template) form = form_objects[template['template_type']](**template)
flash('Are you sure you want to delete {}?'.format(form.name.data), 'delete') flash('Are you sure you want to delete {}?'.format(form.name.data), 'delete')
return render_template( return render_template(
'views/edit-template.html', 'views/edit-{}-template.html'.format(template['template_type']),
h1='Edit template', h1='Edit template',
form=form, form=form,
service_id=service_id, service_id=service_id,

View File

@@ -12,7 +12,10 @@ from app.main.forms import TwoFactorForm
@main.route('/two-factor', methods=['GET', 'POST']) @main.route('/two-factor', methods=['GET', 'POST'])
def two_factor(): def two_factor():
# TODO handle user_email not in session # TODO handle user_email not in session
user_id = session['user_details']['id'] try:
user_id = session['user_details']['id']
except KeyError:
return redirect('main.sign_in')
def _check_code(code): def _check_code(code):
return users_dao.check_verify_code(user_id, code, "sms") return users_dao.check_verify_code(user_id, code, "sms")
@@ -27,7 +30,7 @@ def two_factor():
if 'password' in session['user_details']: if 'password' in session['user_details']:
user.set_password(session['user_details']['password']) user.set_password(session['user_details']['password'])
users_dao.update_user(user) users_dao.update_user(user)
login_user(user) login_user(user, remember=form.remember_me.data if form.remember_me.data else False)
finally: finally:
del session['user_details'] del session['user_details']
if (len(services) == 1): if (len(services) == 1):

View File

@@ -70,7 +70,7 @@ class NotificationsAdminAPIClient(NotificationsAPIClient):
endpoint = "/service/{0}".format(service_id) endpoint = "/service/{0}".format(service_id)
return self.put(endpoint, data) return self.put(endpoint, data)
def create_service_template(self, name, type_, content, service_id): def create_service_template(self, name, type_, content, service_id, subject=None):
""" """
Create a service template. Create a service template.
""" """
@@ -80,10 +80,14 @@ class NotificationsAdminAPIClient(NotificationsAPIClient):
"content": content, "content": content,
"service": service_id "service": service_id
} }
if subject:
data.update({
'subject': subject
})
endpoint = "/service/{0}/template".format(service_id) endpoint = "/service/{0}/template".format(service_id)
return self.post(endpoint, data) return self.post(endpoint, data)
def update_service_template(self, id_, name, type_, content, service_id): def update_service_template(self, id_, name, type_, content, service_id, subject=None):
""" """
Update a service template. Update a service template.
""" """
@@ -94,8 +98,12 @@ class NotificationsAdminAPIClient(NotificationsAPIClient):
'content': content, 'content': content,
'service': service_id 'service': service_id
} }
if subject:
data.update({
'subject': subject
})
endpoint = "/service/{0}/template/{1}".format(service_id, id_) endpoint = "/service/{0}/template/{1}".format(service_id, id_)
return self.put(endpoint, data) return self.post(endpoint, data)
def get_service_template(self, service_id, template_id, *params): def get_service_template(self, service_id, template_id, *params):
""" """

View File

@@ -1,7 +1,7 @@
from notifications_python_client.notifications import BaseAPIClient from notifications_python_client.notifications import BaseAPIClient
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
from flask.ext.login import UserMixin from flask.ext.login import (UserMixin, login_fresh)
class UserApiClient(BaseAPIClient): class UserApiClient(BaseAPIClient):
@@ -81,6 +81,11 @@ class UserApiClient(BaseAPIClient):
return False, 'Code not found' return False, 'Code not found'
raise e raise e
def get_users_for_service(self, service_id):
endpoint = '/service/{}/users'.format(service_id)
resp = self.get(endpoint)
return resp['data']
class User(UserMixin): class User(UserMixin):
def __init__(self, fields, max_failed_login_count=3): def __init__(self, fields, max_failed_login_count=3):
@@ -100,6 +105,12 @@ class User(UserMixin):
def is_active(self): def is_active(self):
return self.state == 'active' return self.state == 'active'
def is_authenticated(self):
# To handle remember me token renewal
if not login_fresh():
return False
return super(User, self).is_authenticated()
@property @property
def id(self): def id(self):
return self._id return self._id

View File

@@ -0,0 +1,30 @@
{% macro checkbox(
field,
hint=False,
help_link=None,
help_link_text=None,
width='2-3',
suffix=None
) %}
<label class="form-checkbox" for="{{ field.name }}">
{{ field.label }}
{% if hint %}
<span class="form-hint">
{{ hint }}
</span>
{% endif %}
{% if field.errors %}
<span class="error-message">
{{ field.errors[0] }}
</span>
{% endif %}
{{ field(**{
'class': 'form-control form-control-{} textbox-highlight-textbox'.format(width) if highlight_tags else 'form-control form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else ''),
'data-module': 'highlight-tags' if highlight_tags else ''})}}
{% if help_link and help_link_text %}
<p class="textbox-help-link">
<a href='{{ help_link }}'>{{ help_link_text }}</a>
</p>
{% endif %}
</label>
{% endmacro %}

View File

@@ -9,29 +9,27 @@
</h3> </h3>
{% endif %} {% endif %}
<div class="email-message"> <div class="email-message">
{% if from_name and from_address %} {% if from_name or subject %}
<div class="email-message-from"> <table class="email-message-meta">
<div class="grid-row"> <tbody>
<div class="column-one-eighth"> {% if from_name and from_address %}
<span class="form-hint">From</span> <tr>
</div> <th>From</th>
<div class="column-seven-eighths"> <td>
{{ from_name }} &lt;{{ from_address }}&gt; {{ from_name }} &lt;{{ from_address }}&gt;
</div> </td>
</div> </tr>
</div> {% endif %}
{% endif %} {% if subject %}
{% if subject %} <tr class="email-message-meta">
<div class="email-message-subject"> <th>Subject</th>
<div class="grid-row"> <td>
<div class="column-one-eighth"> {{ subject }}
<span class="form-hint">Subject</span> </td>
</div> </div>
<div class="column-seven-eighths"> {% endif %}
{{ subject }} </tbody>
</div> </table>
</div>
</div>
{% endif %} {% endif %}
<div class="email-message-body"> <div class="email-message-body">
{{ body|nl2br }} {{ body|nl2br }}

View File

@@ -3,8 +3,8 @@
<a href="{{ url_for('.service_dashboard', service_id=service_id) }}">{{ session.get('service_name', 'Service') }}</a> <a href="{{ url_for('.service_dashboard', service_id=service_id) }}">{{ session.get('service_name', 'Service') }}</a>
</h2> </h2>
<ul> <ul>
<li><a href="{{ url_for('.choose_sms_template', service_id=service_id) }}">Send text messages</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.send_email', service_id=service_id) }}">Send emails</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li>
</ul> </ul>
<ul> <ul>
<li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage team</a></li> <li><a href="{{ url_for('.manage_users', service_id=service_id) }}">Manage team</a></li>

View File

@@ -42,7 +42,7 @@
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="button" value="{{ "Send {} text message{}".format(upload_result.rows|count, '' if upload_result.rows|count == 1 else 's') }}" /> <input type="submit" class="button" value="{{ "Send {} text message{}".format(upload_result.rows|count, '' if upload_result.rows|count == 1 else 's') }}" />
<a href="{{url_for('.send_sms', service_id=service_id, template_id=template.id)}}" class="page-footer-back-link">Back</a> <a href="{{url_for('.send_messages', service_id=service_id, template_id=template.id)}}" class="page-footer-back-link">Back</a>
</form> </form>
{% else %} {% else %}
{{file_upload(form.file, button_text='Upload a CSV file')}} {{file_upload(form.file, button_text='Upload a CSV file')}}

View File

@@ -0,0 +1,36 @@
{% extends "withnav_template.html" %}
{% from "components/email-message.html" import email_message %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/textbox.html" import textbox %}
{% block page_title %}
Send emails GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">Send emails</h1>
<form method="POST" enctype="multipart/form-data">
{% if templates %}
<div class="grid-row">
{% for template in templates %}
<div class="column-two-thirds">
{{ email_message(template.subject, template.formatted_as_markup, name=template.name) }}
</div>
<div class="column-one-third">
<div class="sms-message-use-links">
<a href="{{ url_for(".edit_service_template", service_id=service_id, template_id=template.id) }}">Edit template</a>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<p>
<a href="{{ url_for('.add_service_template', service_id=service_id, template_type='email') }}" class="button">Add a new template</a>
</p>
</form>
{% endblock %}

View File

@@ -30,8 +30,8 @@
</div> </div>
<div class="column-one-third"> <div class="column-one-third">
<div class="sms-message-use-links"> <div class="sms-message-use-links">
<a href="{{ url_for(".send_sms", service_id=service_id, template_id=template.id) }}">Add recipients</a> <a href="{{ url_for(".send_messages", service_id=service_id, template_id=template.id) }}">Add recipients</a>
<a href="{{ url_for(".send_sms_to_self", service_id=service_id, template_id=template.id) }}">Send yourself a test</a> <a href="{{ url_for(".send_message_to_self", service_id=service_id, template_id=template.id) }}">Send yourself a test</a>
<a href="{{ url_for(".edit_service_template", service_id=service_id, template_id=template.id) }}">Edit template</a> <a href="{{ url_for(".edit_service_template", service_id=service_id, template_id=template.id) }}">Edit template</a>
</div> </div>
</div> </div>
@@ -40,7 +40,7 @@
{% endif %} {% endif %}
<p> <p>
<a href="{{ url_for('.add_service_template', service_id=service_id) }}" class="button">Add a new template</a> <a href="{{ url_for('.add_service_template', service_id=service_id, template_type='sms') }}" class="button">Add a new template</a>
</p> </p>
</form> </form>

View File

@@ -8,12 +8,15 @@
{% block maincolumn_content %} {% block maincolumn_content %}
<h1 class="heading-large">{{ h1 }}</h1> <h1 class="heading-large">Edit email template</h1>
<form method="post"> <form method="post">
<div class="grid-row"> <div class="grid-row">
<div class="column-two-thirds"> <div class="column-two-thirds">
{{ textbox(form.name, width='1-1') }} {{ textbox(form.name, width='1-1') }}
{% if 'email' == template_type %}
{{ textbox(form.subject, width='1-1') }}
{% endif %}
</div> </div>
</div> </div>
<div class="grid-row"> <div class="grid-row">
@@ -31,7 +34,7 @@
'Save', 'Save',
delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None, delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None,
delete_link_text='Delete this template', delete_link_text='Delete this template',
back_link=url_for('.choose_sms_template', service_id=service_id), back_link=url_for('.choose_template', template_type=template_type, service_id=service_id),
back_link_text='Cancel' back_link_text='Cancel'
) }} ) }}
</form> </form>

View File

@@ -0,0 +1,43 @@
{% extends "withnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
{{ h1 }} GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">Edit text message template</h1>
<form method="post">
<div class="grid-row">
<div class="column-two-thirds">
{{ textbox(form.name, width='1-1') }}
{% if 'email' == template_type %}
{{ textbox(form.subject, width='1-1') }}
{% endif %}
</div>
</div>
<div class="grid-row">
<div class="column-two-thirds">
{{ textbox(form.template_content, highlight_tags=True, width='1-1') }}
</div>
<div class="column-one-third">
<label for='template_content' class='edit-template-placeholder-hint'>
Add placeholders using double brackets, eg Your thing
is due on ((date))
</label>
</div>
</div>
{{ page_footer(
'Save',
delete_link=url_for('.delete_service_template', service_id=service_id, template_id=template_id) if template_id or None,
delete_link_text='Delete this template',
back_link=url_for('.choose_template', service_id=service_id, template_type=template_type),
back_link_text='Cancel'
) }}
</form>
{% endblock %}

View File

@@ -26,13 +26,13 @@ Manage users GOV.UK Notify
users, caption='Active', **table_options users, caption='Active', **table_options
) %} ) %}
{% call field() %} {% call field() %}
{{ current_user.name }} {{ item.name }}
{% endcall %} {% endcall %}
{{ boolean_field(item.permission_send_messages) }} {{ boolean_field(item.permission_send_messages) }}
{{ boolean_field(item.permission_manage_service) }} {{ boolean_field(item.permission_manage_service) }}
{{ boolean_field(item.permission_manage_api_keys) }} {{ boolean_field(item.permission_manage_api_keys) }}
{% call field(align='right') %} {% call field(align='right') %}
<a href="{{ url_for('.edit_user', service_id=service_id, user_id=0)}}">Change</a> <a href="{{ url_for('.edit_user', service_id=service_id, user_id=item.id)}}">Change</a>
{% endcall %} {% endcall %}
{% endcall %} {% endcall %}

View File

@@ -1,17 +0,0 @@
{% extends "withnav_template.html" %}
{% block page_title %}
Send email GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">Send email</h1>
<p>This page will be where we construct email messages</p>
<p>
<a class="button" href="check-email" role="button">Continue</a>
</p>
{% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/sms-message.html" import sms_message %} {% from "components/sms-message.html" import sms_message %}
{% from "components/email-message.html" import email_message %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/file-upload.html" import file_upload %} {% from "components/file-upload.html" import file_upload %}
{% from "components/table.html" import list_table, field %} {% from "components/table.html" import list_table, field %}
@@ -14,14 +15,22 @@
<div class="grid-row"> <div class="grid-row">
<div class="column-two-thirds"> <div class="column-two-thirds">
{% if 'sms' == template.template_type %}
{{ sms_message(template.formatted_as_markup) }} {{ sms_message(template.formatted_as_markup) }}
{% elif 'email' == template.template_type %}
{{ email_message(
template.subject,
template.formatted_as_markup,
from_address='{}@notifications.service.gov.uk'.format(service.email_from),
from_name=service.name
) }}
{% endif %}
{{ banner( {{ banner(
'You can upload real data, but well only send to your mobile number until you <a href="{}">request to go live</a>'|safe, 'You can upload real data, but well only send to your mobile number until you <a href="{}">request to go live</a>'.format(
url_for('.service_request_to_go_live', service_id=service_id)
)|safe,
'info' 'info'
)}} )}}
</div> </div>
</div> </div>

View File

@@ -36,7 +36,7 @@
</ol> </ol>
""".format( """.format(
url_for(".add_service_template", service_id=service_id), url_for(".add_service_template", service_id=service_id),
url_for(".choose_sms_template", service_id=service_id) url_for(".choose_template", service_id=service_id, template_type="sms")
)|safe, )|safe,
subhead='Get started', subhead='Get started',
type="tip" type="tip"
@@ -46,7 +46,7 @@
""" """
<a href='{}'>Send yourself a text message</a> <a href='{}'>Send yourself a text message</a>
""".format( """.format(
url_for(".choose_sms_template", service_id=service_id) url_for(".choose_template", service_id=service_id, template_type="sms")
)|safe, )|safe,
subhead='Next step', subhead='Next step',
type="tip" type="tip"

View File

@@ -1,5 +1,6 @@
{% extends "withoutnav_template.html" %} {% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
@@ -22,6 +23,7 @@
help_link=url_for('.verification_code_not_received'), help_link=url_for('.verification_code_not_received'),
help_link_text='I havent received a text message' help_link_text='I havent received a text message'
) }} ) }}
{{ checkbox(form.remember_me) }}
{{ page_footer( {{ page_footer(
"Continue" "Continue"
) }} ) }}

View File

@@ -1,3 +1,5 @@
import re
from functools import wraps from functools import wraps
from flask import abort from flask import abort
@@ -28,6 +30,11 @@ class BrowsableItem(object):
pass pass
class InvalidEmailError(Exception):
def __init__(self, message):
self.message = message
class InvalidPhoneError(Exception): class InvalidPhoneError(Exception):
def __init__(self, message): def __init__(self, message):
self.message = message self.message = message
@@ -74,6 +81,19 @@ def format_phone_number(number):
return '+447{}{}{}'.format(*re.findall('...', number)) return '+447{}{}{}'.format(*re.findall('...', number))
def validate_email_address(email_address):
if re.match(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)", email_address):
return
raise InvalidEmailError('Not a valid email address')
def validate_recipient(recipient, template_type):
return {
'email': validate_email_address,
'sms': validate_phone_number
}[template_type](recipient)
def user_has_permissions(*permissions): def user_has_permissions(*permissions):
def wrap(func): def wrap(func):
@wraps(func) @wraps(func)

View File

@@ -1,4 +1,5 @@
import os import os
from datetime import timedelta
class Config(object): class Config(object):
@@ -21,9 +22,14 @@ class Config(object):
SESSION_COOKIE_NAME = 'notify_admin_session' SESSION_COOKIE_NAME = 'notify_admin_session'
SESSION_COOKIE_PATH = '/admin' SESSION_COOKIE_PATH = '/admin'
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = False SESSION_COOKIE_SECURE = True
PERMANENT_SESSION_LIFETIME = 3600 # seconds PERMANENT_SESSION_LIFETIME = 3600 # seconds
SESSION_REFRESH_EACH_REQUEST = True SESSION_REFRESH_EACH_REQUEST = True
REMEMBER_COOKIE_NAME = 'notify_admin_remember_me'
REMEMBER_COOKIE_PATH = '/admin'
REMEMBER_COOKIE_DURATION = timedelta(days=1)
REMEMBER_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_SECURE = True
API_HOST_NAME = os.getenv('API_HOST_NAME') API_HOST_NAME = os.getenv('API_HOST_NAME')
NOTIFY_API_SECRET = os.getenv('NOTIFY_API_SECRET', "dev-secret") NOTIFY_API_SECRET = os.getenv('NOTIFY_API_SECRET', "dev-secret")
@@ -54,6 +60,8 @@ class Development(Config):
ADMIN_CLIENT_USER_NAME = 'dev-notify-admin' ADMIN_CLIENT_USER_NAME = 'dev-notify-admin'
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key' ADMIN_CLIENT_SECRET = 'dev-notify-secret-key'
WTF_CSRF_ENABLED = False WTF_CSRF_ENABLED = False
REMEMBER_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
class Test(Development): class Test(Development):
@@ -64,7 +72,6 @@ class Test(Development):
class Preview(Config): class Preview(Config):
DEBUG = False DEBUG = False
HTTP_PROTOCOL = 'https' HTTP_PROTOCOL = 'https'
SESSION_COOKIE_SECURE = True
HEADER_COLOUR = '#F47738' # $orange HEADER_COLOUR = '#F47738' # $orange

View File

@@ -12,6 +12,6 @@ credstash==1.8.0
boto3==1.2.3 boto3==1.2.3
Pygments==2.0.2 Pygments==2.0.2
git+https://github.com/alphagov/notifications-python-client.git@0.2.5#egg=notifications-python-client==0.2.7 git+https://github.com/alphagov/notifications-python-client.git@0.2.8#egg=notifications-python-client==0.2.8
git+https://github.com/alphagov/notifications-utils.git@0.1.0#egg=notifications-utils==0.1.0 git+https://github.com/alphagov/notifications-utils.git@0.1.1#egg=notifications-utils==0.1.1

View File

@@ -6,7 +6,8 @@ def test_should_show_overview_page(
app_, app_,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_service mock_get_service,
mock_get_users_by_service
): ):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -15,6 +16,7 @@ def test_should_show_overview_page(
assert 'Manage team' in response.get_data(as_text=True) assert 'Manage team' in response.get_data(as_text=True)
assert response.status_code == 200 assert response.status_code == 200
mock_get_users_by_service.assert_called_once_with(service_id='55555')
def test_should_show_page_for_one_user( def test_should_show_page_for_one_user(
@@ -35,7 +37,8 @@ def test_redirect_after_saving_user(
app_, app_,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_service mock_get_service,
mock_get_users_by_service
): ):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -69,7 +72,8 @@ def test_invite_user(
app_, app_,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_service mock_get_service,
mock_get_users_by_service
): ):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:

View File

@@ -1,56 +1,70 @@
from io import BytesIO from io import BytesIO
from flask import url_for from flask import url_for
import pytest
import moto import moto
template_types = ['email', 'sms']
def test_choose_sms_template(app_,
api_user_active, @pytest.mark.parametrize("template_type", template_types)
mock_login, def test_choose_template(
mock_get_user, template_type,
mock_check_verify_code, app_,
mock_get_service_templates, api_user_active,
mock_get_jobs): mock_login,
mock_get_user,
mock_get_service,
mock_check_verify_code,
mock_get_service_templates,
mock_get_jobs
):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
response = client.get(url_for('main.choose_sms_template', service_id=12345)) response = client.get(url_for('main.choose_template', template_type=template_type, service_id=12345))
assert response.status_code == 200 assert response.status_code == 200
content = response.get_data(as_text=True) content = response.get_data(as_text=True)
assert 'template_one' in content assert '{}_template_one'.format(template_type) in content
assert 'template one content' in content assert '{} template one content'.format(template_type) in content
assert 'template_two' in content assert '{}_template_two'.format(template_type) in content
assert 'template two content' in content assert '{} template two content'.format(template_type) in content
def test_upload_empty_csvfile_returns_to_upload_page(app_, def test_upload_empty_csvfile_returns_to_upload_page(
api_user_active, app_,
mock_login, api_user_active,
mock_get_user, mock_login,
mock_get_service_templates, mock_get_user,
mock_check_verify_code, mock_get_service,
mock_get_service_template): mock_get_service_templates,
mock_check_verify_code,
mock_get_service_template
):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
upload_data = {'file': (BytesIO(''.encode('utf-8')), 'emtpy.csv')} upload_data = {'file': (BytesIO(''.encode('utf-8')), 'emtpy.csv')}
response = client.post(url_for('main.send_sms', service_id=12345, template_id=54321), response = client.post(
data=upload_data, follow_redirects=True) url_for('main.send_messages', service_id=12345, template_id=54321),
data=upload_data,
follow_redirects=True
)
assert response.status_code == 200 assert response.status_code == 200
content = response.get_data(as_text=True) content = response.get_data(as_text=True)
assert 'The file emtpy.csv contained no data' in content assert 'The file emtpy.csv contained no data' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3 @moto.mock_s3
def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(app_, def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(
mocker, app_,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_user, mock_get_service_template
mock_get_user_by_email, ):
mock_get_service_template):
contents = 'phone\n+44 123\n+44 456' contents = 'phone\n+44 123\n+44 456'
file_data = (BytesIO(contents.encode('utf-8')), 'invalid.csv') file_data = (BytesIO(contents.encode('utf-8')), 'invalid.csv')
@@ -59,9 +73,11 @@ def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(app_,
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
upload_data = {'file': file_data} upload_data = {'file': file_data}
response = client.post(url_for('main.send_sms', service_id=12345, template_id=54321), response = client.post(
data=upload_data, url_for('main.send_messages', service_id=12345, template_id=54321),
follow_redirects=True) data=upload_data,
follow_redirects=True
)
assert response.status_code == 200 assert response.status_code == 200
content = response.get_data(as_text=True) content = response.get_data(as_text=True)
assert 'Your CSV file contained missing or invalid data' in content assert 'Your CSV file contained missing or invalid data' in content
@@ -70,6 +86,7 @@ def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(app_,
assert 'Upload a CSV file' in content assert 'Upload a CSV file' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3 @moto.mock_s3
def test_send_test_message_to_self( def test_send_test_message_to_self(
app_, app_,
@@ -85,7 +102,7 @@ def test_send_test_message_to_self(
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
response = client.get( response = client.get(
url_for('main.send_sms_to_self', service_id=12345, template_id=54321), url_for('main.send_message_to_self', service_id=12345, template_id=54321),
follow_redirects=True follow_redirects=True
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -94,6 +111,7 @@ def test_send_test_message_to_self(
assert '+4412341234' in content assert '+4412341234' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3 @moto.mock_s3
def test_download_example_csv( def test_download_example_csv(
app_, app_,
@@ -117,14 +135,17 @@ def test_download_example_csv(
assert 'text/csv' in response.headers['Content-Type'] assert 'text/csv' in response.headers['Content-Type']
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3 @moto.mock_s3
def test_upload_csvfile_with_valid_phone_shows_all_numbers(app_, def test_upload_csvfile_with_valid_phone_shows_all_numbers(
mocker, app_,
api_user_active, mocker,
mock_login, api_user_active,
mock_get_user, mock_login,
mock_get_user_by_email, mock_get_user,
mock_get_service_template): mock_get_user_by_email,
mock_get_service_template
):
contents = 'phone\n+44 7700 900981\n+44 7700 900982\n+44 7700 900983\n+44 7700 900984\n+44 7700 900985\n+44 7700 900986' # noqa contents = 'phone\n+44 7700 900981\n+44 7700 900982\n+44 7700 900983\n+44 7700 900984\n+44 7700 900985\n+44 7700 900986' # noqa
@@ -134,7 +155,7 @@ def test_upload_csvfile_with_valid_phone_shows_all_numbers(app_,
with app_.test_client() as client: with app_.test_client() as client:
client.login(api_user_active) client.login(api_user_active)
upload_data = {'file': file_data} upload_data = {'file': file_data}
response = client.post(url_for('main.send_sms', service_id=12345, template_id=54321), response = client.post(url_for('main.send_messages', service_id=12345, template_id=54321),
data=upload_data, data=upload_data,
follow_redirects=True) follow_redirects=True)
with client.session_transaction() as sess: with client.session_transaction() as sess:
@@ -153,17 +174,20 @@ def test_upload_csvfile_with_valid_phone_shows_all_numbers(app_,
assert '+44 7700 900986' in content assert '+44 7700 900986' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3 @moto.mock_s3
def test_create_job_should_call_api(app_, def test_create_job_should_call_api(
service_one, app_,
api_user_active, service_one,
mock_get_user, api_user_active,
mock_get_user_by_email, mock_get_user,
mock_login, mock_get_user_by_email,
job_data, mock_login,
mock_create_job, job_data,
mock_get_job, mock_create_job,
mock_get_service_template): mock_get_job,
mock_get_service_template
):
service_id = service_one['id'] service_id = service_one['id']
job_id = job_data['id'] job_id = job_data['id']
@@ -178,7 +202,7 @@ def test_create_job_should_call_api(app_,
session['upload_data'] = {'original_file_name': original_file_name, session['upload_data'] = {'original_file_name': original_file_name,
'template_id': template_id, 'template_id': template_id,
'notification_count': notification_count} 'notification_count': notification_count}
url = url_for('main.check_sms', service_id=service_one['id'], upload_id=job_id) url = url_for('main.check_messages', service_id=service_one['id'], upload_id=job_id)
response = client.post(url, data=job_data, follow_redirects=True) response = client.post(url, data=job_data, follow_redirects=True)
assert response.status_code == 200 assert response.status_code == 200

View File

@@ -90,3 +90,26 @@ def test_should_return_redirect_when_user_is_pending(app_,
'password': 'val1dPassw0rd!'}) 'password': 'val1dPassw0rd!'})
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True) assert response.location == url_for('main.verify', _external=True)
def test_not_fresh_session_login(app_,
api_user_active,
mock_login,
mock_get_user_by_email,
mock_verify_password,
mock_get_services_with_one_service):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
with client.session_transaction() as session:
assert session['_fresh']
session['_fresh'] = False
# This should skip the two factor
response = client.post(
url_for('main.sign_in'), data={
'email_address': api_user_active.email_address,
'password': 'val1dPassw0rd!'})
assert response.status_code == 302
service_dct = mock_get_services_with_one_service(api_user_active.id)['data'][0]
assert response.location == url_for(
'main.service_dashboard', service_id=service_dct['id'], _external=True)

View File

@@ -7,7 +7,7 @@ def test_render_sign_out_redirects_to_sign_in(app_):
url_for('main.sign_out')) url_for('main.sign_out'))
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for( assert response.location == url_for(
'main.sign_in', _external=True, next=url_for('main.sign_out')) 'main.sign_in', _external=True)
def test_sign_out_user(app_, def test_sign_out_user(app_,
@@ -22,9 +22,9 @@ def test_sign_out_user(app_,
email = 'valid@example.gov.uk' email = 'valid@example.gov.uk'
password = 'val1dPassw0rd!' password = 'val1dPassw0rd!'
with app_.test_client() as client: with app_.test_client() as client:
with client.session_transaction() as session:
print('session: {}'.format(session))
client.login(api_user_active) client.login(api_user_active)
with client.session_transaction() as session:
assert session.get('user_id') is not None
# Check we are logged in # Check we are logged in
response = client.get( response = client.get(
url_for('main.service_dashboard', service_id="123")) url_for('main.service_dashboard', service_id="123"))
@@ -32,5 +32,6 @@ def test_sign_out_user(app_,
response = client.get(url_for('main.sign_out')) response = client.get(url_for('main.sign_out'))
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for( assert response.location == url_for(
'main.index', _external=True) 'main.sign_in', _external=True)
assert session.get('ItsdangerousSession') is None with client.session_transaction() as session:
assert session.get('user_id') is None

View File

@@ -4,24 +4,6 @@ import uuid
from flask import url_for from flask import url_for
def test_should_return_list_of_all_templates(app_,
api_user_active,
mock_get_service_templates,
mock_get_user,
mock_get_user_by_email,
mock_login,
mock_get_jobs):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
service_id = str(uuid.uuid4())
response = client.get(url_for(
'.manage_service_templates', service_id=service_id), follow_redirects=True)
assert response.status_code == 200
mock_get_service_templates.assert_called_with(service_id)
def test_should_show_page_for_one_templates(app_, def test_should_show_page_for_one_templates(app_,
api_user_active, api_user_active,
mock_get_service_template, mock_get_service_template,
@@ -62,8 +44,9 @@ def test_should_redirect_when_saving_a_template(app_,
data = { data = {
'id': template_id, 'id': template_id,
'name': name, 'name': name,
"template_content": content, 'template_content': content,
"service": service_id 'type': 'sms',
'service': service_id
} }
response = client.post(url_for( response = client.post(url_for(
'.edit_service_template', '.edit_service_template',
@@ -72,7 +55,7 @@ def test_should_redirect_when_saving_a_template(app_,
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for( assert response.location == url_for(
'.choose_sms_template', service_id=service_id, _external=True) '.choose_template', service_id=service_id, template_type='sms', _external=True)
mock_update_service_template.assert_called_with( mock_update_service_template.assert_called_with(
template_id, name, 'sms', content, service_id) template_id, name, 'sms', content, service_id)
@@ -127,12 +110,13 @@ def test_should_redirect_when_deleting_a_template(app_,
response = client.post(url_for( response = client.post(url_for(
'.delete_service_template', '.delete_service_template',
service_id=service_id, service_id=service_id,
template_id=template_id), data=data) template_id=template_id
), data=data)
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for( assert response.location == url_for(
'.manage_service_templates', '.choose_template',
service_id=service_id, _external=True) service_id=service_id, template_type=type_, _external=True)
mock_get_service_template.assert_called_with( mock_get_service_template.assert_called_with(
service_id, template_id) service_id, template_id)
mock_delete_service_template.assert_called_with( mock_delete_service_template.assert_called_with(

View File

@@ -92,3 +92,20 @@ def test_should_login_user_when_multiple_valid_codes_exist(app_,
response = client.post(url_for('main.two_factor'), response = client.post(url_for('main.two_factor'),
data={'sms_code': '23456'}) data={'sms_code': '23456'})
assert response.status_code == 302 assert response.status_code == 302
def test_remember_me_set(app_,
api_user_active,
mock_get_user,
mock_get_user_by_email,
mock_check_verify_code,
mock_get_services_with_one_service):
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.two_factor'),
data={'sms_code': '23456', 'remember_me': True})
assert response.status_code == 302

View File

@@ -163,11 +163,20 @@ def mock_update_service_template(mocker):
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_get_service_templates(mocker): def mock_get_service_templates(mocker):
def _create(service_id): def _create(service_id):
template_one = template_json( return {'data': [
1, "template_one", "sms", "template one content", service_id) template_json(
template_two = template_json( 1, "sms_template_one", "sms", "sms template one content", service_id
2, "template_two", "sms", "template two content", service_id) ),
return {'data': [template_one, template_two]} template_json(
2, "sms_template_two", "sms", "sms template two content", service_id
),
template_json(
3, "email_template_one", "email", "email template one content", service_id
),
template_json(
4, "email_template_two", "email", "email template two content", service_id
)
]}
return mocker.patch( return mocker.patch(
'app.notifications_api_client.get_service_templates', 'app.notifications_api_client.get_service_templates',
@@ -501,3 +510,19 @@ def mock_get_jobs(mocker):
data.append(job_data) data.append(job_data)
return {"data": data} return {"data": data}
return mocker.patch('app.job_api_client.get_job', side_effect=_get_jobs) return mocker.patch('app.job_api_client.get_job', side_effect=_get_jobs)
@pytest.fixture(scope='function')
def mock_get_users_by_service(mocker):
def _get_users_for_service(service_id):
data = [{'id': 1,
'logged_in_at': None,
'mobile_number': '+447700900986',
'permissions': [],
'state': 'active',
'password_changed_at': None,
'name': 'Test User',
'email_address': 'notify@digital.cabinet-office.gov.uk',
'failed_login_count': 0}]
return data
return mocker.patch('app.user_api_client.get_users_for_service', side_effect=_get_users_for_service, autospec=True)