Merge branch 'master' of github.com:alphagov/notifications-admin

This commit is contained in:
Martyn Inglis
2016-03-14 17:06:19 +00:00
50 changed files with 611 additions and 519 deletions

View File

@@ -1,14 +1,6 @@
from flask import url_for, abort, current_app
from flask import url_for, current_app
from app import notifications_api_client
from app.utils import BrowsableItem
from notifications_python_client.errors import HTTPError
def insert_new_service(service_name, user_id):
resp = notifications_api_client.create_service(
service_name, False, current_app.config['DEFAULT_SERVICE_LIMIT'], True, user_id)
return resp['data']['id']
def update_service(service):
@@ -26,15 +18,7 @@ def get_service_by_id(id_):
def get_service_by_id_or_404(id_):
try:
return notifications_api_client.get_service(id_)['data']
except KeyError:
abort(404)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
return notifications_api_client.get_service(id_)['data']
def get_services(user_id=None):

View File

@@ -1,7 +1,6 @@
from flask import url_for, abort
from flask import url_for
from app import notifications_api_client
from app.utils import BrowsableItem
from notifications_python_client.errors import HTTPError
def insert_service_template(name, type_, content, service_id, subject=None):
@@ -19,13 +18,7 @@ def get_service_templates(service_id):
def get_service_template_or_404(service_id, template_id):
try:
return notifications_api_client.get_service_template(service_id, template_id)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
return notifications_api_client.get_service_template(service_id, template_id)
def delete_service_template(service_id, template_id):

View File

@@ -1,11 +1,6 @@
from datetime import datetime
from notifications_python_client import HTTPError
from sqlalchemy.orm import load_only
from app import login_manager
from app.main.encryption import hashpw
from app import user_api_client
#

View File

