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

This commit is contained in:
Martyn Inglis
2016-03-01 09:57:52 +00:00
39 changed files with 493 additions and 344 deletions
+6
View File
@@ -15,6 +15,8 @@ from app.notify_client.api_key_api_client import ApiKeyApiClient
from app.notify_client.user_api_client import UserApiClient from app.notify_client.user_api_client import UserApiClient
from app.notify_client.job_api_client import JobApiClient from app.notify_client.job_api_client import JobApiClient
from app.notify_client.status_api_client import StatusApiClient from app.notify_client.status_api_client import StatusApiClient
from app.notify_client.permission_api_client import PermissionApiClient
from app.notify_client.invite_api_client import InviteApiClient
from app.its_dangerous_session import ItsdangerousSessionInterface from app.its_dangerous_session import ItsdangerousSessionInterface
from app.asset_fingerprinter import AssetFingerprinter from app.asset_fingerprinter import AssetFingerprinter
from app.utils import validate_phone_number, InvalidPhoneError from app.utils import validate_phone_number, InvalidPhoneError
@@ -30,7 +32,9 @@ user_api_client = UserApiClient()
api_key_api_client = ApiKeyApiClient() api_key_api_client = ApiKeyApiClient()
job_api_client = JobApiClient() job_api_client = JobApiClient()
status_api_client = StatusApiClient() status_api_client = StatusApiClient()
invite_api_client = InviteApiClient()
asset_fingerprinter = AssetFingerprinter() asset_fingerprinter = AssetFingerprinter()
permission_api_client = PermissionApiClient()
def create_app(config_name, config_overrides=None): def create_app(config_name, config_overrides=None):
@@ -47,6 +51,8 @@ def create_app(config_name, config_overrides=None):
api_key_api_client.init_app(application) api_key_api_client.init_app(application)
job_api_client.init_app(application) job_api_client.init_app(application)
status_api_client.init_app(application) status_api_client.init_app(application)
permission_api_client.init_app(application)
invite_api_client.init_app(application)
login_manager.init_app(application) login_manager.init_app(application)
login_manager.login_view = 'main.sign_in' login_manager.login_view = 'main.sign_in'
+4
View File
@@ -15,3 +15,7 @@
margin-bottom: $gutter; margin-bottom: $gutter;
clear: both; clear: both;
} }
.bottom-gutter-2-3 {
margin-bottom: $gutter * 2/3;
}
+2 -2
View File
@@ -164,13 +164,13 @@ class AddServiceForm(Form):
name = StringField( name = StringField(
'Service name', 'Service name',
validators=[ validators=[
DataRequired(message='Service name can not be empty') DataRequired(message='Service name cant be empty')
] ]
) )
def validate_name(self, a): def validate_name(self, a):
if a.data in self._names_func(): if a.data in self._names_func():
raise ValidationError('Service name already exists') raise ValidationError('This service name is already in use')
class ServiceNameForm(Form): class ServiceNameForm(Form):
+4 -28
View File
@@ -11,39 +11,15 @@ from app.main.forms import AddServiceForm
@login_required @login_required
def add_service(): def add_service():
form = AddServiceForm(services_dao.find_all_service_names) form = AddServiceForm(services_dao.find_all_service_names)
services = services_dao.get_services(current_user.id) heading = 'Which service do you want to set up notifications for?'
if len(services['data']) == 0:
heading = 'Which service do you want to set up notifications for?'
else:
heading = 'Add a new service'
if form.validate_on_submit(): if form.validate_on_submit():
session['service_name'] = form.name.data session['service_name'] = form.name.data
return redirect(url_for('main.add_from_address')) user = users_dao.get_user_by_id(session['user_id'])
service_id = services_dao.insert_new_service(session['service_name'], user.id)
return redirect(url_for('main.service_dashboard', service_id=service_id))
else: else:
return render_template( return render_template(
'views/add-service.html', 'views/add-service.html',
form=form, form=form,
heading=heading heading=heading
) )
@main.route("/confirm-add-service", methods=['GET', 'POST'])
@login_required
def add_from_address():
if request.method == 'POST':
user = users_dao.get_user_by_id(session['user_id'])
service_id = services_dao.insert_new_service(session['service_name'], user.id)
return redirect(url_for('main.service_dashboard', service_id=service_id))
else:
return render_template(
'views/add-from-address.html',
service_name=session['service_name'],
from_address="{}@notifications.service.gov.uk".format(_email_safe(session['service_name']))
)
def _email_safe(string):
return "".join([
character.lower() if character.isalnum() or character == "." else ""
for character in re.sub("\s+", ".", string.strip())
])
+4
View File
@@ -3,6 +3,7 @@ from flask_login import login_required
from app.main import main from app.main import main
from app.main.forms import CreateKeyForm from app.main.forms import CreateKeyForm
from app import api_key_api_client from app import api_key_api_client
from app.utils import user_has_permissions
@main.route("/services/<service_id>/documentation") @main.route("/services/<service_id>/documentation")
@@ -13,6 +14,7 @@ def documentation(service_id):
@main.route("/services/<service_id>/api-keys") @main.route("/services/<service_id>/api-keys")
@login_required @login_required
@user_has_permissions('manage_api_keys')
def api_keys(service_id): def api_keys(service_id):
return render_template( return render_template(
'views/api-keys.html', 'views/api-keys.html',
@@ -23,6 +25,7 @@ def api_keys(service_id):
@main.route("/services/<service_id>/api-keys/create", methods=['GET', 'POST']) @main.route("/services/<service_id>/api-keys/create", methods=['GET', 'POST'])
@login_required @login_required
@user_has_permissions('manage_api_keys')
def create_api_key(service_id): def create_api_key(service_id):
key_names = [ key_names = [
key['name'] for key in api_key_api_client.get_api_keys(service_id=service_id)['apiKeys'] key['name'] for key in api_key_api_client.get_api_keys(service_id=service_id)['apiKeys']
@@ -41,6 +44,7 @@ def create_api_key(service_id):
@main.route("/services/<service_id>/api-keys/revoke/<int:key_id>", methods=['GET', 'POST']) @main.route("/services/<service_id>/api-keys/revoke/<int:key_id>", methods=['GET', 'POST'])
@login_required @login_required
@user_has_permissions('manage_api_keys')
def revoke_api_key(service_id, key_id): def revoke_api_key(service_id, key_id):
key_name = api_key_api_client.get_api_keys(service_id=service_id, key_id=key_id)['apiKeys'][0]['name'] key_name = api_key_api_client.get_api_keys(service_id=service_id, key_id=key_id)['apiKeys'][0]['name']
if request.method == 'GET': if request.method == 'GET':
+1
View File
@@ -23,6 +23,7 @@ def service_dashboard(service_id):
try: try:
service = get_service_by_id(service_id) service = get_service_by_id(service_id)
session['service_name'] = service['data']['name'] session['service_name'] = service['data']['name']
session['service_id'] = service['data']['id']
except HTTPError as e: except HTTPError as e:
if e.status_code == 404: if e.status_code == 404:
abort(404) abort(404)
+6 -2
View File
@@ -13,6 +13,7 @@ from notification_utils.template import Template
from app import job_api_client from app import job_api_client
from app.main import main from app.main import main
from app.main.dao import templates_dao from app.main.dao import templates_dao
from app.main.dao import services_dao
now = time.strftime('%H:%M') now = time.strftime('%H:%M')
@@ -37,6 +38,7 @@ def view_jobs(service_id):
@main.route("/services/<service_id>/jobs/<job_id>") @main.route("/services/<service_id>/jobs/<job_id>")
@login_required @login_required
def view_job(service_id, job_id): def view_job(service_id, job_id):
service = services_dao.get_service_by_id_or_404(service_id)
try: try:
job = job_api_client.get_job(service_id, job_id)['data'] job = job_api_client.get_job(service_id, job_id)['data']
messages = [] messages = []
@@ -56,9 +58,11 @@ def view_job(service_id, job_id):
uploaded_file_name=job['original_file_name'], uploaded_file_name=job['original_file_name'],
uploaded_file_time=job['created_at'], uploaded_file_time=job['created_at'],
template=Template( template=Template(
templates_dao.get_service_template_or_404(service_id, job['template'])['data'] templates_dao.get_service_template_or_404(service_id, job['template'])['data'],
prefix=service['name']
), ),
service_id=service_id service_id=service_id,
service=service
) )
except HTTPError as e: except HTTPError as e:
if e.status_code == 404: if e.status_code == 404:
+43 -13
View File
@@ -7,13 +7,18 @@ from flask import (
flash flash
) )
from flask_login import login_required, current_user from flask_login import (
login_required,
current_user
)
from notifications_python_client.errors import HTTPError
from app.main import main from app.main import main
from app.main.dao import users_dao
from app.main.forms import InviteUserForm from app.main.forms import InviteUserForm
from app.main.dao.services_dao import get_service_by_id_or_404 from app.main.dao.services_dao import get_service_by_id_or_404
from app import user_api_client from app import user_api_client
from app import invite_api_client
fake_users = [ fake_users = [
{ {
@@ -29,14 +34,19 @@ 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) try:
return render_template( users = user_api_client.get_users_for_service(service_id=service_id)
'views/manage-users.html', invited_users = invite_api_client.get_invites_for_service(service_id=service_id)
service_id=service_id, return render_template('views/manage-users.html',
users=users, service_id=service_id,
current_user=current_user, users=users,
invited_users=[] current_user=current_user,
) invited_users=invited_users)
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
@main.route("/services/<service_id>/users/invite", methods=['GET', 'POST']) @main.route("/services/<service_id>/users/invite", methods=['GET', 'POST'])
@@ -44,10 +54,19 @@ def manage_users(service_id):
def invite_user(service_id): def invite_user(service_id):
form = InviteUserForm() form = InviteUserForm()
if form.validate_on_submit(): if form.validate_on_submit():
flash('Invite sent to {}'.format(form.email_address.data), 'default_with_tick') email_address = form.email_address.data
return redirect(url_for('.manage_users', service_id=service_id)) permissions = _get_permissions(request.form)
try:
resp = invite_api_client.create_invite(current_user.id, service_id, email_address, permissions)
flash('Invite sent to {}'.format(resp['email_address']), 'default_with_tick')
return redirect(url_for('.manage_users', service_id=service_id))
except HTTPError as e:
if e.status_code == 404:
abort(404)
else:
raise e
return render_template( return render_template(
'views/invite-user.html', 'views/invite-user.html',
@@ -95,3 +114,14 @@ def delete_user(service_id, user_id):
service=get_service_by_id_or_404(service_id), service=get_service_by_id_or_404(service_id),
service_id=service_id service_id=service_id
) )
def _get_permissions(form):
permissions = []
if form.get('send_messages') and form['send_messages'] == 'yes':
permissions.append('send_messages')
if form.get('manage_service') and form['manage_service'] == 'yes':
permissions.append('manage_service')
if form.get('manage_api_keys') and form['manage_api_keys'] == 'yes':
permissions.append('manage_api_keys')
return ','.join(permissions)
+64 -33
View File
@@ -1,9 +1,6 @@
import csv import csv
import io import io
import uuid import uuid
import botocore
from datetime import date
from flask import ( from flask import (
request, request,
@@ -17,7 +14,6 @@ from flask import (
) )
from flask_login import login_required, current_user from flask_login import login_required, current_user
from werkzeug import secure_filename
from notifications_python_client.errors import HTTPError from notifications_python_client.errors import HTTPError
from utils.template import Template, NeededByTemplateError, NoPlaceholderForDataError from utils.template import Template, NeededByTemplateError, NoPlaceholderForDataError
@@ -32,16 +28,23 @@ from app.main.dao import services_dao
from app import job_api_client from app import job_api_client
from app.utils import validate_recipient, InvalidPhoneError, InvalidEmailError from app.utils import validate_recipient, InvalidPhoneError, InvalidEmailError
first_column_header = { page_headings = {
'email': 'email', 'email': 'Send emails',
'sms': 'phone' 'sms': 'Send text messages'
} }
@main.route("/services/<service_id>/send/letters", methods=['GET'])
def letters_stub(service_id):
return render_template(
'views/letters.html', service_id=service_id
)
@main.route("/services/<service_id>/send/<template_type>", methods=['GET']) @main.route("/services/<service_id>/send/<template_type>", methods=['GET'])
def choose_template(service_id, template_type): def choose_template(service_id, template_type):
services_dao.get_service_by_id_or_404(service_id) service = services_dao.get_service_by_id_or_404(service_id)
if template_type not in ['email', 'sms']: if template_type not in ['email', 'sms']:
abort(404) abort(404)
@@ -53,11 +56,17 @@ def choose_template(service_id, template_type):
else: else:
raise e raise e
return render_template( return render_template(
'views/choose-{}-template.html'.format(template_type), 'views/choose-template.html',
templates=[ templates=[
Template(template) for template in templates_dao.get_service_templates(service_id)['data'] Template(
template,
prefix=service['name']
) for template in templates_dao.get_service_templates(service_id)['data']
if template['template_type'] == template_type if template['template_type'] == template_type
], ],
template_type=template_type,
page_heading=page_headings[template_type],
service=service,
has_jobs=len(jobs), has_jobs=len(jobs),
service_id=service_id service_id=service_id
) )
@@ -70,7 +79,7 @@ def send_messages(service_id, template_id):
form = CsvUploadForm() form = CsvUploadForm()
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
csv_file = form.file.data csv_file = form.file
filedata = _get_filedata(csv_file) filedata = _get_filedata(csv_file)
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'])
@@ -79,19 +88,20 @@ def send_messages(service_id, template_id):
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.data.filename))
flash(str(e)) flash(str(e))
return redirect(url_for('.send_messages', 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) 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'],
prefix=service['name']
) )
return render_template( return render_template(
'views/send.html', 'views/send.html',
template=template, template=template,
column_headers=[first_column_header[template.template_type]] + template.placeholders_as_markup, column_headers=['to'] + template.placeholders_as_markup,
form=form, form=form,
service=service, service=service,
service_id=service_id service_id=service_id
@@ -105,10 +115,14 @@ def get_example_csv(service_id, template_id):
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([first_column_header[template['template_type']]] + placeholders) writer.writerow(['to'] + placeholders)
writer.writerow([current_user.mobile_number] + ["test {}".format(header) for header in placeholders]) writer.writerow([
{
return(output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}) 'email': current_user.email_address,
'sms': current_user.mobile_number
}[template['template_type']]
] + ["test {}".format(header) for header in placeholders])
return output.getvalue(), 200, {'Content-Type': 'text/csv; charset=utf-8'}
@main.route("/services/<service_id>/send/<template_id>/to-self", methods=['GET']) @main.route("/services/<service_id>/send/<template_id>/to-self", methods=['GET'])
@@ -118,7 +132,7 @@ def send_message_to_self(service_id, template_id):
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([first_column_header[template['template_type']]] + placeholders) writer.writerow(['to'] + 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',
@@ -140,29 +154,30 @@ def check_messages(service_id, upload_id):
upload_data = session['upload_data'] upload_data = session['upload_data']
template_id = upload_data.get('template_id') template_id = upload_data.get('template_id')
service = services_dao.get_service_by_id_or_404(service_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')
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={recipient_type} drop_values={'to'},
prefix=service['name']
) )
return render_template( return render_template(
'views/check-sms.html', 'views/check.html',
upload_result=upload_result, upload_result=upload_result,
template=template, template=template,
column_headers=[recipient_type] + list( page_heading=page_headings[template.template_type],
template.placeholders if upload_result['valid'] else template.placeholders_as_markup column_headers=['to'] + list(template.placeholders_as_markup),
),
original_file_name=upload_data.get('original_file_name'), original_file_name=upload_data.get('original_file_name'),
service_id=service_id, service_id=service_id,
service=service,
form=CsvUploadForm() form=CsvUploadForm()
) )
elif request.method == 'POST': elif request.method == 'POST':
@@ -184,17 +199,34 @@ def check_messages(service_id, upload_id):
def _get_filedata(file): def _get_filedata(file):
lines = file.read().decode('utf-8').splitlines() import itertools
if len(lines) < 2: # must be at least header and one line reader = csv.reader(
message = 'The file {} contained no data'.format(file.filename) file.data.getvalue().decode('utf-8').splitlines(),
quoting=csv.QUOTE_NONE,
skipinitialspace=True
)
lines = []
for row in reader:
non_empties = itertools.dropwhile(lambda x: x.strip() == '', row)
has_content = []
for item in non_empties:
has_content.append(item)
if has_content:
lines.append(row)
if len(lines) < 2: # must be header row and at least one data row
message = 'The file {} contained no data'.format(file.data.filename)
raise ValueError(message) raise ValueError(message)
return {'file_name': file.filename, 'data': lines}
content_lines = []
for row in lines:
content_lines.append(','.join(row).rstrip(','))
return {'file_name': file.data.filename, 'data': content_lines}
def _get_rows(contents, raw_template): def _get_rows(contents, raw_template):
reader = csv.DictReader( reader = csv.DictReader(
contents.split('\n'), contents.split('\n'),
lineterminator='\n',
quoting=csv.QUOTE_NONE, quoting=csv.QUOTE_NONE,
skipinitialspace=True skipinitialspace=True
) )
@@ -203,12 +235,11 @@ def _get_rows(contents, raw_template):
for row in reader: for row in reader:
rows.append(row) rows.append(row)
try: try:
recipient_column = first_column_header[raw_template['template_type']]
validate_recipient( validate_recipient(
row[recipient_column], row.get('to', ''),
template_type=raw_template['template_type'] template_type=raw_template['template_type']
) )
Template(raw_template, values=row, drop_values={recipient_column}).replaced Template(raw_template, values=row, drop_values={'to'}).replaced
except (InvalidEmailError, InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError): except (InvalidEmailError, InvalidPhoneError, NeededByTemplateError, NoPlaceholderForDataError):
valid = False valid = False
return {"valid": valid, "rows": rows} return {"valid": valid, "rows": rows}
+29
View File
@@ -0,0 +1,29 @@
from notifications_python_client.base import BaseAPIClient
class InviteApiClient(BaseAPIClient):
def __init__(self, base_url=None, client_id=None, secret=None):
super(self.__class__, self).__init__(base_url=base_url or 'base_url',
client_id=client_id or 'client_id',
secret=secret or 'secret')
def init_app(self, app):
self.base_url = app.config['API_HOST_NAME']
self.client_id = app.config['ADMIN_CLIENT_USER_NAME']
self.secret = app.config['ADMIN_CLIENT_SECRET']
def create_invite(self, invite_from_id, service_id, email_address, permissions):
data = {
'service': str(service_id),
'email_address': email_address,
'from_user': invite_from_id,
'permissions': permissions
}
resp = self.post(url='/service/{}/invite'.format(service_id), data=data)
return resp['data']
def get_invites_for_service(self, service_id):
endpoint = '/service/{}/invite'.format(service_id)
resp = self.get(endpoint)
return resp['data']
-2
View File
@@ -1,4 +1,3 @@
import uuid
from notifications_python_client.base import BaseAPIClient from notifications_python_client.base import BaseAPIClient
@@ -23,7 +22,6 @@ class JobApiClient(BaseAPIClient):
def create_job(self, job_id, service_id, template_id, original_file_name, notification_count): def create_job(self, job_id, service_id, template_id, original_file_name, notification_count):
data = { data = {
"id": job_id, "id": job_id,
"service": service_id,
"template": template_id, "template": template_id,
"original_file_name": original_file_name, "original_file_name": original_file_name,
"bucket_name": "service-{}-notify".format(service_id), "bucket_name": "service-{}-notify".format(service_id),
@@ -0,0 +1,25 @@
import uuid
from notifications_python_client.base import BaseAPIClient
class PermissionApiClient(BaseAPIClient):
def __init__(self, base_url=None, client_id=None, secret=None):
super(self.__class__, self).__init__(base_url=base_url or 'base_url',
client_id=client_id or 'client_id',
secret=secret or 'secret')
def init_app(self, app):
self.base_url = app.config['API_HOST_NAME']
self.client_id = app.config['ADMIN_CLIENT_USER_NAME']
self.secret = app.config['ADMIN_CLIENT_SECRET']
def delete_permission(self, permission_id):
return self.delete(url='/permission/{}'.format(permission_id))['data']
def create_permission(self, permission, user_id, service_id):
return self.post(
url='/permission',
data={'permission': permission,
'user': user_id,
'service': service_id})['data']
+6 -12
View File
@@ -94,7 +94,7 @@ class User(UserMixin):
self._email_address = fields.get('email_address') self._email_address = fields.get('email_address')
self._mobile_number = fields.get('mobile_number') self._mobile_number = fields.get('mobile_number')
self._password_changed_at = fields.get('password_changed_at') self._password_changed_at = fields.get('password_changed_at')
self._permissions = set(fields.get('permissions')) if fields.get('permission') is not None else set() self._permissions = fields.get('permissions')
self._failed_login_count = 0 self._failed_login_count = 0
self._state = fields.get('state') self._state = fields.get('state')
self.max_failed_login_count = max_failed_login_count self.max_failed_login_count = max_failed_login_count
@@ -165,18 +165,12 @@ class User(UserMixin):
@permissions.setter @permissions.setter
def permissions(self, permissions): def permissions(self, permissions):
if permissions is None: raise AttributeError("Read only property")
permissions = set()
self._permissions = set(permissions)
def add_permissions(self, permissions): def has_permissions(self, service_id, permissions):
self._permissions.update(permissions) if service_id in self._permissions:
return set(self._permissions[service_id]) > set(permissions)
def remove_permissions(self, permissions): return False
self._permissions -= permissions
def has_permissions(self, permissions):
return self._permissions > set(permissions)
@property @property
def failed_login_count(self): def failed_login_count(self):
+2 -2
View File
@@ -5,11 +5,11 @@
</legend> </legend>
<div class='yes-no-fields inline'> <div class='yes-no-fields inline'>
<label class='block-label'> <label class='block-label'>
<input type='radio' name='{{ name }}' {% if current_value == True %}checked{% endif %} /> <input type='radio' name='{{ name }}' value='yes' {% if current_value == True %}checked{% endif %} />
Yes Yes
</label> </label>
<label class='block-label'> <label class='block-label'>
<input type='radio' name='{{ name }}' {% if current_value == False %}checked{% endif %} /> <input type='radio' name='{{ name }}' value='no' {% if current_value == False %}checked{% endif %} />
No No
</label> </label>
</div> </div>
+7
View File
@@ -2,16 +2,23 @@
<h2 class="navigation-service-name"> <h2 class="navigation-service-name">
<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>
{% if current_user.has_permissions(session.get('service_id', ''), ['send_messages']) %}
<ul> <ul>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='sms') }}">Send text messages</a></li>
<li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li> <li><a href="{{ url_for('.choose_template', service_id=service_id, template_type='email') }}">Send emails</a></li>
<li><a href="{{ url_for('.letters_stub', service_id=service_id) }}">Send letters</a></li>
</ul> </ul>
{% endif %}
{% if current_user.has_permissions(session.get('service_id', ''), ['manage_service']) %}
<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>
<li><a href="{{ url_for('.service_settings', service_id=service_id) }}">Manage settings</a></li> <li><a href="{{ url_for('.service_settings', service_id=service_id) }}">Manage settings</a></li>
</ul> </ul>
{% endif %}
{% if current_user.has_permissions(session.get('service_id', ''), ['manage_api_keys']) %}
<ul> <ul>
<li><a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys</a></li> <li><a href="{{ url_for('.api_keys', service_id=service_id) }}">API keys</a></li>
<li><a href="{{ url_for('.documentation', service_id=service_id) }}">Developer documentation</a></li> <li><a href="{{ url_for('.documentation', service_id=service_id) }}">Developer documentation</a></li>
</ul> </ul>
{% endif %}
</nav> </nav>
-42
View File
@@ -1,42 +0,0 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/sms-message.html" import sms_message %}
{% from "components/email-message.html" import email_message %}
{% block page_title %}
Preview your service name GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">
Preview your service name
</h1>
<div class="grid-row">
<div class="column-two-thirds">
{{ sms_message(
"{}: we received your payment, thank you".format(service_name),
name="Text message",
recipient='Sent from 40604'
) }}
</div>
</div>
<div class="grid-row">
<div class='column-two-thirds'>
{{ email_message(
subject="We received your payment, thank you",
body="Dear Alice Smith,\n\nThank you for…",
from_name=service_name,
from_address=from_address,
name="Email",
) }}
</div>
</div>
<form method="post">
{{page_footer('Looks good', back_link=url_for(".add_service"))}}
</form>
{% endblock %}
+17 -4
View File
@@ -16,16 +16,29 @@
</h1> </h1>
<p> <p>
Be specific. Remember that there might be other people in your Be specific to your service. Remember that there might be
organisation using GOV.UK Notify. other people in your organisation using GOV.UK Notify.
</p> </p>
<p>
Users will see this:
</p>
<ul class="list-bullet bottom-gutter">
<li>
at the start of every text message, eg Vehicle tax: we received your
payment, thank you
</li>
<li>
as your email sender name
</li>
</ul>
<form autocomplete="off" method="post"> <form autocomplete="off" method="post">
{{ textbox(form.name, hint="You can change this later") }} {{ textbox(form.name, hint="You can change this later") }}
{{ page_footer('Add service') }}
{{ page_footer('Continue') }}
</form> </form>
-18
View File
@@ -1,18 +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 check the email messages we're about to send</p>
<p>
<a class="button" href="dashboard" role="button">Send email messages</a>
</p>
{% endblock %}
@@ -1,4 +1,5 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/email-message.html" import email_message %}
{% from "components/sms-message.html" import sms_message %} {% from "components/sms-message.html" import sms_message %}
{% from "components/table.html" import list_table, field %} {% from "components/table.html" import list_table, field %}
{% from "components/placeholder.html" import placeholder %} {% from "components/placeholder.html" import placeholder %}
@@ -6,7 +7,7 @@
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% block page_title %} {% block page_title %}
Check and confirm GOV.UK Notify {{ "Check and confirm" if upload_result.valid else page_heading }} GOV.UK Notify
{% endblock %} {% endblock %}
{% block maincolumn_content %} {% block maincolumn_content %}
@@ -25,18 +26,25 @@
{% endif %} {% endif %}
<h1 class="heading-large"> <h1 class="heading-large">
{{ "Check and confirm" if upload_result.valid else "Send text messages" }} {{ "Check and confirm" if upload_result.valid else page_heading }}
</h1> </h1>
<div class="grid-row"> {% if 'email' == template.template_type %}
<div class="column-two-thirds"> {{ email_message(
{% if template.missing_data or template.additional_data %} template.subject,
{{ sms_message(template.formatted_as_markup)}} template.replaced if upload_result.valid else template.formatted_as_markup,
{% else %} from_address='{}@notifications.service.gov.uk'.format(service.email_from),
{{ sms_message(template.replaced)}} from_name=service.name
{% endif %} )}}
{% elif 'sms' == template.template_type %}
<div class="grid-row">
<div class="column-two-thirds">
{{ sms_message(
template.replaced if upload_result.valid else template.formatted_as_markup
)}}
</div>
</div> </div>
</div> {% endif %}
{% if upload_result.valid %} {% if upload_result.valid %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
@@ -53,13 +61,13 @@
caption=original_file_name, caption=original_file_name,
field_headings=column_headers field_headings=column_headers
) %} ) %}
{% if item.phone|valid_phone_number %} {% if item.to or ''|valid_phone_number %}
{% call field() %} {% call field() %}
{{ item.phone }} {{ item.to }}
{% endcall %} {% endcall %}
{% else %} {% else %}
{% call field(status='missing') %} {% call field(status='missing') %}
{{ item.phone }} {{ item.to }}
{% endcall %} {% endcall %}
{% endif %} {% endif %}
{% for column in template.placeholders %} {% for column in template.placeholders %}
@@ -1,36 +0,0 @@
{% 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 %}
@@ -1,15 +1,16 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/email-message.html" import email_message %}
{% from "components/sms-message.html" import sms_message %} {% from "components/sms-message.html" import sms_message %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/textbox.html" import textbox %} {% from "components/textbox.html" import textbox %}
{% block page_title %} {% block page_title %}
Send text messages GOV.UK Notify {{ page_heading }} GOV.UK Notify
{% endblock %} {% endblock %}
{% block maincolumn_content %} {% block maincolumn_content %}
<h1 class="heading-large">Send text messages</h1> <h1 class="heading-large">{{ page_heading }}</h1>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
@@ -26,7 +27,15 @@
<div class="grid-row"> <div class="grid-row">
{% for template in templates %} {% for template in templates %}
<div class="column-two-thirds"> <div class="column-two-thirds">
{{ sms_message(template.formatted_as_markup, name=template.name) }} {% if 'email' == template_type %}
{{ email_message(
template.subject,
template.formatted_as_markup,
name=template.name
) }}
{% elif 'sms' == template_type %}
{{ sms_message(template.formatted_as_markup, name=template.name) }}
{% endif %}
</div> </div>
<div class="column-one-third"> <div class="column-one-third">
<div class="sms-message-use-links"> <div class="sms-message-use-links">
@@ -40,7 +49,7 @@
{% endif %} {% endif %}
<p> <p>
<a href="{{ url_for('.add_service_template', service_id=service_id, template_type='sms') }}" class="button">Add a new template</a> <a href="{{ url_for('.add_service_template', service_id=service_id, template_type=template_type) }}" class="button">Add a new template</a>
</p> </p>
</form> </form>
+1 -1
View File
@@ -161,7 +161,7 @@
<h2 class="heading-medium">API endpoint</h2> <h2 class="heading-medium">API endpoint</h2>
<p> <p>
https://www.notify.works/api/endpoint https://api.notify.works
</p> </p>
<p> <p>
+1
View File
@@ -28,6 +28,7 @@ Manage users GOV.UK Notify
<legend class='heading-small'> <legend class='heading-small'>
Permissions Permissions
</legend> </legend>
<span class="form-hint">All team members can see message history</span>
{{ yes_no('send_messages', 'Send messages', user.permission_send_messages) }} {{ yes_no('send_messages', 'Send messages', user.permission_send_messages) }}
{{ yes_no('manage_service', 'Manage service', user.permission_manage_service) }} {{ yes_no('manage_service', 'Manage service', user.permission_manage_service) }}
{{ yes_no('manage_api_keys', 'Manage API keys', user.permission_manage_api_keys) }} {{ yes_no('manage_api_keys', 'Manage API keys', user.permission_manage_api_keys) }}
+15 -6
View File
@@ -14,13 +14,22 @@
{{ uploaded_file_name }} {{ uploaded_file_name }}
</h1> </h1>
<div class="grid-row"> {% if 'sms' == template.template_type %}
<div class="column-two-thirds"> <div class="grid-row">
{{ sms_message( <div class="column-two-thirds">
template.formatted_as_markup, {{ sms_message(
)}} template,
)}}
</div>
</div> </div>
</div> {% elif 'email' == template.template_type %}
{{ email_message(
template.subject,
template,
from_address='{}@notifications.service.gov.uk'.format(service.email_from),
from_name=service.name
)}}
{% endif %}
<p class='heading-small'> <p class='heading-small'>
Started {{ uploaded_file_time|format_datetime }} Started {{ uploaded_file_time|format_datetime }}
+17
View File
@@ -0,0 +1,17 @@
{% extends "withnav_template.html" %}
{% block page_title %}
Send letters GOV.UK Notify
{% endblock %}
{% block maincolumn_content %}
<h1 class="heading-large">
Send letters
</h1>
<p>
This page is where you would go to send letters.
</p>
{% endblock %}
+1 -1
View File
@@ -41,7 +41,7 @@ Manage users GOV.UK Notify
invited_users, caption='Invited', **table_options invited_users, caption='Invited', **table_options
) %} ) %}
{% call field() %} {% call field() %}
{{ item.email_localpart }} {{ item.email_address }}
{% 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) }}
+32 -21
View File
@@ -13,31 +13,42 @@
<h1 class="heading-large">Add recipients</h1> <h1 class="heading-large">Add recipients</h1>
{% if 'sms' == template.template_type %}
<div class="grid-row">
<div class="column-two-thirds">
{{ sms_message(template.formatted_as_markup) }}
</div>
</div>
{% 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 %}
<div class="grid-row"> <div class="grid-row">
<div class="column-two-thirds"> <div class="column-two-thirds">
{% if 'sms' == template.template_type %} <p>
{{ sms_message(template.formatted_as_markup) }} Add recipients by uploading a .csv file with
{% elif 'email' == template.template_type %} {{ template.placeholders|length + 1 }}
{{ email_message( {% if template.placeholders %}
template.subject, columns:
template.formatted_as_markup, {% else %}
from_address='{}@notifications.service.gov.uk'.format(service.email_from), column:
from_name=service.name {% endif %}
) }} </p>
{% endif %} <p class="bottom-gutter-2-3">
{{ banner( <span class='placeholder'>to</span>
'You can upload real data, but well only send to your mobile number until you <a href="{}">request to go live</a>'.format( {{ template.placeholders_as_markup|join(" ") }}
url_for('.service_request_to_go_live', service_id=service_id) </p>
)|safe, <p>
'info' <a href="{{ url_for('.get_example_csv', service_id=service_id, template_id=template.id) }}">Download an example</a>
)}} </p>
</div> </div>
</div> </div>
{{file_upload(form.file, button_text='Upload a CSV file')}} {{file_upload(form.file, button_text='Upload your CSV file')}}
<p>
<a href="{{ url_for('.get_example_csv', service_id=service_id, template_id=template.id) }}">Download an example CSV file</a>
</p>
{% endblock %} {% endblock %}
+5 -3
View File
@@ -1,7 +1,7 @@
import re import re
from functools import wraps from functools import wraps
from flask import abort from flask import (abort, session)
class BrowsableItem(object): class BrowsableItem(object):
@@ -100,8 +100,10 @@ def user_has_permissions(*permissions):
def wrap_func(*args, **kwargs): def wrap_func(*args, **kwargs):
# We are making the assumption that the user is logged in. # We are making the assumption that the user is logged in.
from flask_login import current_user from flask_login import current_user
if set(permissions) > set(current_user.permissions): service_id = session.get('service_id', '')
if current_user and current_user.has_permissions(service_id, permissions):
return func(*args, **kwargs)
else:
abort(403) abort(403)
return func(*args, **kwargs)
return wrap_func return wrap_func
return wrap return wrap
+1 -2
View File
@@ -29,9 +29,8 @@
"gulp-base64": "0.1.3", "gulp-base64": "0.1.3",
"gulp-concat": "2.6.0", "gulp-concat": "2.6.0",
"gulp-include": "2.1.0", "gulp-include": "2.1.0",
"gulp-jquery": "1.1.1",
"gulp-load-plugins": "1.1.0", "gulp-load-plugins": "1.1.0",
"gulp-sass": "2.1.1", "gulp-sass": "2.2.0",
"gulp-uglify": "1.5.1", "gulp-uglify": "1.5.1",
"jquery": "1.11.2", "jquery": "1.11.2",
"query-command-supported": "1.0.0" "query-command-supported": "1.0.0"
+1 -1
View File
@@ -14,4 +14,4 @@ Pygments==2.0.2
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-python-client.git@0.2.8#egg=notifications-python-client==0.2.8
git+https://github.com/alphagov/notifications-utils.git@0.1.1#egg=notifications-utils==0.1.1 git+https://github.com/alphagov/notifications-utils.git@0.1.2#egg=notifications-utils==0.1.2
+12 -2
View File
@@ -47,6 +47,15 @@ def api_key_json(id_, name, expiry_date=None):
'expiry_date': expiry_date 'expiry_date': expiry_date
} }
def invite_json(id, from_user, service_id, email_address):
return {'id': id,
'from_user': from_user,
'service': service_id,
'email_address': email_address,
'status': 'pending'}
TEST_USER_EMAIL = 'test@user.gov.uk' TEST_USER_EMAIL = 'test@user.gov.uk'
@@ -57,14 +66,15 @@ def create_test_user(state):
return user return user
def create_test_api_user(state): def create_test_api_user(state, permissions={}):
from app.notify_client.user_api_client import User from app.notify_client.user_api_client import User
user_data = {'id': 1, user_data = {'id': 1,
'name': 'Test User', 'name': 'Test User',
'password': 'somepassword', 'password': 'somepassword',
'email_address': TEST_USER_EMAIL, 'email_address': TEST_USER_EMAIL,
'mobile_number': '+441234123412', 'mobile_number': '+441234123412',
'state': state 'state': state,
'permissions': permissions
} }
user = User(user_data) user = User(user_data)
return user return user
@@ -11,7 +11,6 @@ def test_client_creates_job_data_correctly(mocker):
expected_data = { expected_data = {
"id": job_id, "id": job_id,
"service": service_id,
"template": template_id, "template": template_id,
"original_file_name": original_file_name, "original_file_name": original_file_name,
"bucket_name": "service-{}-notify".format(service_id), "bucket_name": "service-{}-notify".format(service_id),
+1 -1
View File
@@ -9,4 +9,4 @@ def test_form_should_have_errors_when_duplicate_service_is_added(app_):
form = AddServiceForm(_get_form_names, form = AddServiceForm(_get_form_names,
formdata=MultiDict([('name', 'some service')])) formdata=MultiDict([('name', 'some service')]))
form.validate() form.validate()
assert {'name': ['Service name already exists']} == form.errors assert {'name': ['This service name is already in use']} == form.errors
+3 -43
View File
@@ -14,7 +14,7 @@ def test_get_should_render_add_service_template(app_,
client.login(api_user_active) client.login(api_user_active)
response = client.get(url_for('main.add_service')) response = client.get(url_for('main.add_service'))
assert response.status_code == 200 assert response.status_code == 200
assert 'Add a new service' in response.get_data(as_text=True) assert 'Which service do you want to set up notifications for?' in response.get_data(as_text=True)
def test_should_add_service_and_redirect_to_next_page(app_, def test_should_add_service_and_redirect_to_next_page(app_,
@@ -31,48 +31,8 @@ def test_should_add_service_and_redirect_to_next_page(app_,
url_for('main.add_service'), url_for('main.add_service'),
data={'name': 'testing the post'}) data={'name': 'testing the post'})
assert response.status_code == 302 assert response.status_code == 302
assert response.location == url_for('main.add_from_address', _external=True)
def test_should_confirm_add_service(
app_,
mock_login,
mock_get_services,
api_user_active,
mock_get_user,
mock_get_user_by_email
):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
with client.session_transaction() as session:
session['service_name'] = 'Renew Your Pet Passport'
response = client.get(url_for('main.add_from_address'))
assert response.status_code == 200
assert 'Preview your service name' in response.get_data(as_text=True)
assert 'Renew Your Pet Passport' in response.get_data(as_text=True)
assert 'renew.your.pet.passport@notifications.service.gov.uk' in response.get_data(as_text=True)
def test_should_add_service_after_confirmation(
app_,
mock_login,
mock_create_service,
mock_get_services,
api_user_active,
mock_get_user,
mock_get_user_by_email
):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
with client.session_transaction() as session:
session['service_name'] = 'Renew Your Pet Passport'
response = client.post(url_for('main.add_from_address'))
assert response.status_code == 302
assert response.location == url_for('main.service_dashboard', service_id=101, _external=True) assert response.location == url_for('main.service_dashboard', service_id=101, _external=True)
assert mock_create_service.called assert mock_create_service.called
assert mock_get_services.called
def test_should_return_form_errors_when_service_name_is_empty(app_, def test_should_return_form_errors_when_service_name_is_empty(app_,
@@ -87,7 +47,7 @@ def test_should_return_form_errors_when_service_name_is_empty(app_,
client.login(api_user_active) client.login(api_user_active)
response = client.post(url_for('main.add_service'), data={}) response = client.post(url_for('main.add_service'), data={})
assert response.status_code == 200 assert response.status_code == 200
assert 'Service name can not be empty' in response.get_data(as_text=True) assert 'Service name cant be empty' in response.get_data(as_text=True)
def test_should_return_form_errors_with_duplicate_service_name(app_, def test_should_return_form_errors_with_duplicate_service_name(app_,
@@ -102,5 +62,5 @@ def test_should_return_form_errors_with_duplicate_service_name(app_,
response = client.post( response = client.post(
url_for('main.add_service'), data={'name': 'service_one'}) url_for('main.add_service'), data={'name': 'service_one'})
assert response.status_code == 200 assert response.status_code == 200
assert 'Service name already exists' in response.get_data(as_text=True) assert 'This service name is already in use' in response.get_data(as_text=True)
assert mock_get_services.called assert mock_get_services.called
+12 -6
View File
@@ -21,7 +21,8 @@ def test_should_show_empty_api_keys_page(app_,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_get_no_api_keys, mock_get_no_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
@@ -39,7 +40,8 @@ def test_should_show_api_keys_page(app_,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_get_api_keys, mock_get_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
@@ -58,7 +60,8 @@ def test_should_show_name_api_key_page(app_,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_get_api_keys, mock_get_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
@@ -74,7 +77,8 @@ def test_should_render_show_api_key(app_,
mock_get_user_by_email, mock_get_user_by_email,
mock_create_api_key, mock_create_api_key,
mock_get_api_keys, mock_get_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
@@ -92,7 +96,8 @@ def test_should_show_confirm_revoke_api_key(app_,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_get_api_keys, mock_get_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
@@ -110,7 +115,8 @@ def test_should_redirect_after_revoking_api_key(app_,
mock_get_user_by_email, mock_get_user_by_email,
mock_revoke_api_key, mock_revoke_api_key,
mock_get_api_keys, mock_get_api_keys,
mock_login): mock_login,
mock_has_permissions):
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)
+1
View File
@@ -27,6 +27,7 @@ def test_should_show_page_for_one_job(app_,
mock_login, mock_login,
mock_get_user, mock_get_user,
mock_get_user_by_email, mock_get_user_by_email,
mock_get_service,
mock_get_service_template, mock_get_service_template,
job_data, job_data,
mock_get_job): mock_get_job):
+26 -8
View File
@@ -1,13 +1,15 @@
import json
from flask import url_for from flask import url_for
from bs4 import BeautifulSoup
def test_should_show_overview_page( 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 mock_get_users_by_service,
mock_get_invites_for_service
): ):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -38,7 +40,8 @@ def test_redirect_after_saving_user(
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_service, mock_get_service,
mock_get_users_by_service mock_get_users_by_service,
mock_get_invites_for_service
): ):
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -70,19 +73,34 @@ def test_should_show_page_for_inviting_user(
def test_invite_user( def test_invite_user(
app_, app_,
service_one,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_service, mock_get_users_by_service,
mock_get_users_by_service mock_create_invite,
mock_get_invites_for_service
): ):
from_user = api_user_active.id
service_id = service_one['id']
email_address = 'test@example.gov.uk'
permissions = 'send_messages,manage_service,manage_api_keys'
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.post( response = client.post(
url_for('main.invite_user', service_id=55555), url_for('main.invite_user', service_id=service_id),
data={'email_address': 'test@example.gov.uk'}, data={'email_address': email_address,
'send_messages': 'yes',
'manage_service': 'yes',
'manage_api_keys': 'yes'},
follow_redirects=True follow_redirects=True
) )
assert response.status_code == 200 assert response.status_code == 200
assert 'Invite sent to test@example.gov.uk' in response.get_data(as_text=True) mock_create_invite.assert_called_with(from_user, service_id, email_address, permissions)
mock_get_invites_for_service.assert_called_with(service_id=service_id)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.h1.string.strip() == 'Manage team'
flash_banner = page.find('div', class_='banner-default-with-tick').string.strip()
assert flash_banner == 'Invite sent to test@example.gov.uk'
+56 -31
View File
@@ -1,8 +1,8 @@
import pytest
from io import BytesIO from io import BytesIO
from flask import url_for from flask import url_for
from unittest.mock import ANY
import pytest
import moto
template_types = ['email', 'sms'] template_types = ['email', 'sms']
@@ -57,17 +57,19 @@ def test_upload_empty_csvfile_returns_to_upload_page(
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
def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors( def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(
app_, app_,
api_user_active, api_user_active,
mocker,
mock_login, mock_login,
mock_get_service_template mock_get_service,
mock_get_service_template,
mock_s3_upload
): ):
contents = 'phone\n+44 123\n+44 456' contents = 'to,name\n+44 123,test1\n+44 456,test2'
file_data = (BytesIO(contents.encode('utf-8')), 'invalid.csv') file_data = (BytesIO(contents.encode('utf-8')), 'invalid.csv')
mocker.patch('app.main.views.send.s3download', return_value=contents)
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -86,18 +88,51 @@ def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(
assert 'Upload a CSV file' in content assert 'Upload a CSV file' in content
@pytest.mark.skipif(True, reason='Errors on travis') def test_upload_csvfile_removes_empty_lines_and_trailing_commas(
@moto.mock_s3 app_,
api_user_active,
mocker,
mock_login,
mock_get_service,
mock_get_service_template,
mock_s3_upload
):
contents = 'to,name,,,\n++44 7700 900981,test1,,,\n+44 7700 900981,test2,,,\n ,,, \n ,,, \t \t \n'
file_data = (BytesIO(contents.encode('utf-8')), 'invalid.csv')
expected_data = {'data': ['to,name', '++44 7700 900981,test1', '+44 7700 900981,test2'],
'file_name': 'invalid.csv'}
mocker.patch('app.main.views.send.s3download',
return_value='to,name\n++44 7700 900981,test1\n+44 7700 900981,test2')
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
upload_data = {'file': file_data}
response = client.post(
url_for('main.send_messages', service_id=12345, template_id=54321),
data=upload_data,
follow_redirects=True
)
assert response.status_code == 200
mock_s3_upload.assert_called_with(ANY, '12345', expected_data, 'eu-west-1')
def test_send_test_message_to_self( def test_send_test_message_to_self(
app_, app_,
mocker, mocker,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_user, mock_get_service,
mock_get_user_by_email, mock_get_service_template,
mock_get_service_template mock_s3_upload
): ):
expected_data = {'data': ['to', '+4412341234'], 'file_name': 'Test run'}
mocker.patch('app.main.views.send.s3download', return_value='to\r\n+4412341234')
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)
@@ -106,20 +141,15 @@ def test_send_test_message_to_self(
follow_redirects=True follow_redirects=True
) )
assert response.status_code == 200 assert response.status_code == 200
content = response.get_data(as_text=True) mock_s3_upload.assert_called_with(ANY, '12345', expected_data, 'eu-west-1')
assert 'Test run' in content
assert '+4412341234' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3
def test_download_example_csv( def test_download_example_csv(
app_, app_,
mocker, mocker,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_user, mock_get_service,
mock_get_user_by_email,
mock_get_service_template mock_get_service_template
): ):
@@ -131,25 +161,23 @@ def test_download_example_csv(
follow_redirects=True follow_redirects=True
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.get_data(as_text=True) == 'phone\r\n+4412341234\r\n' assert response.get_data(as_text=True) == 'to\r\n+4412341234\r\n'
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
def test_upload_csvfile_with_valid_phone_shows_all_numbers( def test_upload_csvfile_with_valid_phone_shows_all_numbers(
app_, app_,
mocker, mocker,
api_user_active, api_user_active,
mock_login, mock_login,
mock_get_user, mock_get_service,
mock_get_user_by_email, mock_get_service_template,
mock_get_service_template mock_s3_upload
): ):
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 = 'to\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
file_data = (BytesIO(contents.encode('utf-8')), 'valid.csv') file_data = (BytesIO(contents.encode('utf-8')), 'valid.csv')
mocker.patch('app.main.views.send.s3download', return_value=contents)
with app_.test_request_context(): with app_.test_request_context():
with app_.test_client() as client: with app_.test_client() as client:
@@ -174,18 +202,15 @@ def test_upload_csvfile_with_valid_phone_shows_all_numbers(
assert '+44 7700 900986' in content assert '+44 7700 900986' in content
@pytest.mark.skipif(True, reason='Errors on travis')
@moto.mock_s3
def test_create_job_should_call_api( def test_create_job_should_call_api(
app_, app_,
service_one, service_one,
api_user_active, api_user_active,
mock_get_user,
mock_get_user_by_email,
mock_login, mock_login,
job_data, job_data,
mock_create_job, mock_create_job,
mock_get_job, mock_get_job,
mock_get_service,
mock_get_service_template mock_get_service_template
): ):
+54 -6
View File
@@ -1,4 +1,4 @@
import os import uuid
from datetime import date from datetime import date
import pytest import pytest
@@ -9,7 +9,8 @@ from . import (
TestClient, TestClient,
template_json, template_json,
api_key_json, api_key_json,
job_json job_json,
invite_json
) )
@@ -204,7 +205,8 @@ def api_user_pending():
'email_address': 'test@user.gov.uk', 'email_address': 'test@user.gov.uk',
'mobile_number': '+4412341234', 'mobile_number': '+4412341234',
'state': 'pending', 'state': 'pending',
'failed_login_count': 0 'failed_login_count': 0,
'permissions': {}
} }
user = User(user_data) user = User(user_data)
return user return user
@@ -219,7 +221,8 @@ def api_user_active():
'email_address': 'test@user.gov.uk', 'email_address': 'test@user.gov.uk',
'mobile_number': '+4412341234', 'mobile_number': '+4412341234',
'state': 'active', 'state': 'active',
'failed_login_count': 0 'failed_login_count': 0,
'permissions': {}
} }
user = User(user_data) user = User(user_data)
return user return user
@@ -234,7 +237,8 @@ def api_user_locked():
'email_address': 'test@user.gov.uk', 'email_address': 'test@user.gov.uk',
'mobile_number': '+4412341234', 'mobile_number': '+4412341234',
'state': 'active', 'state': 'active',
'failed_login_count': 5 'failed_login_count': 5,
'permissions': {}
} }
user = User(user_data) user = User(user_data)
return user return user
@@ -249,7 +253,8 @@ def api_user_request_password_reset():
'email_address': 'test@user.gov.uk', 'email_address': 'test@user.gov.uk',
'mobile_number': '+4412341234', 'mobile_number': '+4412341234',
'state': 'request_password_reset', 'state': 'request_password_reset',
'failed_login_count': 5 'failed_login_count': 5,
'permissions': {}
} }
user = User(user_data) user = User(user_data)
return user return user
@@ -512,6 +517,15 @@ def mock_get_jobs(mocker):
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_has_permissions(mocker):
def _has_permission(service_id, permissions):
return True
return mocker.patch(
'app.notify_client.user_api_client.User.has_permissions',
side_effect=_has_permission)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def mock_get_users_by_service(mocker): def mock_get_users_by_service(mocker):
def _get_users_for_service(service_id): def _get_users_for_service(service_id):
@@ -526,3 +540,37 @@ def mock_get_users_by_service(mocker):
'failed_login_count': 0}] 'failed_login_count': 0}]
return data return data
return mocker.patch('app.user_api_client.get_users_for_service', side_effect=_get_users_for_service, autospec=True) return mocker.patch('app.user_api_client.get_users_for_service', side_effect=_get_users_for_service, autospec=True)
@pytest.fixture(scope='function')
def mock_s3_upload(mocker):
def _upload(upload_id, service_id, filedata, region):
pass
return mocker.patch('app.main.views.send.s3upload', side_effect=_upload)
@pytest.fixture(scope='function')
def mock_create_invite(mocker):
def _create_invite(from_user, service_id, email_address, permissions):
data = {'id': uuid.uuid4(),
'from_user': from_user,
'service': service_id,
'email_address': email_address,
'status': 'pending',
'permissions': permissions}
return data
return mocker.patch('app.invite_api_client.create_invite', side_effect=_create_invite)
@pytest.fixture(scope='function')
def mock_get_invites_for_service(mocker, service_one):
def _get_invites(service_id):
data = []
from_user = service_one['users'][0]
service_id = service_one['id']
for i in range(0, 5):
email_address = 'user_{}@testnotify.gov.uk'.format(i)
invite = invite_json(uuid.uuid4(), from_user, service_id, email_address)
data.append(invite)
return data
return mocker.patch('app.invite_api_client.get_invites_for_service', side_effect=_get_invites)