mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-27 19:29:11 -04:00
Merge branch 'master' of github.com:alphagov/notifications-admin
This commit is contained in:
@@ -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.job_api_client import JobApiClient
|
||||
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.asset_fingerprinter import AssetFingerprinter
|
||||
from app.utils import validate_phone_number, InvalidPhoneError
|
||||
@@ -30,7 +32,9 @@ user_api_client = UserApiClient()
|
||||
api_key_api_client = ApiKeyApiClient()
|
||||
job_api_client = JobApiClient()
|
||||
status_api_client = StatusApiClient()
|
||||
invite_api_client = InviteApiClient()
|
||||
asset_fingerprinter = AssetFingerprinter()
|
||||
permission_api_client = PermissionApiClient()
|
||||
|
||||
|
||||
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)
|
||||
job_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.login_view = 'main.sign_in'
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
margin-bottom: $gutter;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bottom-gutter-2-3 {
|
||||
margin-bottom: $gutter * 2/3;
|
||||
}
|
||||
|
||||
@@ -164,13 +164,13 @@ class AddServiceForm(Form):
|
||||
name = StringField(
|
||||
'Service name',
|
||||
validators=[
|
||||
DataRequired(message='Service name can not be empty')
|
||||
DataRequired(message='Service name can’t be empty')
|
||||
]
|
||||
)
|
||||
|
||||
def validate_name(self, a):
|
||||
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):
|
||||
|
||||
@@ -11,39 +11,15 @@ from app.main.forms import AddServiceForm
|
||||
@login_required
|
||||
def add_service():
|
||||
form = AddServiceForm(services_dao.find_all_service_names)
|
||||
services = services_dao.get_services(current_user.id)
|
||||
if len(services['data']) == 0:
|
||||
heading = 'Which service do you want to set up notifications for?'
|
||||
else:
|
||||
heading = 'Add a new service'
|
||||
heading = 'Which service do you want to set up notifications for?'
|
||||
if form.validate_on_submit():
|
||||
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:
|
||||
return render_template(
|
||||
'views/add-service.html',
|
||||
form=form,
|
||||
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())
|
||||
])
|
||||
|
||||
@@ -3,6 +3,7 @@ from flask_login import login_required
|
||||
from app.main import main
|
||||
from app.main.forms import CreateKeyForm
|
||||
from app import api_key_api_client
|
||||
from app.utils import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/documentation")
|
||||
@@ -13,6 +14,7 @@ def documentation(service_id):
|
||||
|
||||
@main.route("/services/<service_id>/api-keys")
|
||||
@login_required
|
||||
@user_has_permissions('manage_api_keys')
|
||||
def api_keys(service_id):
|
||||
return render_template(
|
||||
'views/api-keys.html',
|
||||
@@ -23,6 +25,7 @@ def api_keys(service_id):
|
||||
|
||||
@main.route("/services/<service_id>/api-keys/create", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_api_keys')
|
||||
def create_api_key(service_id):
|
||||
key_names = [
|
||||
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'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_api_keys')
|
||||
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']
|
||||
if request.method == 'GET':
|
||||
|
||||
@@ -23,6 +23,7 @@ def service_dashboard(service_id):
|
||||
try:
|
||||
service = get_service_by_id(service_id)
|
||||
session['service_name'] = service['data']['name']
|
||||
session['service_id'] = service['data']['id']
|
||||
except HTTPError as e:
|
||||
if e.status_code == 404:
|
||||
abort(404)
|
||||
|
||||
@@ -13,6 +13,7 @@ from notification_utils.template import Template
|
||||
from app import job_api_client
|
||||
from app.main import main
|
||||
from app.main.dao import templates_dao
|
||||
from app.main.dao import services_dao
|
||||
|
||||
now = time.strftime('%H:%M')
|
||||
|
||||
@@ -37,6 +38,7 @@ def view_jobs(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']
|
||||
messages = []
|
||||
@@ -56,9 +58,11 @@ def view_job(service_id, job_id):
|
||||
uploaded_file_name=job['original_file_name'],
|
||||
uploaded_file_time=job['created_at'],
|
||||
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:
|
||||
if e.status_code == 404:
|
||||
|
||||
@@ -7,13 +7,18 @@ from flask import (
|
||||
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.dao import users_dao
|
||||
from app.main.forms import InviteUserForm
|
||||
from app.main.dao.services_dao import get_service_by_id_or_404
|
||||
from app import user_api_client
|
||||
from app import invite_api_client
|
||||
|
||||
fake_users = [
|
||||
{
|
||||
@@ -29,14 +34,19 @@ fake_users = [
|
||||
@main.route("/services/<service_id>/users")
|
||||
@login_required
|
||||
def manage_users(service_id):
|
||||
users = user_api_client.get_users_for_service(service_id=service_id)
|
||||
return render_template(
|
||||
'views/manage-users.html',
|
||||
service_id=service_id,
|
||||
users=users,
|
||||
current_user=current_user,
|
||||
invited_users=[]
|
||||
)
|
||||
try:
|
||||
users = user_api_client.get_users_for_service(service_id=service_id)
|
||||
invited_users = invite_api_client.get_invites_for_service(service_id=service_id)
|
||||
return render_template('views/manage-users.html',
|
||||
service_id=service_id,
|
||||
users=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'])
|
||||
@@ -44,10 +54,19 @@ def manage_users(service_id):
|
||||
def invite_user(service_id):
|
||||
|
||||
form = InviteUserForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
flash('Invite sent to {}'.format(form.email_address.data), 'default_with_tick')
|
||||
return redirect(url_for('.manage_users', service_id=service_id))
|
||||
email_address = form.email_address.data
|
||||
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(
|
||||
'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_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)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import csv
|
||||
import io
|
||||
import uuid
|
||||
import botocore
|
||||
|
||||
from datetime import date
|
||||
|
||||
from flask import (
|
||||
request,
|
||||
@@ -17,7 +14,6 @@ from flask import (
|
||||
)
|
||||
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug import secure_filename
|
||||
from notifications_python_client.errors import HTTPError
|
||||
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.utils import validate_recipient, InvalidPhoneError, InvalidEmailError
|
||||
|
||||
first_column_header = {
|
||||
'email': 'email',
|
||||
'sms': 'phone'
|
||||
page_headings = {
|
||||
'email': 'Send emails',
|
||||
'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'])
|
||||
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']:
|
||||
abort(404)
|
||||
@@ -53,11 +56,17 @@ def choose_template(service_id, template_type):
|
||||
else:
|
||||
raise e
|
||||
return render_template(
|
||||
'views/choose-{}-template.html'.format(template_type),
|
||||
'views/choose-template.html',
|
||||
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
|
||||
],
|
||||
template_type=template_type,
|
||||
page_heading=page_headings[template_type],
|
||||
service=service,
|
||||
has_jobs=len(jobs),
|
||||
service_id=service_id
|
||||
)
|
||||
@@ -70,7 +79,7 @@ def send_messages(service_id, template_id):
|
||||
form = CsvUploadForm()
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
csv_file = form.file.data
|
||||
csv_file = form.file
|
||||
filedata = _get_filedata(csv_file)
|
||||
upload_id = str(uuid.uuid4())
|
||||
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,
|
||||
upload_id=upload_id))
|
||||
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))
|
||||
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(
|
||||
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(
|
||||
'views/send.html',
|
||||
template=template,
|
||||
column_headers=[first_column_header[template.template_type]] + template.placeholders_as_markup,
|
||||
column_headers=['to'] + template.placeholders_as_markup,
|
||||
form=form,
|
||||
service=service,
|
||||
service_id=service_id
|
||||
@@ -105,10 +115,14 @@ def get_example_csv(service_id, template_id):
|
||||
placeholders = list(Template(template).placeholders)
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow([first_column_header[template['template_type']]] + 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'})
|
||||
writer.writerow(['to'] + placeholders)
|
||||
writer.writerow([
|
||||
{
|
||||
'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'])
|
||||
@@ -118,7 +132,7 @@ def send_message_to_self(service_id, template_id):
|
||||
placeholders = list(Template(template).placeholders)
|
||||
output = io.StringIO()
|
||||
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])
|
||||
filedata = {
|
||||
'file_name': 'Test run',
|
||||
@@ -140,29 +154,30 @@ def check_messages(service_id, upload_id):
|
||||
|
||||
upload_data = session['upload_data']
|
||||
template_id = upload_data.get('template_id')
|
||||
service = services_dao.get_service_by_id_or_404(service_id)
|
||||
|
||||
if request.method == 'GET':
|
||||
contents = s3download(service_id, upload_id)
|
||||
if not contents:
|
||||
flash('There was a problem reading your upload file')
|
||||
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)
|
||||
session['upload_data']['notification_count'] = len(upload_result['rows'])
|
||||
template = Template(
|
||||
raw_template,
|
||||
values=upload_result['rows'][0] if upload_result['valid'] else {},
|
||||
drop_values={recipient_type}
|
||||
drop_values={'to'},
|
||||
prefix=service['name']
|
||||
)
|
||||
return render_template(
|
||||
'views/check-sms.html',
|
||||
'views/check.html',
|
||||
upload_result=upload_result,
|
||||
template=template,
|
||||
column_headers=[recipient_type] + list(
|
||||
template.placeholders if upload_result['valid'] else template.placeholders_as_markup
|
||||
),
|
||||
page_heading=page_headings[template.template_type],
|
||||
column_headers=['to'] + list(template.placeholders_as_markup),
|
||||
original_file_name=upload_data.get('original_file_name'),
|
||||
service_id=service_id,
|
||||
service=service,
|
||||
form=CsvUploadForm()
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
@@ -184,17 +199,34 @@ def check_messages(service_id, upload_id):
|
||||
|
||||
|
||||
def _get_filedata(file):
|
||||
lines = file.read().decode('utf-8').splitlines()
|
||||
if len(lines) < 2: # must be at least header and one line
|
||||
message = 'The file {} contained no data'.format(file.filename)
|
||||
import itertools
|
||||
reader = csv.reader(
|
||||
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)
|
||||
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):
|
||||
reader = csv.DictReader(
|
||||
contents.split('\n'),
|
||||
lineterminator='\n',
|
||||
quoting=csv.QUOTE_NONE,
|
||||
skipinitialspace=True
|
||||
)
|
||||
@@ -203,12 +235,11 @@ def _get_rows(contents, raw_template):
|
||||
for row in reader:
|
||||
rows.append(row)
|
||||
try:
|
||||
recipient_column = first_column_header[raw_template['template_type']]
|
||||
validate_recipient(
|
||||
row[recipient_column],
|
||||
row.get('to', ''),
|
||||
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):
|
||||
valid = False
|
||||
return {"valid": valid, "rows": rows}
|
||||
|
||||
29
app/notify_client/invite_api_client.py
Normal file
29
app/notify_client/invite_api_client.py
Normal 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']
|
||||
@@ -1,4 +1,3 @@
|
||||
import uuid
|
||||
|
||||
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):
|
||||
data = {
|
||||
"id": job_id,
|
||||
"service": service_id,
|
||||
"template": template_id,
|
||||
"original_file_name": original_file_name,
|
||||
"bucket_name": "service-{}-notify".format(service_id),
|
||||
|
||||
25
app/notify_client/permission_api_client.py
Normal file
25
app/notify_client/permission_api_client.py
Normal file
@@ -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']
|
||||
@@ -94,7 +94,7 @@ class User(UserMixin):
|
||||
self._email_address = fields.get('email_address')
|
||||
self._mobile_number = fields.get('mobile_number')
|
||||
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._state = fields.get('state')
|
||||
self.max_failed_login_count = max_failed_login_count
|
||||
@@ -165,18 +165,12 @@ class User(UserMixin):
|
||||
|
||||
@permissions.setter
|
||||
def permissions(self, permissions):
|
||||
if permissions is None:
|
||||
permissions = set()
|
||||
self._permissions = set(permissions)
|
||||
raise AttributeError("Read only property")
|
||||
|
||||
def add_permissions(self, permissions):
|
||||
self._permissions.update(permissions)
|
||||
|
||||
def remove_permissions(self, permissions):
|
||||
self._permissions -= permissions
|
||||
|
||||
def has_permissions(self, permissions):
|
||||
return self._permissions > set(permissions)
|
||||
def has_permissions(self, service_id, permissions):
|
||||
if service_id in self._permissions:
|
||||
return set(self._permissions[service_id]) > set(permissions)
|
||||
return False
|
||||
|
||||
@property
|
||||
def failed_login_count(self):
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
</legend>
|
||||
<div class='yes-no-fields inline'>
|
||||
<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
|
||||
</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
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
<h2 class="navigation-service-name">
|
||||
<a href="{{ url_for('.service_dashboard', service_id=service_id) }}">{{ session.get('service_name', 'Service') }}</a>
|
||||
</h2>
|
||||
{% if current_user.has_permissions(session.get('service_id', ''), ['send_messages']) %}
|
||||
<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='email') }}">Send emails</a></li>
|
||||
<li><a href="{{ url_for('.letters_stub', service_id=service_id) }}">Send letters</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions(session.get('service_id', ''), ['manage_service']) %}
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions(session.get('service_id', ''), ['manage_api_keys']) %}
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -16,16 +16,29 @@
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Be specific. Remember that there might be other people in your
|
||||
organisation using GOV.UK Notify.
|
||||
Be specific to your service. Remember that there might be
|
||||
other people in your organisation using GOV.UK Notify.
|
||||
</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">
|
||||
|
||||
{{ textbox(form.name, hint="You can change this later") }}
|
||||
|
||||
|
||||
{{ page_footer('Continue') }}
|
||||
{{ page_footer('Add service') }}
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
@@ -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" %}
|
||||
{% from "components/email-message.html" import email_message %}
|
||||
{% from "components/sms-message.html" import sms_message %}
|
||||
{% from "components/table.html" import list_table, field %}
|
||||
{% from "components/placeholder.html" import placeholder %}
|
||||
@@ -6,7 +7,7 @@
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
{% block page_title %}
|
||||
Check and confirm – GOV.UK Notify
|
||||
{{ "Check and confirm" if upload_result.valid else page_heading }} – GOV.UK Notify
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -25,18 +26,25 @@
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
{% if template.missing_data or template.additional_data %}
|
||||
{{ sms_message(template.formatted_as_markup)}}
|
||||
{% else %}
|
||||
{{ sms_message(template.replaced)}}
|
||||
{% endif %}
|
||||
{% if 'email' == template.template_type %}
|
||||
{{ email_message(
|
||||
template.subject,
|
||||
template.replaced if upload_result.valid else template.formatted_as_markup,
|
||||
from_address='{}@notifications.service.gov.uk'.format(service.email_from),
|
||||
from_name=service.name
|
||||
)}}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
||||
{% if upload_result.valid %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
@@ -53,13 +61,13 @@
|
||||
caption=original_file_name,
|
||||
field_headings=column_headers
|
||||
) %}
|
||||
{% if item.phone|valid_phone_number %}
|
||||
{% if item.to or ''|valid_phone_number %}
|
||||
{% call field() %}
|
||||
{{ item.phone }}
|
||||
{{ item.to }}
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% call field(status='missing') %}
|
||||
{{ item.phone }}
|
||||
{{ item.to }}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% 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" %}
|
||||
{% from "components/email-message.html" import email_message %}
|
||||
{% from "components/sms-message.html" import sms_message %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
|
||||
{% block page_title %}
|
||||
Send text messages – GOV.UK Notify
|
||||
{{ page_heading }} – GOV.UK Notify
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
|
||||
@@ -26,7 +27,15 @@
|
||||
<div class="grid-row">
|
||||
{% for template in templates %}
|
||||
<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 class="column-one-third">
|
||||
<div class="sms-message-use-links">
|
||||
@@ -40,7 +49,7 @@
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
|
||||
</form>
|
||||
@@ -161,7 +161,7 @@
|
||||
<h2 class="heading-medium">API endpoint</h2>
|
||||
|
||||
<p>
|
||||
https://www.notify.works/api/endpoint
|
||||
https://api.notify.works
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -28,6 +28,7 @@ Manage users – GOV.UK Notify
|
||||
<legend class='heading-small'>
|
||||
Permissions
|
||||
</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('manage_service', 'Manage service', user.permission_manage_service) }}
|
||||
{{ yes_no('manage_api_keys', 'Manage API keys', user.permission_manage_api_keys) }}
|
||||
|
||||
@@ -14,13 +14,22 @@
|
||||
{{ uploaded_file_name }}
|
||||
</h1>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
{{ sms_message(
|
||||
template.formatted_as_markup,
|
||||
)}}
|
||||
{% if 'sms' == template.template_type %}
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
{{ sms_message(
|
||||
template,
|
||||
)}}
|
||||
</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'>
|
||||
Started {{ uploaded_file_time|format_datetime }}
|
||||
|
||||
17
app/templates/views/letters.html
Normal file
17
app/templates/views/letters.html
Normal 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 %}
|
||||
@@ -41,7 +41,7 @@ Manage users – GOV.UK Notify
|
||||
invited_users, caption='Invited', **table_options
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.email_localpart }}
|
||||
{{ item.email_address }}
|
||||
{% endcall %}
|
||||
{{ boolean_field(item.permission_send_messages) }}
|
||||
{{ boolean_field(item.permission_manage_service) }}
|
||||
|
||||
@@ -13,31 +13,42 @@
|
||||
|
||||
<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="column-two-thirds">
|
||||
{% if 'sms' == template.template_type %}
|
||||
{{ 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(
|
||||
'You can upload real data, but we’ll 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'
|
||||
)}}
|
||||
<p>
|
||||
Add recipients by uploading a .csv file with
|
||||
{{ template.placeholders|length + 1 }}
|
||||
{% if template.placeholders %}
|
||||
columns:
|
||||
{% else %}
|
||||
column:
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="bottom-gutter-2-3">
|
||||
<span class='placeholder'>to</span>
|
||||
{{ template.placeholders_as_markup|join(" ") }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url_for('.get_example_csv', service_id=service_id, template_id=template.id) }}">Download an example</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{file_upload(form.file, button_text='Upload a 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>
|
||||
{{file_upload(form.file, button_text='Upload your CSV file')}}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
|
||||
from functools import wraps
|
||||
from flask import abort
|
||||
from flask import (abort, session)
|
||||
|
||||
|
||||
class BrowsableItem(object):
|
||||
@@ -100,8 +100,10 @@ def user_has_permissions(*permissions):
|
||||
def wrap_func(*args, **kwargs):
|
||||
# We are making the assumption that the user is logged in.
|
||||
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)
|
||||
return func(*args, **kwargs)
|
||||
return wrap_func
|
||||
return wrap
|
||||
|
||||
@@ -29,9 +29,8 @@
|
||||
"gulp-base64": "0.1.3",
|
||||
"gulp-concat": "2.6.0",
|
||||
"gulp-include": "2.1.0",
|
||||
"gulp-jquery": "1.1.1",
|
||||
"gulp-load-plugins": "1.1.0",
|
||||
"gulp-sass": "2.1.1",
|
||||
"gulp-sass": "2.2.0",
|
||||
"gulp-uglify": "1.5.1",
|
||||
"jquery": "1.11.2",
|
||||
"query-command-supported": "1.0.0"
|
||||
|
||||
@@ -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-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
|
||||
|
||||
@@ -47,6 +47,15 @@ def api_key_json(id_, name, expiry_date=None):
|
||||
'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'
|
||||
|
||||
|
||||
@@ -57,14 +66,15 @@ def create_test_user(state):
|
||||
return user
|
||||
|
||||
|
||||
def create_test_api_user(state):
|
||||
def create_test_api_user(state, permissions={}):
|
||||
from app.notify_client.user_api_client import User
|
||||
user_data = {'id': 1,
|
||||
'name': 'Test User',
|
||||
'password': 'somepassword',
|
||||
'email_address': TEST_USER_EMAIL,
|
||||
'mobile_number': '+441234123412',
|
||||
'state': state
|
||||
'state': state,
|
||||
'permissions': permissions
|
||||
}
|
||||
user = User(user_data)
|
||||
return user
|
||||
|
||||
@@ -11,7 +11,6 @@ def test_client_creates_job_data_correctly(mocker):
|
||||
|
||||
expected_data = {
|
||||
"id": job_id,
|
||||
"service": service_id,
|
||||
"template": template_id,
|
||||
"original_file_name": original_file_name,
|
||||
"bucket_name": "service-{}-notify".format(service_id),
|
||||
|
||||
@@ -9,4 +9,4 @@ def test_form_should_have_errors_when_duplicate_service_is_added(app_):
|
||||
form = AddServiceForm(_get_form_names,
|
||||
formdata=MultiDict([('name', 'some service')]))
|
||||
form.validate()
|
||||
assert {'name': ['Service name already exists']} == form.errors
|
||||
assert {'name': ['This service name is already in use']} == form.errors
|
||||
|
||||
@@ -14,7 +14,7 @@ def test_get_should_render_add_service_template(app_,
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.add_service'))
|
||||
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_,
|
||||
@@ -31,48 +31,8 @@ def test_should_add_service_and_redirect_to_next_page(app_,
|
||||
url_for('main.add_service'),
|
||||
data={'name': 'testing the post'})
|
||||
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 mock_create_service.called
|
||||
assert mock_get_services.called
|
||||
|
||||
|
||||
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)
|
||||
response = client.post(url_for('main.add_service'), data={})
|
||||
assert response.status_code == 200
|
||||
assert 'Service name can not be empty' in response.get_data(as_text=True)
|
||||
assert 'Service name can’t be empty' in response.get_data(as_text=True)
|
||||
|
||||
|
||||
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(
|
||||
url_for('main.add_service'), data={'name': 'service_one'})
|
||||
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
|
||||
|
||||
@@ -21,7 +21,8 @@ def test_should_show_empty_api_keys_page(app_,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_no_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -39,7 +40,8 @@ def test_should_show_api_keys_page(app_,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -58,7 +60,8 @@ def test_should_show_name_api_key_page(app_,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -74,7 +77,8 @@ def test_should_render_show_api_key(app_,
|
||||
mock_get_user_by_email,
|
||||
mock_create_api_key,
|
||||
mock_get_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -92,7 +96,8 @@ def test_should_show_confirm_revoke_api_key(app_,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -110,7 +115,8 @@ def test_should_redirect_after_revoking_api_key(app_,
|
||||
mock_get_user_by_email,
|
||||
mock_revoke_api_key,
|
||||
mock_get_api_keys,
|
||||
mock_login):
|
||||
mock_login,
|
||||
mock_has_permissions):
|
||||
with app_.test_request_context():
|
||||
with app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
|
||||
@@ -27,6 +27,7 @@ def test_should_show_page_for_one_job(app_,
|
||||
mock_login,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_service,
|
||||
mock_get_service_template,
|
||||
job_data,
|
||||
mock_get_job):
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import json
|
||||
from flask import url_for
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def test_should_show_overview_page(
|
||||
app_,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
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_client() as client:
|
||||
@@ -38,7 +40,8 @@ def test_redirect_after_saving_user(
|
||||
api_user_active,
|
||||
mock_login,
|
||||
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_client() as client:
|
||||
@@ -70,19 +73,34 @@ def test_should_show_page_for_inviting_user(
|
||||
|
||||
def test_invite_user(
|
||||
app_,
|
||||
service_one,
|
||||
api_user_active,
|
||||
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_client() as client:
|
||||
client.login(api_user_active)
|
||||
response = client.post(
|
||||
url_for('main.invite_user', service_id=55555),
|
||||
data={'email_address': 'test@example.gov.uk'},
|
||||
url_for('main.invite_user', service_id=service_id),
|
||||
data={'email_address': email_address,
|
||||
'send_messages': 'yes',
|
||||
'manage_service': 'yes',
|
||||
'manage_api_keys': 'yes'},
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
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'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from io import BytesIO
|
||||
from flask import url_for
|
||||
|
||||
import pytest
|
||||
import moto
|
||||
from unittest.mock import ANY
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason='Errors on travis')
|
||||
@moto.mock_s3
|
||||
def test_upload_csvfile_with_invalid_phone_shows_check_page_with_errors(
|
||||
app_,
|
||||
api_user_active,
|
||||
mocker,
|
||||
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')
|
||||
mocker.patch('app.main.views.send.s3download', return_value=contents)
|
||||
|
||||
with app_.test_request_context():
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason='Errors on travis')
|
||||
@moto.mock_s3
|
||||
def test_upload_csvfile_removes_empty_lines_and_trailing_commas(
|
||||
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(
|
||||
app_,
|
||||
mocker,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_service_template
|
||||
mock_get_service,
|
||||
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_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -106,20 +141,15 @@ def test_send_test_message_to_self(
|
||||
follow_redirects=True
|
||||
)
|
||||
assert response.status_code == 200
|
||||
content = response.get_data(as_text=True)
|
||||
assert 'Test run' in content
|
||||
assert '+4412341234' in content
|
||||
mock_s3_upload.assert_called_with(ANY, '12345', expected_data, 'eu-west-1')
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason='Errors on travis')
|
||||
@moto.mock_s3
|
||||
def test_download_example_csv(
|
||||
app_,
|
||||
mocker,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_service,
|
||||
mock_get_service_template
|
||||
):
|
||||
|
||||
@@ -131,25 +161,23 @@ def test_download_example_csv(
|
||||
follow_redirects=True
|
||||
)
|
||||
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']
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason='Errors on travis')
|
||||
@moto.mock_s3
|
||||
def test_upload_csvfile_with_valid_phone_shows_all_numbers(
|
||||
app_,
|
||||
mocker,
|
||||
api_user_active,
|
||||
mock_login,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_get_service_template
|
||||
mock_get_service,
|
||||
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')
|
||||
mocker.patch('app.main.views.send.s3download', return_value=contents)
|
||||
|
||||
with app_.test_request_context():
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.skipif(True, reason='Errors on travis')
|
||||
@moto.mock_s3
|
||||
def test_create_job_should_call_api(
|
||||
app_,
|
||||
service_one,
|
||||
api_user_active,
|
||||
mock_get_user,
|
||||
mock_get_user_by_email,
|
||||
mock_login,
|
||||
job_data,
|
||||
mock_create_job,
|
||||
mock_get_job,
|
||||
mock_get_service,
|
||||
mock_get_service_template
|
||||
):
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
import uuid
|
||||
from datetime import date
|
||||
import pytest
|
||||
|
||||
@@ -9,7 +9,8 @@ from . import (
|
||||
TestClient,
|
||||
template_json,
|
||||
api_key_json,
|
||||
job_json
|
||||
job_json,
|
||||
invite_json
|
||||
)
|
||||
|
||||
|
||||
@@ -204,7 +205,8 @@ def api_user_pending():
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'pending',
|
||||
'failed_login_count': 0
|
||||
'failed_login_count': 0,
|
||||
'permissions': {}
|
||||
}
|
||||
user = User(user_data)
|
||||
return user
|
||||
@@ -219,7 +221,8 @@ def api_user_active():
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'active',
|
||||
'failed_login_count': 0
|
||||
'failed_login_count': 0,
|
||||
'permissions': {}
|
||||
}
|
||||
user = User(user_data)
|
||||
return user
|
||||
@@ -234,7 +237,8 @@ def api_user_locked():
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'active',
|
||||
'failed_login_count': 5
|
||||
'failed_login_count': 5,
|
||||
'permissions': {}
|
||||
}
|
||||
user = User(user_data)
|
||||
return user
|
||||
@@ -249,7 +253,8 @@ def api_user_request_password_reset():
|
||||
'email_address': 'test@user.gov.uk',
|
||||
'mobile_number': '+4412341234',
|
||||
'state': 'request_password_reset',
|
||||
'failed_login_count': 5
|
||||
'failed_login_count': 5,
|
||||
'permissions': {}
|
||||
}
|
||||
user = User(user_data)
|
||||
return user
|
||||
@@ -512,6 +517,15 @@ def mock_get_jobs(mocker):
|
||||
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')
|
||||
def mock_get_users_by_service(mocker):
|
||||
def _get_users_for_service(service_id):
|
||||
@@ -526,3 +540,37 @@ def mock_get_users_by_service(mocker):
|
||||
'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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
Reference in New Issue
Block a user