@@ -150,6 +150,23 @@ class TwoFactorForm(Form):
raise ValidationError(reason)
class VerifySmsForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
'''
Keyword arguments:
validate_code_func -- Validates the code with the API.
'''
self.validate_code_func = validate_code_func
super(VerifySmsForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data, 'sms')
if not is_valid:
raise ValidationError(reason)
class VerifyForm(Form):
def __init__(self, validate_code_func, *args, **kwargs):
'''
@@ -168,10 +185,12 @@ class VerifyForm(Form):
raise ValidationError(reason)
def validate_email_code(self, field):
self._validate_code(field.data, 'email')
if self.sms_code.data:
self._validate_code(field.data, 'email')
def validate_sms_code(self, field):
self._validate_code(field.data, 'sms')
if self.email_code.data:
self._validate_code(field.data, 'sms')
class EmailNotReceivedForm(Form):
@@ -205,7 +224,24 @@ class AddServiceForm(Form):
class ServiceNameForm(Form):
name = StringField(u'New name')
def __init__(self, names_func, *args, **kwargs):
"""
Keyword arguments:
names_func -- Returns a list of unique service_names already registered
on the system.
"""
self._names_func = names_func
super(ServiceNameForm, self).__init__(*args, **kwargs)
name = StringField(
u'New name',
validators=[
DataRequired(message='Service name cant be empty')
])
def validate_name(self, a):
if a.data in self._names_func():
raise ValidationError('This service name is already in use')
class ConfirmPasswordForm(Form):

View File

@@ -2,8 +2,8 @@ from flask import (
render_template,
redirect,
session,
url_for
)
url_for,
current_app)
from flask_login import login_required
@@ -14,8 +14,8 @@ from app.notify_client.models import InvitedUser
from app import (
invite_api_client,
user_api_client
)
user_api_client,
notifications_api_client)
@main.route("/add-service", methods=['GET', 'POST'])
@@ -36,8 +36,9 @@ def add_service():
heading = 'Which service do you want to set up notifications for?'
if form.validate_on_submit():
session['service_name'] = form.name.data
user = users_dao.get_user_by_id(session['user_id'])
service_id = services_dao.insert_new_service(session['service_name'], user.id)
service_id = notifications_api_client.create_service(
session['service_name'], False, current_app.config['DEFAULT_SERVICE_LIMIT'], True, session['user_id'])
return redirect(url_for('main.service_dashboard', service_id=service_id))
else:
return render_template(

View File

@@ -1,5 +1,4 @@
from flask import (
abort,
render_template,
session,
flash
@@ -9,36 +8,25 @@ from flask_login import login_required
from app.main import main
from app.main.dao.services_dao import get_service_by_id
from app.main.dao import templates_dao
from notifications_python_client.errors import HTTPError
from app import job_api_client
@main.route("/services/<service_id>/dashboard")
@login_required
def service_dashboard(service_id):
try:
templates = templates_dao.get_service_templates(service_id)['data']
jobs = job_api_client.get_job(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
try:
service = get_service_by_id(service_id)
session['service_name'] = service['data']['name']
session['service_id'] = service['data']['id']
templates = templates_dao.get_service_templates(service_id)['data']
jobs = job_api_client.get_job(service_id)['data']
service = get_service_by_id(service_id)
session['service_name'] = service['data']['name']
session['service_id'] = service['data']['id']
if session.get('invited_user'):
session.pop('invited_user', None)
service_name = service['data']['name']
message = 'You have successfully accepted your invitation and been added to {}'.format(service_name)
flash(message, 'default_with_tick')
if session.get('invited_user'):
session.pop('invited_user', None)
service_name = service['data']['name']
message = 'You have sucessfully accepted your invitation and been added to {}'.format(service_name)
flash(message, 'default_with_tick')
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
return render_template(
'views/service_dashboard.html',
jobs=jobs[:5],

View File

@@ -1,7 +1,6 @@
from flask import (
render_template,
)
from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.forms import ForgotPasswordForm
@@ -12,11 +11,8 @@ from app import user_api_client
def forgot_password():
form = ForgotPasswordForm()
if form.validate_on_submit():
try:
user_api_client.send_reset_password_url(form.email_address.data)
except HTTPError as e:
if e.status_code != 404:
raise e
user_api_client.send_reset_password_url(form.email_address.data)
return render_template('views/password-reset-sent.html')
return render_template('views/forgot-password.html', form=form)

View File

@@ -2,11 +2,9 @@ from flask import (
redirect,
url_for,
session,
abort,
render_template
)
flash,
render_template)
from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.dao.services_dao import get_service_by_id_or_404
@@ -18,32 +16,29 @@ from app import (
@main.route("/invitation/<token>")
def accept_invite(token):
invited_user = invite_api_client.check_token(token)
try:
if invited_user.status == 'cancelled':
from_user = user_api_client.get_user(invited_user.from_user)
service = get_service_by_id_or_404(invited_user.service)
return render_template('views/cancelled-invitation.html',
from_user=from_user.name,
service_name=service['name'])
invited_user = invite_api_client.check_token(token)
if invited_user.status == 'cancelled':
from_user = user_api_client.get_user(invited_user.from_user)
service = get_service_by_id_or_404(invited_user.service)
return render_template('views/cancelled-invitation.html',
from_user=from_user.name,
service_name=service['name'])
if invited_user.status == 'accepted':
session.pop('invited_user', None)
flash('You have already accepted this invitation', 'default')
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
session['invited_user'] = invited_user.serialize()
existing_user = user_api_client.get_user_by_email(invited_user.email_address)
session['invited_user'] = invited_user.serialize()
if existing_user:
if existing_user:
user_api_client.add_user_to_service(invited_user.service,
existing_user.id,
invited_user.permissions)
invite_api_client.accept_invite(invited_user.service, invited_user.id)
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
else:
return redirect(url_for('main.register_from_invite'))
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
user_api_client.add_user_to_service(invited_user.service,
existing_user.id,
invited_user.permissions)
invite_api_client.accept_invite(invited_user.service, invited_user.id)
return redirect(url_for('main.service_dashboard', service_id=invited_user.service))
else:
return redirect(url_for('main.register_from_invite'))

View File

@@ -8,7 +8,6 @@ from flask import (
jsonify
)
from flask_login import login_required
from notifications_python_client.errors import HTTPError
from utils.template import Template
from app import job_api_client, notification_api_client
@@ -20,89 +19,71 @@ from app.main.dao import services_dao
@main.route("/services/<service_id>/jobs")
@login_required
def view_jobs(service_id):
try:
jobs = job_api_client.get_job(service_id)['data']
return render_template(
'views/jobs/jobs.html',
jobs=jobs,
service_id=service_id
)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
jobs = job_api_client.get_job(service_id)['data']
return render_template(
'views/jobs/jobs.html',
jobs=jobs,
service_id=service_id
)
@main.route("/services/<service_id>/jobs/<job_id>")
@login_required
def view_job(service_id, job_id):
service = services_dao.get_service_by_id_or_404(service_id)
try:
job = job_api_client.get_job(service_id, job_id)['data']
template = templates_dao.get_service_template_or_404(service_id, job['template'])['data']
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
finished = job['status'] == 'finished'
return render_template(
'views/jobs/job.html',
notifications=notifications['notifications'],
counts={
'queued': 0 if finished else job['notification_count'],
'sent': job['notification_count'] if finished else 0,
'failed': 0,
'cost': u'£0.00'
},
uploaded_at=job['created_at'],
finished_at=job['updated_at'] if finished else None,
uploaded_file_name=job['original_file_name'],
template=Template(
template,
prefix=service['name'] if template['template_type'] == 'sms' else ''
),
service_id=service_id,
service=service,
job_id=job_id
)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
job = job_api_client.get_job(service_id, job_id)['data']
template = templates_dao.get_service_template_or_404(service_id, job['template'])['data']
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
finished = job['status'] == 'finished'
return render_template(
'views/jobs/job.html',
notifications=notifications['notifications'],
counts={
'queued': 0 if finished else job['notification_count'],
'sent': job['notification_count'] if finished else 0,
'failed': 0,
'cost': u'£0.00'
},
uploaded_at=job['created_at'],
finished_at=job['updated_at'] if finished else None,
uploaded_file_name=job['original_file_name'],
template=Template(
template,
prefix=service['name'] if template['template_type'] == 'sms' else ''
),
service_id=service_id,
service=service,
job_id=job_id
)
@main.route("/services/<service_id>/jobs/<job_id>.json")
@login_required
def view_job_updates(service_id, job_id):
service = services_dao.get_service_by_id_or_404(service_id)
try:
job = job_api_client.get_job(service_id, job_id)['data']
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
finished = job['status'] == 'finished'
return jsonify(**{
'counts': render_template(
'partials/jobs/count.html',
counts={
'queued': 0 if finished else job['notification_count'],
'sent': job['notification_count'] if finished else 0,
'failed': 0,
'cost': u'£0.00'
}
),
'notifications': render_template(
'partials/jobs/notifications.html',
notifications=notifications['notifications']
),
'status': render_template(
'partials/jobs/status.html',
uploaded_at=job['created_at'],
finished_at=job['updated_at'] if finished else None
),
})
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
job = job_api_client.get_job(service_id, job_id)['data']
notifications = notification_api_client.get_notifications_for_service(service_id, job_id)
finished = job['status'] == 'finished'
return jsonify(**{
'counts': render_template(
'partials/jobs/count.html',
counts={
'queued': 0 if finished else job['notification_count'],
'sent': job['notification_count'] if finished else 0,
'failed': 0,
'cost': u'£0.00'
}
),
'notifications': render_template(
'partials/jobs/notifications.html',
notifications=notifications['notifications']
),
'status': render_template(
'partials/jobs/status.html',
uploaded_at=job['created_at'],
finished_at=job['updated_at'] if finished else None
),
})
@main.route("/services/<service_id>/jobs/<job_id>/notification/<string:notification_id>")

View File

@@ -11,9 +11,6 @@ from flask_login import (
current_user
)
from notifications_python_client.errors import HTTPError
from app import user_api_client
from app.main import main
from app.main.forms import (
InviteUserForm,

View File

@@ -11,8 +11,6 @@ from flask import (
from flask.ext.login import current_user
from notifications_python_client.errors import HTTPError
from app.main import main
from app.main.dao import users_dao
from app.main.forms import (
@@ -65,17 +63,10 @@ def register_from_invite():
def _do_registration(form, service=None):
if users_dao.is_email_unique(form.email_address.data):
try:
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data,
form.password.data)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
user = user_api_client.register_user(form.name.data,
form.email_address.data,
form.mobile_number.data,
form.password.data)
# TODO possibly there should be some exception handling
# for sending sms and email codes.

View File

@@ -15,7 +15,6 @@ from flask import (
)
from flask_login import login_required, current_user
from notifications_python_client.errors import HTTPError
from utils.template import Template
from utils.recipients import RecipientCSV, first_column_heading
@@ -38,8 +37,8 @@ send_messages_page_headings = {
manage_templates_page_headings = {
'email': 'Manage templates',
'sms': 'Manage templates'
'email': 'Email templates',
'sms': 'Text message templates'
}
@@ -80,13 +79,8 @@ def choose_template(service_id, template_type):
if template_type not in ['email', 'sms']:
abort(404)
try:
jobs = job_api_client.get_job(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
jobs = job_api_client.get_job(service_id)['data']
return render_template(
'views/choose-template.html',
templates=[
@@ -166,7 +160,7 @@ def get_example_csv(service_id, template_id):
'email': current_user.email_address,
'sms': current_user.mobile_number
}[template.template_type]
] + ["test {}".format(header) for header in template.placeholders])
] + _get_fake_personalisation(template.placeholders))
return output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}
@@ -183,13 +177,11 @@ def send_message_to_self(service_id, template_id):
)
if template.template_type == 'sms':
writer.writerow(
[current_user.mobile_number] +
["test {}".format(header) for header in template.placeholders]
[current_user.mobile_number] + _get_fake_personalisation(template.placeholders)
)
if template.template_type == 'email':
writer.writerow(
[current_user.email_address] +
["test {}".format(header) for header in template.placeholders]
[current_user.email_address] + _get_fake_personalisation(template.placeholders)
)
filedata = {
@@ -230,7 +222,8 @@ def check_messages(service_id, upload_id):
contents,
template_type=template.template_type,
placeholders=template.placeholders,
max_initial_rows_shown=5
max_initial_rows_shown=15,
max_errors_shown=15
)
with suppress(StopIteration):
@@ -245,8 +238,13 @@ def check_messages(service_id, upload_id):
template=template,
page_heading=get_page_headings(template.template_type),
errors=get_errors_for_csv(recipients, template.template_type),
rows_have_errors=any(recipients.rows_with_errors),
count_of_recipients=session['upload_data']['notification_count'],
count_of_displayed_recipients=len(list(recipients.rows_annotated_and_truncated)),
count_of_displayed_recipients=(
len(list(recipients.initial_annotated_rows_with_errors))
if any(recipients.rows_with_errors) else
len(list(recipients.initial_annotated_rows))
),
original_file_name=session['upload_data'].get('original_file_name'),
send_button_text=get_send_button_text(template.template_type, session['upload_data']['notification_count']),
service_id=service_id,
@@ -280,3 +278,9 @@ def start_job(service_id, upload_id):
return redirect(
url_for('main.view_job', service_id=service_id, job_id=upload_id)
)
def _get_fake_personalisation(placeholders):
return [
"{} 1".format(header) for header in placeholders
]

View File

@@ -12,13 +12,13 @@ from flask_login import (
login_required,
current_user
)
from notifications_python_client.errors import HTTPError
from notifications_python_client import HTTPError
from app.main.dao.services_dao import (
get_service_by_id,
delete_service,
update_service
update_service,
find_all_service_names
)
from app.main import main
@@ -31,13 +31,8 @@ from app.main.forms import ConfirmPasswordForm, ServiceNameForm
@login_required
@user_has_permissions('manage_settings')
def service_settings(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
return render_template(
'views/service-settings.html',
service=service,
@@ -49,15 +44,9 @@ def service_settings(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_name_change(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
form = ServiceNameForm()
form = ServiceNameForm(find_all_service_names)
if form.validate_on_submit():
session['service_name_change'] = form.name.data
@@ -74,13 +63,7 @@ def service_name_change(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_name_change_confirm(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
# Validate password for form
def _check_password(pwd):
@@ -89,10 +72,20 @@ def service_name_change_confirm(service_id):
if form.validate_on_submit():
service['name'] = session['service_name_change']
update_service(service)
session['service_name'] = service['name']
session.pop('service_name_change')
return redirect(url_for('.service_settings', service_id=service_id))
try:
update_service(service)
except HTTPError as e:
error_msg = "Duplicate service name '{}'".format(session['service_name_change'])
if e.status_code == 400 and error_msg in e.message['name']:
# Redirect the user back to the change service name screen
flash('This service name is already in use', 'error')
return redirect(url_for('main.service_name_change', service_id=service_id))
else:
raise e
else:
session['service_name'] = service['name']
session.pop('service_name_change')
return redirect(url_for('.service_settings', service_id=service_id))
return render_template(
'views/service-settings/confirm.html',
heading='Change your service name',
@@ -104,13 +97,7 @@ def service_name_change_confirm(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_request_to_go_live(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
if request.method == 'GET':
return render_template(
'views/service-settings/request-to-go-live.html',
@@ -127,13 +114,7 @@ def service_request_to_go_live(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_status_change(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
if request.method == 'GET':
return render_template(
@@ -149,13 +130,7 @@ def service_status_change(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_status_change_confirm(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
# Validate password for form
def _check_password(pwd):
@@ -178,13 +153,7 @@ def service_status_change_confirm(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_delete(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
if request.method == 'GET':
return render_template(
@@ -200,13 +169,7 @@ def service_delete(service_id):
@login_required
@user_has_permissions('manage_settings')
def service_delete_confirm(service_id):
try:
service = get_service_by_id(service_id)['data']
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = get_service_by_id(service_id)['data']
# Validate password for form
def _check_password(pwd):
@@ -214,13 +177,7 @@ def service_delete_confirm(service_id):
form = ConfirmPasswordForm(_check_password)
if form.validate_on_submit():
try:
service = delete_service(service_id)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
service = delete_service(service_id)
return redirect(url_for('.choose_service'))
return render_template(

View File

@@ -13,6 +13,11 @@ form_objects = {
'sms': SMSTemplateForm
}
page_headings = {
'email': 'email',
'sms': 'text message'
}
@main.route("/services/<service_id>/templates/add-<template_type>", methods=['GET', 'POST'])
@login_required
@@ -42,7 +47,8 @@ def add_service_template(service_id, template_type):
'views/edit-{}-template.html'.format(template_type),
form=form,
template_type=template_type,
service_id=service_id
service_id=service_id,
heading_action='Add'
)
@@ -70,7 +76,8 @@ def edit_service_template(service_id, template_id):
form=form,
service_id=service_id,
template_id=template_id,
template_type=template['template_type']
template_type=template['template_type'],
heading_action='Edit'
)

View File

@@ -2,17 +2,17 @@ from flask import (
render_template,
redirect,
session,
url_for,
abort
url_for
)
from notifications_python_client.errors import HTTPError
from flask_login import login_user
from app.main import main
from app.main.dao import users_dao
from app.main.forms import VerifyForm
from app.main.forms import (
VerifyForm,
VerifySmsForm
)
@main.route('/verify', methods=['GET', 'POST'])
@@ -24,19 +24,19 @@ def verify():
def _check_code(code, code_type):
return users_dao.check_verify_code(user_id, code, code_type)
form = VerifyForm(_check_code)
if session.get('invited_user'):
form = VerifySmsForm(_check_code)
else:
form = VerifyForm(_check_code)
if form.validate_on_submit():
try:
user = users_dao.get_user_by_id(user_id)
activated_user = users_dao.activate_user(user)
login_user(activated_user)
return redirect(url_for('main.add_service', first='first'))
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
finally:
del session['user_details']
session.pop('user_details', None)
return render_template('views/verify.html', form=form)