mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-26 17:24:23 -04:00
merge from main
This commit is contained in:
@@ -94,7 +94,6 @@ from app.notify_client import InviteTokenError
|
|||||||
from app.notify_client.api_key_api_client import api_key_api_client
|
from app.notify_client.api_key_api_client import api_key_api_client
|
||||||
from app.notify_client.billing_api_client import billing_api_client
|
from app.notify_client.billing_api_client import billing_api_client
|
||||||
from app.notify_client.complaint_api_client import complaint_api_client
|
from app.notify_client.complaint_api_client import complaint_api_client
|
||||||
from app.notify_client.contact_list_api_client import contact_list_api_client
|
|
||||||
from app.notify_client.email_branding_client import email_branding_client
|
from app.notify_client.email_branding_client import email_branding_client
|
||||||
from app.notify_client.events_api_client import events_api_client
|
from app.notify_client.events_api_client import events_api_client
|
||||||
from app.notify_client.inbound_number_client import inbound_number_client
|
from app.notify_client.inbound_number_client import inbound_number_client
|
||||||
@@ -216,7 +215,6 @@ def create_app(application):
|
|||||||
# API clients
|
# API clients
|
||||||
api_key_api_client,
|
api_key_api_client,
|
||||||
billing_api_client,
|
billing_api_client,
|
||||||
contact_list_api_client,
|
|
||||||
complaint_api_client,
|
complaint_api_client,
|
||||||
email_branding_client,
|
email_branding_client,
|
||||||
events_api_client,
|
events_api_client,
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ class Development(Config):
|
|||||||
|
|
||||||
# Buckets
|
# Buckets
|
||||||
CSV_UPLOAD_BUCKET = _s3_credentials_from_env('CSV')
|
CSV_UPLOAD_BUCKET = _s3_credentials_from_env('CSV')
|
||||||
CONTACT_LIST_BUCKET = _s3_credentials_from_env('CONTACT')
|
|
||||||
LOGO_UPLOAD_BUCKET = _s3_credentials_from_env('LOGO')
|
LOGO_UPLOAD_BUCKET = _s3_credentials_from_env('LOGO')
|
||||||
|
|
||||||
# credential overrides
|
# credential overrides
|
||||||
@@ -127,8 +126,6 @@ class Production(Config):
|
|||||||
# buckets
|
# buckets
|
||||||
CSV_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
CSV_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||||
f"notify-api-csv-upload-bucket-{getenv('NOTIFY_ENVIRONMENT')}")
|
f"notify-api-csv-upload-bucket-{getenv('NOTIFY_ENVIRONMENT')}")
|
||||||
CONTACT_LIST_BUCKET = cloud_config.s3_credentials(
|
|
||||||
f"notify-api-contact-list-bucket-{getenv('NOTIFY_ENVIRONMENT')}")
|
|
||||||
LOGO_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
LOGO_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||||
f"notify-admin-logo-upload-bucket-{getenv('NOTIFY_ENVIRONMENT')}")
|
f"notify-admin-logo-upload-bucket-{getenv('NOTIFY_ENVIRONMENT')}")
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ from app.main.forms import (
|
|||||||
SetSenderForm,
|
SetSenderForm,
|
||||||
get_placeholder_form_instance,
|
get_placeholder_form_instance,
|
||||||
)
|
)
|
||||||
from app.models.contact_list import ContactList, ContactListsAlphabetical
|
|
||||||
from app.models.user import Users
|
from app.models.user import Users
|
||||||
from app.s3_client.s3_csv_client import (
|
from app.s3_client.s3_csv_client import (
|
||||||
get_csv_metadata,
|
get_csv_metadata,
|
||||||
@@ -390,47 +389,6 @@ def send_one_off_step(service_id, template_id, step_index):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route(
|
|
||||||
'/services/<uuid:service_id>/send/<uuid:template_id>'
|
|
||||||
'/from-contact-list'
|
|
||||||
)
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def choose_from_contact_list(service_id, template_id):
|
|
||||||
db_template = current_service.get_template_with_user_permission_or_403(
|
|
||||||
template_id, current_user
|
|
||||||
)
|
|
||||||
template = get_template(
|
|
||||||
db_template, current_service,
|
|
||||||
)
|
|
||||||
return render_template(
|
|
||||||
'views/send-contact-list.html',
|
|
||||||
contact_lists=ContactListsAlphabetical(
|
|
||||||
current_service.id,
|
|
||||||
template_type=template.template_type,
|
|
||||||
),
|
|
||||||
template=template,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@main.route(
|
|
||||||
'/services/<uuid:service_id>/send/<uuid:template_id>'
|
|
||||||
'/from-contact-list/<uuid:contact_list_id>'
|
|
||||||
)
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def send_from_contact_list(service_id, template_id, contact_list_id):
|
|
||||||
contact_list = ContactList.from_id(
|
|
||||||
contact_list_id,
|
|
||||||
service_id=current_service.id,
|
|
||||||
)
|
|
||||||
return redirect(url_for(
|
|
||||||
'main.check_messages',
|
|
||||||
service_id=current_service.id,
|
|
||||||
template_id=template_id,
|
|
||||||
upload_id=contact_list.copy_to_uploads(),
|
|
||||||
contact_list_id=contact_list.id,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
def _check_messages(service_id, template_id, upload_id, preview_row):
|
def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||||
try:
|
try:
|
||||||
# The happy path is that the job doesn’t already exist, so the
|
# The happy path is that the job doesn’t already exist, so the
|
||||||
@@ -573,7 +531,6 @@ def start_job(service_id, upload_id):
|
|||||||
upload_id,
|
upload_id,
|
||||||
service_id,
|
service_id,
|
||||||
scheduled_for=request.form.get('scheduled_for', ''),
|
scheduled_for=request.form.get('scheduled_for', ''),
|
||||||
contact_list_id=request.form.get('contact_list_id', ''),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
session.pop('sender_id', None)
|
session.pop('sender_id', None)
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
import itertools
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO
|
|
||||||
from zipfile import BadZipFile
|
|
||||||
|
|
||||||
from flask import flash, redirect, render_template, request, send_file, url_for
|
from flask import render_template, request
|
||||||
from notifications_utils.insensitive_dict import InsensitiveDict
|
|
||||||
from notifications_utils.recipients import RecipientCSV
|
|
||||||
from notifications_utils.sanitise_text import SanitiseASCII
|
|
||||||
from xlrd.biffh import XLRDError
|
|
||||||
from xlrd.xldate import XLDateError
|
|
||||||
|
|
||||||
from app import current_service
|
from app import current_service
|
||||||
from app.main import main
|
from app.main import main
|
||||||
from app.main.forms import CsvUploadForm
|
|
||||||
from app.models.contact_list import ContactList
|
|
||||||
from app.utils import unicode_truncate
|
|
||||||
from app.utils.csv import Spreadsheet, get_errors_for_csv
|
|
||||||
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
||||||
from app.utils.templates import get_sample_template
|
|
||||||
from app.utils.user import user_has_permissions
|
from app.utils.user import user_has_permissions
|
||||||
|
|
||||||
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
||||||
@@ -39,7 +26,6 @@ def uploads(service_id):
|
|||||||
|
|
||||||
if uploads.current_page == 1:
|
if uploads.current_page == 1:
|
||||||
listed_uploads = (
|
listed_uploads = (
|
||||||
current_service.contact_lists +
|
|
||||||
current_service.scheduled_jobs +
|
current_service.scheduled_jobs +
|
||||||
uploads
|
uploads
|
||||||
)
|
)
|
||||||
@@ -53,198 +39,3 @@ def uploads(service_id):
|
|||||||
next_page=next_page,
|
next_page=next_page,
|
||||||
now=datetime.utcnow().isoformat(),
|
now=datetime.utcnow().isoformat(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/upload-contact-list", methods=['GET', 'POST'])
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def upload_contact_list(service_id):
|
|
||||||
form = CsvUploadForm()
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
|
||||||
try:
|
|
||||||
upload_id = ContactList.upload(
|
|
||||||
current_service.id,
|
|
||||||
Spreadsheet.from_file_form(form).as_dict,
|
|
||||||
)
|
|
||||||
file_name_metadata = unicode_truncate(
|
|
||||||
SanitiseASCII.encode(form.file.data.filename),
|
|
||||||
1600
|
|
||||||
)
|
|
||||||
ContactList.set_metadata(
|
|
||||||
current_service.id,
|
|
||||||
upload_id,
|
|
||||||
original_file_name=file_name_metadata
|
|
||||||
)
|
|
||||||
return redirect(url_for(
|
|
||||||
'.check_contact_list',
|
|
||||||
service_id=service_id,
|
|
||||||
upload_id=upload_id,
|
|
||||||
))
|
|
||||||
except (UnicodeDecodeError, BadZipFile, XLRDError):
|
|
||||||
flash('Could not read {}. Try using a different file format.'.format(
|
|
||||||
form.file.data.filename
|
|
||||||
))
|
|
||||||
except (XLDateError):
|
|
||||||
flash((
|
|
||||||
'{} contains numbers or dates that Notify cannot understand. '
|
|
||||||
'Try formatting all columns as ‘text’ or export your file as CSV.'
|
|
||||||
).format(
|
|
||||||
form.file.data.filename
|
|
||||||
))
|
|
||||||
elif form.errors:
|
|
||||||
# just show the first error, as we don't expect the form to have more
|
|
||||||
# than one, since it only has one field
|
|
||||||
first_field_errors = list(form.errors.values())[0]
|
|
||||||
flash(first_field_errors[0])
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/upload.html',
|
|
||||||
form=form,
|
|
||||||
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@main.route(
|
|
||||||
"/services/<uuid:service_id>/check-contact-list/<uuid:upload_id>",
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def check_contact_list(service_id, upload_id):
|
|
||||||
|
|
||||||
form = CsvUploadForm()
|
|
||||||
|
|
||||||
contents = ContactList.download(service_id, upload_id)
|
|
||||||
first_row = contents.splitlines()[0].strip().rstrip(',') if contents else ''
|
|
||||||
original_file_name = ContactList.get_metadata(service_id, upload_id).get('original_file_name', '')
|
|
||||||
|
|
||||||
template_type = InsensitiveDict({
|
|
||||||
'email address': 'email',
|
|
||||||
'phone number': 'sms',
|
|
||||||
}).get(first_row)
|
|
||||||
|
|
||||||
recipients = RecipientCSV(
|
|
||||||
contents,
|
|
||||||
template=get_sample_template(template_type or 'sms'),
|
|
||||||
guestlist=itertools.chain.from_iterable(
|
|
||||||
[user.name, user.mobile_number, user.email_address]
|
|
||||||
for user in current_service.active_users
|
|
||||||
) if current_service.trial_mode else None,
|
|
||||||
allow_international_sms=current_service.has_permission('international_sms'),
|
|
||||||
max_initial_rows_shown=50,
|
|
||||||
max_errors_shown=50,
|
|
||||||
)
|
|
||||||
|
|
||||||
non_empty_column_headers = list(filter(None, recipients.column_headers))
|
|
||||||
|
|
||||||
if len(non_empty_column_headers) > 1 or not template_type or not recipients:
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/too-many-columns.html',
|
|
||||||
recipients=recipients,
|
|
||||||
original_file_name=original_file_name,
|
|
||||||
template_type=template_type,
|
|
||||||
form=form,
|
|
||||||
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
if recipients.too_many_rows or not len(recipients):
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/column-errors.html',
|
|
||||||
recipients=recipients,
|
|
||||||
original_file_name=original_file_name,
|
|
||||||
form=form,
|
|
||||||
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
row_errors = get_errors_for_csv(recipients, template_type)
|
|
||||||
if row_errors:
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/row-errors.html',
|
|
||||||
recipients=recipients,
|
|
||||||
original_file_name=original_file_name,
|
|
||||||
row_errors=row_errors,
|
|
||||||
form=form,
|
|
||||||
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
if recipients.has_errors:
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/column-errors.html',
|
|
||||||
recipients=recipients,
|
|
||||||
original_file_name=original_file_name,
|
|
||||||
form=form,
|
|
||||||
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
metadata_kwargs = {
|
|
||||||
'row_count': len(recipients),
|
|
||||||
'valid': True,
|
|
||||||
'original_file_name': original_file_name,
|
|
||||||
'template_type': template_type
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactList.set_metadata(service_id, upload_id, **metadata_kwargs)
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/ok.html',
|
|
||||||
recipients=recipients,
|
|
||||||
original_file_name=original_file_name,
|
|
||||||
upload_id=upload_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/save-contact-list/<uuid:upload_id>", methods=['POST'])
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def save_contact_list(service_id, upload_id):
|
|
||||||
ContactList.create(current_service.id, upload_id)
|
|
||||||
return redirect(url_for(
|
|
||||||
'.uploads',
|
|
||||||
service_id=current_service.id,
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/contact-list/<uuid:contact_list_id>", methods=['GET'])
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def contact_list(service_id, contact_list_id):
|
|
||||||
contact_list = ContactList.from_id(contact_list_id, service_id=service_id)
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/contact-list.html',
|
|
||||||
contact_list=contact_list,
|
|
||||||
jobs=contact_list.get_jobs(
|
|
||||||
page=1,
|
|
||||||
limit_days=current_service.get_days_of_retention(contact_list.template_type),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/contact-list/<uuid:contact_list_id>/delete", methods=['GET', 'POST'])
|
|
||||||
@user_has_permissions('manage_templates')
|
|
||||||
def delete_contact_list(service_id, contact_list_id):
|
|
||||||
contact_list = ContactList.from_id(contact_list_id, service_id=service_id)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
contact_list.delete()
|
|
||||||
return redirect(url_for(
|
|
||||||
'.uploads',
|
|
||||||
service_id=service_id,
|
|
||||||
))
|
|
||||||
|
|
||||||
flash([
|
|
||||||
f"Are you sure you want to delete ‘{contact_list.original_file_name}’?",
|
|
||||||
], 'delete')
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'views/uploads/contact-list/contact-list.html',
|
|
||||||
contact_list=contact_list,
|
|
||||||
confirm_delete_banner=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/contact-list/<uuid:contact_list_id>.csv", methods=['GET'])
|
|
||||||
@user_has_permissions('send_messages')
|
|
||||||
def download_contact_list(service_id, contact_list_id):
|
|
||||||
contact_list = ContactList.from_id(contact_list_id, service_id=service_id)
|
|
||||||
return send_file(
|
|
||||||
path_or_file=BytesIO(contact_list.contents.encode('utf-8')),
|
|
||||||
download_name=contact_list.saved_file_name,
|
|
||||||
as_attachment=True,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
from functools import partial
|
|
||||||
from os import path
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from flask import abort, current_app
|
|
||||||
from notifications_utils.formatters import strip_all_whitespace
|
|
||||||
from notifications_utils.recipients import RecipientCSV
|
|
||||||
from notifications_utils.s3 import s3upload as utils_s3upload
|
|
||||||
from werkzeug.utils import cached_property
|
|
||||||
|
|
||||||
from app.models import JSONModel, ModelList
|
|
||||||
from app.models.job import PaginatedJobsAndScheduledJobs
|
|
||||||
from app.notify_client.contact_list_api_client import contact_list_api_client
|
|
||||||
from app.s3_client import (
|
|
||||||
get_s3_contents,
|
|
||||||
get_s3_metadata,
|
|
||||||
get_s3_object,
|
|
||||||
set_s3_metadata,
|
|
||||||
)
|
|
||||||
from app.s3_client.s3_csv_client import s3upload, set_metadata_on_csv_upload
|
|
||||||
from app.utils.templates import get_sample_template
|
|
||||||
|
|
||||||
|
|
||||||
class ContactList(JSONModel):
|
|
||||||
|
|
||||||
ALLOWED_PROPERTIES = {
|
|
||||||
'id',
|
|
||||||
'created_at',
|
|
||||||
'created_by',
|
|
||||||
'has_jobs',
|
|
||||||
'recent_job_count',
|
|
||||||
'service_id',
|
|
||||||
'original_file_name',
|
|
||||||
'row_count',
|
|
||||||
'template_type',
|
|
||||||
}
|
|
||||||
|
|
||||||
upload_type = 'contact_list'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_id(cls, contact_list_id, *, service_id):
|
|
||||||
return cls(contact_list_api_client.get_contact_list(
|
|
||||||
service_id=service_id,
|
|
||||||
contact_list_id=contact_list_id,
|
|
||||||
))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_bucket_credentials(key):
|
|
||||||
return current_app.config['CONTACT_LIST_BUCKET'][key]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_bucket_name():
|
|
||||||
return ContactList.get_bucket_credentials('bucket')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_access_key():
|
|
||||||
return ContactList.get_bucket_credentials('access_key_id')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_secret_key():
|
|
||||||
return ContactList.get_bucket_credentials('secret_access_key')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_region():
|
|
||||||
return ContactList.get_bucket_credentials('region')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_filename(service_id, upload_id):
|
|
||||||
return f"service-{service_id}-notify/{upload_id}.csv"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_s3_arguments(service_id, upload_id):
|
|
||||||
return (
|
|
||||||
ContactList.get_bucket_name(),
|
|
||||||
ContactList.get_filename(service_id, upload_id),
|
|
||||||
ContactList.get_access_key(),
|
|
||||||
ContactList.get_secret_key(),
|
|
||||||
ContactList.get_region(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def upload(service_id, file_dict):
|
|
||||||
upload_id = str(uuid4())
|
|
||||||
utils_s3upload(
|
|
||||||
filedata=file_dict['data'],
|
|
||||||
region=ContactList.get_region(),
|
|
||||||
bucket_name=ContactList.get_bucket_name(),
|
|
||||||
file_location=ContactList.get_filename(service_id, upload_id),
|
|
||||||
access_key=ContactList.get_access_key(),
|
|
||||||
secret_key=ContactList.get_secret_key(),
|
|
||||||
)
|
|
||||||
return upload_id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def download(service_id, upload_id):
|
|
||||||
return strip_all_whitespace(
|
|
||||||
get_s3_contents(
|
|
||||||
get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id))))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_metadata(service_id, upload_id, **kwargs):
|
|
||||||
return set_s3_metadata(get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id)), **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_metadata(service_id, upload_id):
|
|
||||||
return get_s3_metadata(get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id)))
|
|
||||||
|
|
||||||
def copy_to_uploads(self):
|
|
||||||
raise RuntimeError("RCA probably an issue with copying between buckets")
|
|
||||||
metadata = self.get_metadata(self.service_id, self.id)
|
|
||||||
new_upload_id = s3upload(
|
|
||||||
self.service_id,
|
|
||||||
{'data': self.contents},
|
|
||||||
ContactList.get_region(),
|
|
||||||
)
|
|
||||||
set_metadata_on_csv_upload(
|
|
||||||
self.service_id,
|
|
||||||
new_upload_id,
|
|
||||||
**metadata,
|
|
||||||
)
|
|
||||||
return new_upload_id
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create(cls, service_id, upload_id):
|
|
||||||
|
|
||||||
metadata = cls.get_metadata(service_id, upload_id)
|
|
||||||
|
|
||||||
if not metadata.get('valid'):
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
return cls(contact_list_api_client.create_contact_list(
|
|
||||||
service_id=service_id,
|
|
||||||
upload_id=upload_id,
|
|
||||||
original_file_name=metadata['original_file_name'],
|
|
||||||
row_count=int(metadata['row_count']),
|
|
||||||
template_type=metadata['template_type'],
|
|
||||||
))
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
contact_list_api_client.delete_contact_list(
|
|
||||||
service_id=self.service_id,
|
|
||||||
contact_list_id=self.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def contents(self):
|
|
||||||
return self.download(self.service_id, self.id)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def recipients(self):
|
|
||||||
return RecipientCSV(
|
|
||||||
self.contents,
|
|
||||||
template=get_sample_template(self.template_type),
|
|
||||||
allow_international_sms=True,
|
|
||||||
max_initial_rows_shown=50,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def saved_file_name(self):
|
|
||||||
file_name, extention = path.splitext(self.original_file_name)
|
|
||||||
return f'{file_name}.csv'
|
|
||||||
|
|
||||||
def get_jobs(self, *, page, limit_days=None):
|
|
||||||
return PaginatedJobsAndScheduledJobs(
|
|
||||||
self.service_id,
|
|
||||||
contact_list_id=self.id,
|
|
||||||
page=page,
|
|
||||||
limit_days=limit_days,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContactLists(ModelList):
|
|
||||||
|
|
||||||
client_method = contact_list_api_client.get_contact_lists
|
|
||||||
model = ContactList
|
|
||||||
sort_function = partial(
|
|
||||||
sorted,
|
|
||||||
key=lambda item: item['created_at'],
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, service_id, template_type=None):
|
|
||||||
super().__init__(service_id)
|
|
||||||
self.items = self.sort_function([
|
|
||||||
item for item in self.items
|
|
||||||
if template_type in {item['template_type'], None}
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class ContactListsAlphabetical(ContactLists):
|
|
||||||
|
|
||||||
sort_function = partial(
|
|
||||||
sorted,
|
|
||||||
key=lambda item: item['original_file_name'].lower(),
|
|
||||||
)
|
|
||||||
@@ -172,10 +172,9 @@ class PaginatedJobs(PaginatedModelList, ImmediateJobs):
|
|||||||
client_method = job_api_client.get_page_of_jobs
|
client_method = job_api_client.get_page_of_jobs
|
||||||
statuses = None
|
statuses = None
|
||||||
|
|
||||||
def __init__(self, service_id, *, contact_list_id=None, page=None, limit_days=None):
|
def __init__(self, service_id, *, page=None, limit_days=None):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
service_id,
|
service_id,
|
||||||
contact_list_id=contact_list_id,
|
|
||||||
statuses=self.statuses,
|
statuses=self.statuses,
|
||||||
page=page,
|
page=page,
|
||||||
limit_days=limit_days,
|
limit_days=limit_days,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from notifications_utils.serialised_model import SerialisedModelCollection
|
|||||||
from werkzeug.utils import cached_property
|
from werkzeug.utils import cached_property
|
||||||
|
|
||||||
from app.models import JSONModel, SortByNameMixin
|
from app.models import JSONModel, SortByNameMixin
|
||||||
from app.models.contact_list import ContactLists
|
|
||||||
from app.models.job import (
|
from app.models.job import (
|
||||||
ImmediateJobs,
|
ImmediateJobs,
|
||||||
PaginatedJobs,
|
PaginatedJobs,
|
||||||
@@ -527,10 +526,6 @@ class Service(JSONModel, SortByNameMixin):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def contact_lists(self):
|
|
||||||
return ContactLists(self.id)
|
|
||||||
|
|
||||||
|
|
||||||
class Services(SerialisedModelCollection):
|
class Services(SerialisedModelCollection):
|
||||||
model = Service
|
model = Service
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ class MainNavigation(Navigation):
|
|||||||
'add_service_template',
|
'add_service_template',
|
||||||
'check_messages',
|
'check_messages',
|
||||||
'check_notification',
|
'check_notification',
|
||||||
'choose_from_contact_list',
|
|
||||||
'choose_template',
|
'choose_template',
|
||||||
'choose_template_to_copy',
|
'choose_template_to_copy',
|
||||||
'confirm_redact_template',
|
'confirm_redact_template',
|
||||||
@@ -161,11 +160,6 @@ class MainNavigation(Navigation):
|
|||||||
'view_template_versions',
|
'view_template_versions',
|
||||||
},
|
},
|
||||||
'uploads': {
|
'uploads': {
|
||||||
'upload_contact_list',
|
|
||||||
'check_contact_list',
|
|
||||||
'save_contact_list',
|
|
||||||
'contact_list',
|
|
||||||
'delete_contact_list',
|
|
||||||
'uploads',
|
'uploads',
|
||||||
'view_job',
|
'view_job',
|
||||||
'view_jobs',
|
'view_jobs',
|
||||||
@@ -244,7 +238,6 @@ class CaseworkNavigation(Navigation):
|
|||||||
|
|
||||||
mapping = {
|
mapping = {
|
||||||
'send-one-off': {
|
'send-one-off': {
|
||||||
'choose_from_contact_list',
|
|
||||||
'choose_template',
|
'choose_template',
|
||||||
'send_one_off',
|
'send_one_off',
|
||||||
'send_one_off_step',
|
'send_one_off_step',
|
||||||
@@ -257,11 +250,6 @@ class CaseworkNavigation(Navigation):
|
|||||||
'uploads': {
|
'uploads': {
|
||||||
'view_jobs',
|
'view_jobs',
|
||||||
'view_job',
|
'view_job',
|
||||||
'upload_contact_list',
|
|
||||||
'check_contact_list',
|
|
||||||
'save_contact_list',
|
|
||||||
'contact_list',
|
|
||||||
'delete_contact_list',
|
|
||||||
'uploads',
|
'uploads',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
|
||||||
|
|
||||||
|
|
||||||
class ContactListApiClient(NotifyAdminAPIClient):
|
|
||||||
|
|
||||||
def create_contact_list(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
service_id,
|
|
||||||
upload_id,
|
|
||||||
original_file_name,
|
|
||||||
row_count,
|
|
||||||
template_type,
|
|
||||||
):
|
|
||||||
data = {
|
|
||||||
"id": upload_id,
|
|
||||||
"original_file_name": original_file_name,
|
|
||||||
"row_count": row_count,
|
|
||||||
"template_type": template_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
data = _attach_current_user(data)
|
|
||||||
job = self.post(url='/service/{}/contact-list'.format(service_id), data=data)
|
|
||||||
|
|
||||||
return job
|
|
||||||
|
|
||||||
def get_contact_lists(self, service_id):
|
|
||||||
return self.get(f'/service/{service_id}/contact-list')
|
|
||||||
|
|
||||||
def get_contact_list(self, *, service_id, contact_list_id):
|
|
||||||
return self.get(f'/service/{service_id}/contact-list/{contact_list_id}')
|
|
||||||
|
|
||||||
def delete_contact_list(self, *, service_id, contact_list_id):
|
|
||||||
return self.delete(f'/service/{service_id}/contact-list/{contact_list_id}')
|
|
||||||
|
|
||||||
|
|
||||||
contact_list_api_client = ContactListApiClient()
|
|
||||||
@@ -25,14 +25,12 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def get_jobs(self, service_id, *, limit_days=None, contact_list_id=None, statuses=None, page=1):
|
def get_jobs(self, service_id, *, limit_days=None, statuses=None, page=1):
|
||||||
params = {'page': page}
|
params = {'page': page}
|
||||||
if limit_days is not None:
|
if limit_days is not None:
|
||||||
params['limit_days'] = limit_days
|
params['limit_days'] = limit_days
|
||||||
if statuses is not None:
|
if statuses is not None:
|
||||||
params['statuses'] = ','.join(statuses)
|
params['statuses'] = ','.join(statuses)
|
||||||
if contact_list_id is not None:
|
|
||||||
params['contact_list_id'] = contact_list_id
|
|
||||||
|
|
||||||
return self.get(url='/service/{}/job'.format(service_id), params=params)
|
return self.get(url='/service/{}/job'.format(service_id), params=params)
|
||||||
|
|
||||||
@@ -53,12 +51,11 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
if job['job_status'] != 'cancelled'
|
if job['job_status'] != 'cancelled'
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_page_of_jobs(self, service_id, *, page, statuses=None, contact_list_id=None, limit_days=None):
|
def get_page_of_jobs(self, service_id, *, page, statuses=None, limit_days=None):
|
||||||
return self.get_jobs(
|
return self.get_jobs(
|
||||||
service_id,
|
service_id,
|
||||||
statuses=statuses or self.NON_SCHEDULED_JOB_STATUSES,
|
statuses=statuses or self.NON_SCHEDULED_JOB_STATUSES,
|
||||||
page=page,
|
page=page,
|
||||||
contact_list_id=contact_list_id,
|
|
||||||
limit_days=limit_days,
|
limit_days=limit_days,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,15 +85,12 @@ class JobApiClient(NotifyAdminAPIClient):
|
|||||||
def has_jobs(self, service_id):
|
def has_jobs(self, service_id):
|
||||||
return bool(self.get_jobs(service_id)['data'])
|
return bool(self.get_jobs(service_id)['data'])
|
||||||
|
|
||||||
def create_job(self, job_id, service_id, scheduled_for=None, contact_list_id=None):
|
def create_job(self, job_id, service_id, scheduled_for=None):
|
||||||
data = {"id": job_id}
|
data = {"id": job_id}
|
||||||
|
|
||||||
if scheduled_for:
|
if scheduled_for:
|
||||||
data.update({'scheduled_for': scheduled_for})
|
data.update({'scheduled_for': scheduled_for})
|
||||||
|
|
||||||
if contact_list_id:
|
|
||||||
data.update({'contact_list_id': contact_list_id})
|
|
||||||
|
|
||||||
data = _attach_current_user(data)
|
data = _attach_current_user(data)
|
||||||
job = self.post(url='/service/{}/job'.format(service_id), data=data)
|
job = self.post(url='/service/{}/job'.format(service_id), data=data)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
<div class="bottom-gutter-3-2">
|
<div class="bottom-gutter-3-2">
|
||||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
|
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="hidden" name="contact_list_id" value="{{ request.args.get('contact_list_id', '') }}" />
|
|
||||||
{% if choose_time_form %}
|
{% if choose_time_form %}
|
||||||
{{ choose_time_form.scheduled_for(param_extensions={
|
{{ choose_time_form.scheduled_for(param_extensions={
|
||||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||||
@@ -41,12 +40,12 @@
|
|||||||
}
|
}
|
||||||
}) }}
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set button_text %}
|
{% set button_text %}
|
||||||
Send {{ count_of_recipients|message_count(template.template_type) }}
|
Send {{ count_of_recipients|message_count(template.template_type) }}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
{{ govukButton({ "text": button_text }) }}
|
{{ govukButton({ "text": button_text }) }}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,31 +17,13 @@
|
|||||||
) %}
|
) %}
|
||||||
{% call row_heading() %}
|
{% call row_heading() %}
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
{% if item.upload_type == 'contact_list' %}
|
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.contact_list', service_id=current_service.id, contact_list_id=item.id) }}">{{ item.original_file_name }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if item.scheduled %}
|
{% if item.scheduled %}
|
||||||
<span class="file-list-hint-large">
|
<span class="file-list-hint-large">
|
||||||
Sending {{
|
Sending {{
|
||||||
item.scheduled_for|format_datetime_relative
|
item.scheduled_for|format_datetime_relative
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
{% elif item.upload_type == 'contact_list' %}
|
|
||||||
<span class="file-list-hint-large">
|
|
||||||
{% if item.recent_job_count %}
|
|
||||||
Used {{ item.recent_job_count|iteration_count }}
|
|
||||||
in the last
|
|
||||||
{{ current_service.get_days_of_retention(item.template_type) }}
|
|
||||||
days
|
|
||||||
{% elif item.has_jobs %}
|
|
||||||
Not used in the last
|
|
||||||
{{ current_service.get_days_of_retention(item.template_type) }}
|
|
||||||
days
|
|
||||||
{% else %}
|
|
||||||
Not used yet
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="file-list-hint-large">
|
<span class="file-list-hint-large">
|
||||||
Sent {{
|
Sent {{
|
||||||
@@ -62,12 +44,6 @@
|
|||||||
suffix='waiting to send'
|
suffix='waiting to send'
|
||||||
)
|
)
|
||||||
) }}
|
) }}
|
||||||
{% elif item.upload_type == 'contact_list' %}
|
|
||||||
{{ big_number(
|
|
||||||
item.row_count,
|
|
||||||
smallest=True,
|
|
||||||
label="saved {}".format(item.row_count|recipient_count_label(item.template_type))
|
|
||||||
) }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="govuk-grid-row">
|
<div class="govuk-grid-row">
|
||||||
<div class="govuk-grid-column-one-third">
|
<div class="govuk-grid-column-one-third">
|
||||||
|
|||||||
@@ -17,15 +17,5 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ previous_next_navigation(prev_page, next_page) }}
|
{{ previous_next_navigation(prev_page, next_page) }}
|
||||||
{% if current_user.has_permissions('send_messages') %}
|
|
||||||
<div class="js-stick-at-bottom-when-scrolling">
|
|
||||||
{{ govukButton({
|
|
||||||
"element": "a",
|
|
||||||
"text": "Upload an emergency contact list",
|
|
||||||
"href": url_for('.upload_contact_list', service_id=current_service.id),
|
|
||||||
"classes": "govuk-button--secondary"
|
|
||||||
}) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/big-number.html" import big_number -%}
|
|
||||||
{% from "components/list.html" import list_of_placeholders %}
|
|
||||||
{% from "components/page-header.html" import page_header %}
|
|
||||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
Choose an emergency contact list
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('.send_one_off', service_id=current_service.id, template_id=template.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
{{ page_header('Choose an emergency contact list') }}
|
|
||||||
|
|
||||||
{% if template.placeholders %}
|
|
||||||
<div class="govuk-grid-row">
|
|
||||||
<div class="govuk-grid-column-five-sixths">
|
|
||||||
<p class="govuk-body">
|
|
||||||
You cannot use an emergency contact list with this template because it
|
|
||||||
is personalized with {{ list_of_placeholders(template.placeholders) }}.
|
|
||||||
</p>
|
|
||||||
<p class="govuk-body">
|
|
||||||
Emergency contact lists can only include email addresses or phone
|
|
||||||
numbers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif contact_lists %}
|
|
||||||
<div class='dashboard-table ajax-block-container'>
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
contact_lists,
|
|
||||||
caption="Emergency contact lists",
|
|
||||||
caption_visible=False,
|
|
||||||
empty_message=(
|
|
||||||
'You have not saved any contact lists yet.'
|
|
||||||
),
|
|
||||||
field_headings=[
|
|
||||||
'File',
|
|
||||||
'Status'
|
|
||||||
],
|
|
||||||
field_headings_visible=False
|
|
||||||
) %}
|
|
||||||
{% call row_heading() %}
|
|
||||||
<div class="file-list">
|
|
||||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('main.send_from_contact_list', service_id=current_service.id, template_id=template.id, contact_list_id=item.id) }}">{{ item.original_file_name }}</a>
|
|
||||||
<span class="file-list-hint-large">
|
|
||||||
Uploaded {{ item.created_at|format_datetime_relative }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
|
||||||
{% call field() %}
|
|
||||||
{{ big_number(
|
|
||||||
item.row_count,
|
|
||||||
smallest=True,
|
|
||||||
label=item.row_count|recipient_count_label(item.template_type),
|
|
||||||
) }}
|
|
||||||
{% endcall %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="govuk-grid-row">
|
|
||||||
<div class="govuk-grid-column-five-sixths">
|
|
||||||
<p class="govuk-body">
|
|
||||||
You have not saved any lists of {{ 99|recipient_count_label(template.template_type) }} yet.
|
|
||||||
</p>
|
|
||||||
<p class="govuk-body">
|
|
||||||
To upload and save an emergency contact list, go to the <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.uploads', service_id=current_service.id) }}">uploads</a> page.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -31,9 +31,6 @@
|
|||||||
<div class="govuk-grid-column-full">
|
<div class="govuk-grid-column-full">
|
||||||
{% if link_to_upload %}
|
{% if link_to_upload %}
|
||||||
<a class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-3" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">Upload a list of {{ 999|recipient_count_label(template.template_type) }}</a>
|
<a class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-3" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">Upload a list of {{ 999|recipient_count_label(template.template_type) }}</a>
|
||||||
{% if current_service.contact_lists %}
|
|
||||||
<a class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-3" href="{{ url_for('.choose_from_contact_list', service_id=current_service.id, template_id=template.id) }}">Use an emergency list</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if skip_link %}
|
{% if skip_link %}
|
||||||
<a href="{{ skip_link[1] }}" class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-3">{{ skip_link[0] }}</a>
|
<a href="{{ skip_link[1] }}" class="govuk-link govuk-link--no-visited-state govuk-!-margin-right-3">{{ skip_link[0] }}</a>
|
||||||
@@ -49,6 +46,6 @@
|
|||||||
{{ page_footer('Continue') }}
|
{{ page_footer('Continue') }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/radios.html" import radio_select %}
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
|
||||||
{% from "components/file-upload.html" import file_upload %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
Error
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
<div class="bottom-gutter-1-2">
|
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
|
||||||
|
|
||||||
{% if recipients.too_many_rows %}
|
|
||||||
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
|
||||||
Your file has too many rows
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
Notify can store files up to
|
|
||||||
{{ "{:,}".format(recipients.max_rows) }} rows in size. Your
|
|
||||||
file has {{ "{:,}".format(recipients|length) }} rows.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% elif not recipients.allowed_to_send_to %}
|
|
||||||
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Trial mode: bad recipients" data-error-label="{{ upload_id }}">
|
|
||||||
You cannot save
|
|
||||||
{{ 'this' if recipients|length == 1 else 'these' }}
|
|
||||||
{{ recipients|length|recipient_count_label(recipients.template_type) }}
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
In <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.trial_mode_new') }}">trial mode</a> you can only
|
|
||||||
send to yourself and members of your team
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="js-stick-at-top-when-scrolling">
|
|
||||||
<div class="form-group">
|
|
||||||
{{ file_upload(
|
|
||||||
form.file,
|
|
||||||
allowed_file_extensions=allowed_file_extensions,
|
|
||||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
|
||||||
button_text='Upload your file again'
|
|
||||||
) }}
|
|
||||||
</div>
|
|
||||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="heading-medium" id="file-preview">{{ original_file_name }}</h2>
|
|
||||||
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
recipients.displayed_rows,
|
|
||||||
caption=original_file_name,
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=[
|
|
||||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
|
||||||
] + recipients.column_headers
|
|
||||||
) %}
|
|
||||||
{{ index_field(item.index + 2) }}
|
|
||||||
{% for column in recipients.column_headers %}
|
|
||||||
{{ text_field(item[column].data or '') }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/big-number.html" import big_number %}
|
|
||||||
{% from "components/radios.html" import radio_select %}
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading, row, row_heading %}
|
|
||||||
{% from "components/page-header.html" import page_header %}
|
|
||||||
{% from "components/uk_components/button/macro.njk" import govukButton %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
{{ contact_list.original_file_name }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.uploads', service_id=current_service.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
{{ page_header(contact_list.original_file_name) }}
|
|
||||||
|
|
||||||
<p class="govuk-body">
|
|
||||||
Uploaded by {{ contact_list.created_by }} {{ contact_list.created_at|format_datetime_human }}.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if jobs %}
|
|
||||||
<p class="govuk-body">
|
|
||||||
Used {{ jobs|length|iteration_count }}
|
|
||||||
in the last {{ current_service.get_days_of_retention(contact_list.template_type) }}
|
|
||||||
days.
|
|
||||||
</p>
|
|
||||||
<div class='dashboard-table ajax-block-container'>
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
jobs,
|
|
||||||
caption="Messages sent from this contact list",
|
|
||||||
caption_visible=False,
|
|
||||||
empty_message='',
|
|
||||||
field_headings=[
|
|
||||||
'Template',
|
|
||||||
'Status'
|
|
||||||
],
|
|
||||||
field_headings_visible=False
|
|
||||||
) %}
|
|
||||||
{% call row_heading() %}
|
|
||||||
<div class="file-list">
|
|
||||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.template_name }}</a>
|
|
||||||
{% if item.scheduled %}
|
|
||||||
<span class="file-list-hint-large">
|
|
||||||
Sending {{
|
|
||||||
item.scheduled_for|format_datetime_relative
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="file-list-hint-large">
|
|
||||||
Sent {{
|
|
||||||
(item.scheduled_for or item.created_at)|format_datetime_relative
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endcall %}
|
|
||||||
{% call field() %}
|
|
||||||
{% if item.scheduled %}
|
|
||||||
{{ big_number(
|
|
||||||
item.notification_count,
|
|
||||||
smallest=True,
|
|
||||||
label=item.notification_count|message_count_label(
|
|
||||||
item.template_type,
|
|
||||||
suffix='waiting to send'
|
|
||||||
)
|
|
||||||
) }}
|
|
||||||
{% else %}
|
|
||||||
<div class="govuk-grid-row">
|
|
||||||
<div class="govuk-grid-column-one-third">
|
|
||||||
{{ big_number(
|
|
||||||
item.notifications_sending,
|
|
||||||
smallest=True,
|
|
||||||
label='sending',
|
|
||||||
) }}
|
|
||||||
</div>
|
|
||||||
<div class="govuk-grid-column-one-third">
|
|
||||||
{{ big_number(item.notifications_delivered, smallest=True, label='delivered') }}
|
|
||||||
</div>
|
|
||||||
<div class="govuk-grid-column-one-third">
|
|
||||||
{{ big_number(item.notifications_failed, smallest=True, label='failed') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="govuk-body">
|
|
||||||
{% if contact_list.has_jobs %}
|
|
||||||
Not used in the last {{ current_service.get_days_of_retention(contact_list.template_type) }} days.
|
|
||||||
{% else %}
|
|
||||||
Not used yet.
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2 class="govuk-heading-m govuk-!-margin-bottom-2">
|
|
||||||
{{ contact_list.recipients|length|format_thousands }} saved {{ contact_list.recipients|length|recipient_count_label(contact_list.template_type) }}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{% set recipient_column = contact_list.recipients.column_headers[0] %}
|
|
||||||
|
|
||||||
<div class="body-copy-table">
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
contact_list.recipients.displayed_rows,
|
|
||||||
caption=contact_list.recipients|length|recipient_count_label(contact_list.template_type)|capitalize,
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=[recipient_column],
|
|
||||||
field_headings_visible=False,
|
|
||||||
) %}
|
|
||||||
{% if not loop.first %}
|
|
||||||
{{ text_field(item[recipient_column].data) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if contact_list.recipients.displayed_rows|list|length < contact_list.recipients|length %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing the first {{ contact_list.recipients.displayed_rows|list|length }} rows
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="js-stick-at-bottom-when-scrolling">
|
|
||||||
<div class="page-footer">
|
|
||||||
{% if not confirm_delete_banner %}
|
|
||||||
<span class="page-footer-link page-footer-delete-link-without-button">
|
|
||||||
<a class="govuk-link govuk-link--destructive" href="{{ url_for('main.delete_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Delete this contact list</a>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
<a class="govuk-link govuk-link--no-visited-state page-footer-right-aligned-link-without-button" download href="{{ url_for('main.download_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Download this contact list (<abbr title="Comma separated values">CSV</abbr>)</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/radios.html" import radio_select %}
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
|
||||||
{% from "components/page-header.html" import page_header %}
|
|
||||||
{% from "components/uk_components/button/macro.njk" import govukButton %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
{{ original_file_name }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
{{ page_header(original_file_name) }}
|
|
||||||
|
|
||||||
<p class="govuk-body">
|
|
||||||
{{ recipients|length|recipient_count(recipients.template_type) }} found
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="bottom-gutter-3-2">
|
|
||||||
<form method="post" enctype="multipart/form-data" action="{{ url_for('main.save_contact_list', service_id=current_service.id, upload_id=upload_id) }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
{{ govukButton({ "text": "Save contact list" }) }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="govuk-heading-m">
|
|
||||||
File preview
|
|
||||||
</h2>
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
recipients.displayed_rows,
|
|
||||||
caption=original_file_name,
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=[
|
|
||||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
|
||||||
] + recipients.column_headers
|
|
||||||
) %}
|
|
||||||
{{ index_field(item.index + 2) }}
|
|
||||||
{% for column in recipients.column_headers %}
|
|
||||||
{% if item[column].ignore %}
|
|
||||||
{{ text_field(item[column].data or '', status='default') }}
|
|
||||||
{% else %}
|
|
||||||
{{ text_field(item[column].data or '') }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if item[None].data %}
|
|
||||||
{% for column in item[None].data %}
|
|
||||||
{{ text_field(column, status='default') }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/radios.html" import radio_select %}
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
|
||||||
{% from "components/file-upload.html" import file_upload %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
Error
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
<div class="bottom-gutter-1-2">
|
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
|
||||||
{% if row_errors|length == 1 %}
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
|
|
||||||
There’s a problem with {{ original_file_name }}
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
You need to {{ row_errors[0] }}.
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
|
|
||||||
There are some problems with {{ original_file_name }}
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
You need to:
|
|
||||||
</p>
|
|
||||||
<ul class="list-bullet">
|
|
||||||
{% for error in row_errors %}
|
|
||||||
<li>{{ error }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="js-stick-at-top-when-scrolling">
|
|
||||||
<div class="form-group">
|
|
||||||
{{ file_upload(
|
|
||||||
form.file,
|
|
||||||
allowed_file_extensions=allowed_file_extensions,
|
|
||||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
|
||||||
button_text='Upload your file again'
|
|
||||||
) }}
|
|
||||||
</div>
|
|
||||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
recipients.displayed_rows,
|
|
||||||
caption=original_file_name,
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=[
|
|
||||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true" class="table-field-invisible-error">1</span>'|safe
|
|
||||||
] + recipients.column_headers
|
|
||||||
) %}
|
|
||||||
{% call index_field() %}
|
|
||||||
<span class="{% if item.has_errors %}table-field-error{% endif %}">
|
|
||||||
{{ item.index + 2 }}
|
|
||||||
</span>
|
|
||||||
{% endcall %}
|
|
||||||
{% for column in recipients.column_headers %}
|
|
||||||
{% if item[column].error and not recipients.missing_column_headers %}
|
|
||||||
{% call field() %}
|
|
||||||
<span>
|
|
||||||
<span class="table-field-error-label">{{ item[column].error }}</span>
|
|
||||||
{{ item[column].data if item[column].data != None }}
|
|
||||||
</span>
|
|
||||||
{% endcall %}
|
|
||||||
{% elif item[column].ignore %}
|
|
||||||
{{ text_field(item[column].data or '', status='default') }}
|
|
||||||
{% else %}
|
|
||||||
{{ text_field(item[column].data or '') }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if item[None].data %}
|
|
||||||
{% for column in item[None].data %}
|
|
||||||
{{ text_field(column, status='default') }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
|
|
||||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
|
||||||
{% if recipients.displayed_rows|list|length < recipients.rows_with_errors|list|length %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows with errors
|
|
||||||
</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing rows with errors
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/radios.html" import radio_select %}
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
|
||||||
{% from "components/file-upload.html" import file_upload %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
Error
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
|
|
||||||
<div class="bottom-gutter-1-2">
|
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
|
||||||
|
|
||||||
{% if not recipients|length %}
|
|
||||||
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="No rows" data-error-label="{{ upload_id }}">
|
|
||||||
Your file is missing some rows
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
It needs at least one row of data
|
|
||||||
{%- if template_type %}.{% else %}, in a column called ‘email address’ or ‘phone number’.{% endif %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% elif recipients.column_headers|length == 1 %}
|
|
||||||
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
|
||||||
Your file needs a column called ‘email address’ or ‘phone number’.
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
Right now it has 1 column called ‘{{ recipients._raw_column_headers[0] }}’.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
|
|
||||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
|
||||||
Your file has too many columns
|
|
||||||
</h1>
|
|
||||||
<p class="govuk-body">
|
|
||||||
It needs to have 1 column, called ‘email address’ or ‘phone number’.
|
|
||||||
</p>
|
|
||||||
<p class="govuk-body">
|
|
||||||
Right now it has {{ recipients._raw_column_headers|length }} columns called {{ recipients._raw_column_headers | formatted_list }}.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="js-stick-at-top-when-scrolling">
|
|
||||||
<div class="form-group">
|
|
||||||
{{ file_upload(
|
|
||||||
form.file,
|
|
||||||
allowed_file_extensions=allowed_file_extensions,
|
|
||||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
|
||||||
button_text='Upload your file again'
|
|
||||||
) }}
|
|
||||||
</div>
|
|
||||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% set column_headers = recipients._raw_column_headers if recipients.duplicate_recipient_column_headers else recipients.column_headers %}
|
|
||||||
|
|
||||||
<h2 class="heading-medium" id="file-preview">{{ original_file_name }}</h2>
|
|
||||||
|
|
||||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
recipients.displayed_rows,
|
|
||||||
caption=original_file_name,
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=[
|
|
||||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
|
||||||
] + recipients._raw_column_headers
|
|
||||||
) %}
|
|
||||||
{{ index_field(item.index + 2) }}
|
|
||||||
{% for column in column_headers %}
|
|
||||||
{% if item[column].ignore %}
|
|
||||||
{{ text_field(item[column].data or '', status='default') }}
|
|
||||||
{% else %}
|
|
||||||
{{ text_field(item[column].data or '') }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if item[None].data %}
|
|
||||||
{% for column in item[None].data %}
|
|
||||||
{{ text_field(column, status='default') }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
|
||||||
<p class="table-show-more-link">
|
|
||||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
{% extends "withnav_template.html" %}
|
|
||||||
{% from "components/banner.html" import banner_wrapper %}
|
|
||||||
{% from "components/file-upload.html" import file_upload %}
|
|
||||||
{% from "components/page-header.html" import page_header %}
|
|
||||||
{% from "components/table.html" import list_table, text_field, index_field, index_field_heading %}
|
|
||||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
|
||||||
|
|
||||||
{% block service_page_title %}
|
|
||||||
Upload an emergency contact list
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block backLink %}
|
|
||||||
{% if not error %}
|
|
||||||
{{ govukBackLink({ "href": url_for('main.uploads', service_id=current_service.id) }) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
|
||||||
{% if error %}
|
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
|
||||||
<h1 class="banner-title">{{ error.title }}</h1>
|
|
||||||
{% if error.detail %}
|
|
||||||
<p class="govuk-body">{{ error.detail | safe }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endcall %}
|
|
||||||
{% else %}
|
|
||||||
{{ page_header('Upload an emergency contact list') }}
|
|
||||||
<p class="govuk-body">
|
|
||||||
Save a list of staff email addresses or phone numbers in Notify.
|
|
||||||
</p>
|
|
||||||
<p class="govuk-body">
|
|
||||||
In an emergency, you can send a message to everyone on the list.
|
|
||||||
</p>
|
|
||||||
<p class="govuk-body">
|
|
||||||
Do not include contact details for members of the public.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="bottom-gutter">
|
|
||||||
{{ file_upload(
|
|
||||||
form.file,
|
|
||||||
allowed_file_extensions=allowed_file_extensions,
|
|
||||||
button_text='Choose file',
|
|
||||||
show_errors=False,
|
|
||||||
)}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="heading-medium">Your file needs to look like one of these examples</h2>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
Save your file as a
|
|
||||||
<acronym title="Comma Separated Values">CSV</acronym>,
|
|
||||||
<acronym title="Tab Separated Values">TSV</acronym>,
|
|
||||||
<acronym title="Open Document Spreadsheet">ODS</acronym>,
|
|
||||||
or Microsoft Excel spreadsheet
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="govuk-grid-row">
|
|
||||||
<div class="govuk-grid-column-one-half">
|
|
||||||
<div class="spreadsheet">
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
[
|
|
||||||
['email address'],
|
|
||||||
['test@example.gsa.gov'],
|
|
||||||
],
|
|
||||||
caption="Example",
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=['', 'A']
|
|
||||||
) %}
|
|
||||||
{{ index_field(row_number - 1) }}
|
|
||||||
{% for column in item %}
|
|
||||||
{{ text_field(column) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="govuk-grid-column-one-half">
|
|
||||||
<div class="spreadsheet">
|
|
||||||
{% call(item, row_number) list_table(
|
|
||||||
[
|
|
||||||
['phone number'],
|
|
||||||
['555-867-5309'],
|
|
||||||
],
|
|
||||||
caption="Example",
|
|
||||||
caption_visible=False,
|
|
||||||
field_headings=['', 'A']
|
|
||||||
) %}
|
|
||||||
{{ index_field(row_number - 1) }}
|
|
||||||
{% for column in item %}
|
|
||||||
{{ text_field(column) }}
|
|
||||||
{% endfor %}
|
|
||||||
{% endcall %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
3
deploy-config/egress_proxy/notify-admin-demo.deploy.acl
Normal file
3
deploy-config/egress_proxy/notify-admin-demo.deploy.acl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Update this file to force a re-deploy of the egress proxy even when notify-admin-demo.<allow|deny>.acl haven't changed
|
||||||
|
|
||||||
|
20230412: Redeploy to re-calculate the list of allowed s3 buckets
|
||||||
@@ -1 +1,3 @@
|
|||||||
Update this file to force a re-deploy of the egress proxy even when notify-admin-staging.<allow|deny>.acl haven't changed
|
Update this file to force a re-deploy of the egress proxy even when notify-admin-staging.<allow|deny>.acl haven't changed
|
||||||
|
|
||||||
|
20230412: Redeploy to re-calculate the list of allowed s3 buckets
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
### The Public Benefits Studio
|
# The Public Benefits Studio
|
||||||
|
|
||||||
The Public Benefits Studio is a team inside of GSA’s Technology
|
The Public Benefits Studio is a team inside of GSA’s Technology
|
||||||
Transformation Services (TTS), home to innovative programs like 18F and
|
Transformation Services (TTS), home to innovative programs like 18F and
|
||||||
@@ -11,7 +11,7 @@ We’re a cross-functional team of technologists with specialized
|
|||||||
experience working across public benefits programs like Medicaid, SNAP,
|
experience working across public benefits programs like Medicaid, SNAP,
|
||||||
and unemployment insurance.
|
and unemployment insurance.
|
||||||
|
|
||||||
### WHAT WE’RE CURRENTLY EXPLORING
|
## WHAT WE’RE CURRENTLY EXPLORING
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -34,7 +34,7 @@ agency money.</td>
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
OUR FIRST BET: U.S. Notify
|
### OUR FIRST BET: U.S. Notify
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -63,7 +63,7 @@ send thousands of customized-to-the-user text messages per year at
|
|||||||
little-to-no-cost. The easy interface requires no technical expertise to
|
little-to-no-cost. The easy interface requires no technical expertise to
|
||||||
use and the setup process takes only ten minutes.
|
use and the setup process takes only ten minutes.
|
||||||
|
|
||||||
### WHERE WE’RE AT
|
## WHERE WE’RE AT
|
||||||
|
|
||||||
**We’re in the early stages of assessing this product’s market fit and
|
**We’re in the early stages of assessing this product’s market fit and
|
||||||
targeting to pilot** this shared service with at least 3 partners in
|
targeting to pilot** this shared service with at least 3 partners in
|
||||||
@@ -79,7 +79,7 @@ additional features based on partner needs.
|
|||||||
| Message send/failure analytics | Application status page | Multilingual interface and content library options |
|
| Message send/failure analytics | Application status page | Multilingual interface and content library options |
|
||||||
| 1-day records deletion | Scheduled send option | Recurring scheduled send |
|
| 1-day records deletion | Scheduled send option | Recurring scheduled send |
|
||||||
|
|
||||||
### OPPORTUNITIES TO GET INVOLVED
|
## OPPORTUNITIES TO GET INVOLVED
|
||||||
|
|
||||||
To get involved, email us at [notify-support@gsa.gov](mailto:notify-support@gsa.gov) with the following in the subject line!
|
To get involved, email us at [notify-support@gsa.gov](mailto:notify-support@gsa.gov) with the following in the subject line!
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ To get involved, email us at [notify-support@gsa.gov](mailto:notify-support@gsa.
|
|||||||
Early adopters will have wrap-around set-up support from the Studio
|
Early adopters will have wrap-around set-up support from the Studio
|
||||||
and an opportunity to shape the future of this product.
|
and an opportunity to shape the future of this product.
|
||||||
|
|
||||||
### US Notify Demo
|
## US Notify Demo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
docs/sprint-goals.md
Normal file
10
docs/sprint-goals.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Notify Sprint Goals Log
|
||||||
|
|
||||||
|
## Sprint: Heron (4/13/23)
|
||||||
|
|
||||||
|
| | Goal | Impact |
|
||||||
|
|-------------|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
|
||||||
|
| Engineering | Tackle data retention and message delivery receipts | Expand user capabilities while remaining LATO compliant. Give users ability to easily view failed messages. |
|
||||||
|
| UX | Audit & create a plan for implementing some “low-hanging fruit” aspects of USWDS | Understand similarities between UK Design and USWDS. Scope lift and path to allow team to schedule migration. |
|
||||||
|
| Content | Audit current content and recommend short-term content changes | Eliminate unnecessary/confusing content and add clarity to improve UX for pilot partners. |
|
||||||
|
| Security | Complete tasks to get assessment and begin assessment | Complete LATO docs to move us closer to LATO award and thus, piloting in earnest. |
|
||||||
@@ -14,7 +14,6 @@ applications:
|
|||||||
services:
|
services:
|
||||||
- notify-admin-redis-((env))
|
- notify-admin-redis-((env))
|
||||||
- notify-api-csv-upload-bucket-((env))
|
- notify-api-csv-upload-bucket-((env))
|
||||||
- notify-api-contact-list-bucket-((env))
|
|
||||||
- notify-admin-logo-upload-bucket-((env))
|
- notify-admin-logo-upload-bucket-((env))
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -32,15 +32,6 @@ resource "cloudfoundry_service_key" "csv_key" {
|
|||||||
service_instance = data.cloudfoundry_service_instance.csv_bucket.id
|
service_instance = data.cloudfoundry_service_instance.csv_bucket.id
|
||||||
}
|
}
|
||||||
|
|
||||||
data "cloudfoundry_service_instance" "contact_list_bucket" {
|
|
||||||
name_or_id = "${var.username}-contact-list-bucket"
|
|
||||||
space = data.cloudfoundry_space.dev.id
|
|
||||||
}
|
|
||||||
resource "cloudfoundry_service_key" "contact_list_key" {
|
|
||||||
name = local.key_name
|
|
||||||
service_instance = data.cloudfoundry_service_instance.contact_list_bucket.id
|
|
||||||
}
|
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
credentials = <<EOM
|
credentials = <<EOM
|
||||||
|
|
||||||
@@ -50,11 +41,6 @@ CSV_BUCKET_NAME=${cloudfoundry_service_key.csv_key.credentials.bucket}
|
|||||||
CSV_AWS_ACCESS_KEY_ID=${cloudfoundry_service_key.csv_key.credentials.access_key_id}
|
CSV_AWS_ACCESS_KEY_ID=${cloudfoundry_service_key.csv_key.credentials.access_key_id}
|
||||||
CSV_AWS_SECRET_ACCESS_KEY=${cloudfoundry_service_key.csv_key.credentials.secret_access_key}
|
CSV_AWS_SECRET_ACCESS_KEY=${cloudfoundry_service_key.csv_key.credentials.secret_access_key}
|
||||||
CSV_AWS_REGION=${cloudfoundry_service_key.csv_key.credentials.region}
|
CSV_AWS_REGION=${cloudfoundry_service_key.csv_key.credentials.region}
|
||||||
# CONTACT_LIST_BUCKET
|
|
||||||
CONTACT_BUCKET_NAME=${cloudfoundry_service_key.contact_list_key.credentials.bucket}
|
|
||||||
CONTACT_AWS_ACCESS_KEY_ID=${cloudfoundry_service_key.contact_list_key.credentials.access_key_id}
|
|
||||||
CONTACT_AWS_SECRET_ACCESS_KEY=${cloudfoundry_service_key.contact_list_key.credentials.secret_access_key}
|
|
||||||
CONTACT_AWS_REGION=${cloudfoundry_service_key.contact_list_key.credentials.region}
|
|
||||||
# LOGO_UPLOAD_BUCKET
|
# LOGO_UPLOAD_BUCKET
|
||||||
LOGO_BUCKET_NAME=${cloudfoundry_service_key.logo_key.credentials.bucket}
|
LOGO_BUCKET_NAME=${cloudfoundry_service_key.logo_key.credentials.bucket}
|
||||||
LOGO_AWS_ACCESS_KEY_ID=${cloudfoundry_service_key.logo_key.credentials.access_key_id}
|
LOGO_AWS_ACCESS_KEY_ID=${cloudfoundry_service_key.logo_key.credentials.access_key_id}
|
||||||
|
|||||||
@@ -670,28 +670,3 @@ def assert_url_expected(actual, expected):
|
|||||||
|
|
||||||
def find_element_by_tag_and_partial_text(page, tag, string):
|
def find_element_by_tag_and_partial_text(page, tag, string):
|
||||||
return [e for e in page.find_all(tag) if string in e.text][0]
|
return [e for e in page.find_all(tag) if string in e.text][0]
|
||||||
|
|
||||||
|
|
||||||
def contact_list_json(
|
|
||||||
*,
|
|
||||||
id_=None,
|
|
||||||
created_at='2020-06-13T09:59:56.000000Z',
|
|
||||||
created_by='Test User',
|
|
||||||
service_id,
|
|
||||||
original_file_name='EmergencyContactList.xls',
|
|
||||||
row_count=100,
|
|
||||||
recent_job_count=0,
|
|
||||||
has_jobs=True,
|
|
||||||
template_type='email',
|
|
||||||
):
|
|
||||||
return {
|
|
||||||
'id': id_ or sample_uuid(),
|
|
||||||
'created_at': created_at,
|
|
||||||
'created_by': created_by,
|
|
||||||
'service_id': service_id,
|
|
||||||
'original_file_name': original_file_name,
|
|
||||||
'row_count': row_count,
|
|
||||||
'recent_job_count': recent_job_count,
|
|
||||||
'has_jobs': has_jobs,
|
|
||||||
'template_type': template_type,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ from io import BytesIO
|
|||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from os import path
|
from os import path
|
||||||
from random import randbytes
|
from random import randbytes
|
||||||
from unittest.mock import ANY
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from zipfile import BadZipFile
|
from zipfile import BadZipFile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from freezegun import freeze_time
|
|
||||||
from notifications_python_client.errors import HTTPError
|
from notifications_python_client.errors import HTTPError
|
||||||
from notifications_utils.recipients import RecipientCSV
|
from notifications_utils.recipients import RecipientCSV
|
||||||
from notifications_utils.template import SMSPreviewTemplate
|
from notifications_utils.template import SMSPreviewTemplate
|
||||||
@@ -1034,7 +1032,6 @@ def test_send_one_off_step_redirects_to_start_if_session_not_setup(
|
|||||||
mock_get_service_statistics,
|
mock_get_service_statistics,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
template_type,
|
template_type,
|
||||||
):
|
):
|
||||||
@@ -1093,7 +1090,6 @@ def test_send_one_off_has_correct_page_title(
|
|||||||
client_request,
|
client_request,
|
||||||
service_one,
|
service_one,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
mocker,
|
mocker,
|
||||||
user,
|
user,
|
||||||
@@ -1135,7 +1131,6 @@ def test_send_one_off_shows_placeholders_in_correct_order(
|
|||||||
client_request,
|
client_request,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
mock_get_service_template_with_multiple_placeholders,
|
mock_get_service_template_with_multiple_placeholders,
|
||||||
step_index,
|
step_index,
|
||||||
prefilled,
|
prefilled,
|
||||||
@@ -1180,7 +1175,6 @@ def test_send_one_off_has_skip_link(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
mock_get_service_email_template,
|
mock_get_service_email_template,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
mocker,
|
mocker,
|
||||||
template_type,
|
template_type,
|
||||||
expected_link_text,
|
expected_link_text,
|
||||||
@@ -1221,7 +1215,6 @@ def test_send_one_off_has_sticky_header_for_email(
|
|||||||
client_request,
|
client_request,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
template_type,
|
template_type,
|
||||||
expected_sticky,
|
expected_sticky,
|
||||||
):
|
):
|
||||||
@@ -1249,7 +1242,6 @@ def test_skip_link_will_not_show_on_sms_one_off_if_service_has_no_mobile_number(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
mock_get_service_template,
|
mock_get_service_template,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
mocker,
|
mocker,
|
||||||
user,
|
user,
|
||||||
):
|
):
|
||||||
@@ -1279,7 +1271,6 @@ def test_send_one_off_offers_link_to_upload(
|
|||||||
fake_uuid,
|
fake_uuid,
|
||||||
mock_get_service_template,
|
mock_get_service_template,
|
||||||
mock_has_jobs,
|
mock_has_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
user,
|
user,
|
||||||
):
|
):
|
||||||
client_request.login(user)
|
client_request.login(user)
|
||||||
@@ -1307,7 +1298,6 @@ def test_send_one_off_has_link_to_use_existing_list(
|
|||||||
client_request,
|
client_request,
|
||||||
mock_get_service_template,
|
mock_get_service_template,
|
||||||
mock_has_jobs,
|
mock_has_jobs,
|
||||||
mock_get_contact_lists,
|
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
):
|
):
|
||||||
page = client_request.get(
|
page = client_request.get(
|
||||||
@@ -1328,14 +1318,6 @@ def test_send_one_off_has_link_to_use_existing_list(
|
|||||||
template_id=fake_uuid,
|
template_id=fake_uuid,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
|
||||||
'Use an emergency list',
|
|
||||||
url_for(
|
|
||||||
'main.choose_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
'Use my phone number',
|
'Use my phone number',
|
||||||
url_for(
|
url_for(
|
||||||
@@ -1347,33 +1329,6 @@ def test_send_one_off_has_link_to_use_existing_list(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_no_link_to_use_existing_list_for_service_without_lists(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_service_template,
|
|
||||||
mock_has_jobs,
|
|
||||||
platform_admin_user,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.ContactLists.client_method',
|
|
||||||
return_value=[],
|
|
||||||
)
|
|
||||||
client_request.login(platform_admin_user)
|
|
||||||
page = client_request.get(
|
|
||||||
'main.send_one_off',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
_follow_redirects=True,
|
|
||||||
)
|
|
||||||
assert [
|
|
||||||
link.text for link in page.select('form a')
|
|
||||||
] == [
|
|
||||||
'Upload a list of phone numbers',
|
|
||||||
'Use my phone number',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('user', (
|
@pytest.mark.parametrize('user', (
|
||||||
create_active_user_with_permissions(),
|
create_active_user_with_permissions(),
|
||||||
create_active_caseworking_user(),
|
create_active_caseworking_user(),
|
||||||
@@ -1590,7 +1545,6 @@ def test_send_one_off_step_0_back_link(
|
|||||||
mock_get_service,
|
mock_get_service,
|
||||||
mock_get_service_template_with_placeholders,
|
mock_get_service_template_with_placeholders,
|
||||||
mock_has_no_jobs,
|
mock_has_no_jobs,
|
||||||
mock_get_contact_lists,
|
|
||||||
permissions,
|
permissions,
|
||||||
expected_back_link_endpoint,
|
expected_back_link_endpoint,
|
||||||
extra_args,
|
extra_args,
|
||||||
@@ -1671,7 +1625,6 @@ def test_send_one_off_sms_message_puts_submitted_data_in_session(
|
|||||||
mock_get_service_template_with_placeholders,
|
mock_get_service_template_with_placeholders,
|
||||||
mock_get_users_by_service,
|
mock_get_users_by_service,
|
||||||
mock_get_service_statistics,
|
mock_get_service_statistics,
|
||||||
mock_get_contact_lists,
|
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
):
|
):
|
||||||
with client_request.session_transaction() as session:
|
with client_request.session_transaction() as session:
|
||||||
@@ -1847,32 +1800,6 @@ def test_upload_csvfile_with_international_validates(
|
|||||||
assert mock_recipients.call_args[1]['allow_international_sms'] == should_allow_international
|
assert mock_recipients.call_args[1]['allow_international_sms'] == should_allow_international
|
||||||
|
|
||||||
|
|
||||||
def test_job_from_contact_list_knows_where_its_come_from(
|
|
||||||
client_request,
|
|
||||||
mocker,
|
|
||||||
service_one,
|
|
||||||
mock_get_service_template,
|
|
||||||
mock_s3_download,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
mock_get_service_statistics,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_s3_get_metadata,
|
|
||||||
mock_s3_set_metadata,
|
|
||||||
fake_uuid
|
|
||||||
):
|
|
||||||
page = client_request.get(
|
|
||||||
'main.check_messages',
|
|
||||||
service_id=service_one['id'],
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
contact_list_id=unchanging_fake_uuid,
|
|
||||||
)
|
|
||||||
assert page.select_one(
|
|
||||||
'form input[type=hidden][name=contact_list_id]'
|
|
||||||
)['value'] == str(unchanging_fake_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
def test_test_message_can_only_be_sent_now(
|
def test_test_message_can_only_be_sent_now(
|
||||||
client_request,
|
client_request,
|
||||||
mocker,
|
mocker,
|
||||||
@@ -1932,9 +1859,6 @@ def test_send_button_is_correctly_labelled(
|
|||||||
@pytest.mark.parametrize('when', [
|
@pytest.mark.parametrize('when', [
|
||||||
'', '2016-08-25T13:04:21.767198'
|
'', '2016-08-25T13:04:21.767198'
|
||||||
])
|
])
|
||||||
@pytest.mark.parametrize('contact_list_id', [
|
|
||||||
'', unchanging_fake_uuid,
|
|
||||||
])
|
|
||||||
def test_create_job_should_call_api(
|
def test_create_job_should_call_api(
|
||||||
client_request,
|
client_request,
|
||||||
mock_create_job,
|
mock_create_job,
|
||||||
@@ -1945,7 +1869,6 @@ def test_create_job_should_call_api(
|
|||||||
mocker,
|
mocker,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
when,
|
when,
|
||||||
contact_list_id,
|
|
||||||
):
|
):
|
||||||
data = mock_get_job(SERVICE_ONE_ID, fake_uuid)['data']
|
data = mock_get_job(SERVICE_ONE_ID, fake_uuid)['data']
|
||||||
job_id = data['id']
|
job_id = data['id']
|
||||||
@@ -1968,7 +1891,6 @@ def test_create_job_should_call_api(
|
|||||||
original_file_name=original_file_name,
|
original_file_name=original_file_name,
|
||||||
_data={
|
_data={
|
||||||
'scheduled_for': when,
|
'scheduled_for': when,
|
||||||
'contact_list_id': contact_list_id,
|
|
||||||
},
|
},
|
||||||
_follow_redirects=True,
|
_follow_redirects=True,
|
||||||
_expected_status=200,
|
_expected_status=200,
|
||||||
@@ -1980,7 +1902,6 @@ def test_create_job_should_call_api(
|
|||||||
job_id,
|
job_id,
|
||||||
SERVICE_ONE_ID,
|
SERVICE_ONE_ID,
|
||||||
scheduled_for=when,
|
scheduled_for=when,
|
||||||
contact_list_id=str(contact_list_id),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2724,7 +2645,6 @@ def test_reply_to_is_previewed_if_chosen(
|
|||||||
mock_get_service_statistics,
|
mock_get_service_statistics,
|
||||||
mock_get_job_doesnt_exist,
|
mock_get_job_doesnt_exist,
|
||||||
mock_get_jobs,
|
mock_get_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
get_default_reply_to_email_address,
|
get_default_reply_to_email_address,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -2777,7 +2697,6 @@ def test_sms_sender_is_previewed(
|
|||||||
mock_get_service_statistics,
|
mock_get_service_statistics,
|
||||||
mock_get_job_doesnt_exist,
|
mock_get_job_doesnt_exist,
|
||||||
mock_get_jobs,
|
mock_get_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
get_default_sms_sender,
|
get_default_sms_sender,
|
||||||
fake_uuid,
|
fake_uuid,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -2838,163 +2757,6 @@ def test_redirects_to_template_if_job_exists_already(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize((
|
|
||||||
'template_type, '
|
|
||||||
'expected_list_id, '
|
|
||||||
'expected_filenames, '
|
|
||||||
'expected_time, '
|
|
||||||
'expected_count'
|
|
||||||
), (
|
|
||||||
(
|
|
||||||
'email',
|
|
||||||
'6ce466d0-fd6a-11e5-82f5-e0accb9d11a6',
|
|
||||||
['EmergencyContactList.xls'],
|
|
||||||
'Uploaded today at 5:59am',
|
|
||||||
'100 email addresses',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'sms',
|
|
||||||
'd7b0bd1a-d1c7-4621-be5c-3c1b4278a2ad',
|
|
||||||
['phone number list.csv', 'UnusedList.tsv'],
|
|
||||||
'Uploaded today at 8:00am',
|
|
||||||
'123 phone numbers',
|
|
||||||
),
|
|
||||||
))
|
|
||||||
@freeze_time('2020-06-13 13:00')
|
|
||||||
def test_choose_from_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_contact_lists,
|
|
||||||
fake_uuid,
|
|
||||||
template_type,
|
|
||||||
expected_list_id,
|
|
||||||
expected_filenames,
|
|
||||||
expected_time,
|
|
||||||
expected_count,
|
|
||||||
):
|
|
||||||
template = create_template(template_type=template_type)
|
|
||||||
mocker.patch(
|
|
||||||
'app.service_api_client.get_service_template',
|
|
||||||
return_value={'data': template},
|
|
||||||
)
|
|
||||||
page = client_request.get(
|
|
||||||
'main.choose_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert [
|
|
||||||
normalize_spaces(filename.text)
|
|
||||||
for filename in page.select('.file-list-filename-large')
|
|
||||||
] == expected_filenames
|
|
||||||
|
|
||||||
assert page.select_one('a.file-list-filename-large')['href'] == url_for(
|
|
||||||
'main.send_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=template['id'],
|
|
||||||
contact_list_id=expected_list_id,
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('.file-list-hint-large').text) == (
|
|
||||||
expected_time
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('.big-number-smallest').text) == (
|
|
||||||
expected_count
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_choose_from_contact_list_with_personalised_template(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_contact_lists,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
template = create_template(
|
|
||||||
content="Hey ((name)) ((thing)) is happening"
|
|
||||||
)
|
|
||||||
mocker.patch(
|
|
||||||
'app.service_api_client.get_service_template',
|
|
||||||
return_value={'data': template},
|
|
||||||
)
|
|
||||||
page = client_request.get(
|
|
||||||
'main.choose_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert [
|
|
||||||
normalize_spaces(p.text) for p in page.select('main p')
|
|
||||||
] == [
|
|
||||||
'You cannot use an emergency contact list with this template because '
|
|
||||||
'it is personalized with ((name)) and ((thing)).',
|
|
||||||
'Emergency contact lists can only include email addresses or phone numbers.',
|
|
||||||
]
|
|
||||||
assert not page.select('table')
|
|
||||||
|
|
||||||
|
|
||||||
def test_choose_from_contact_list_with_no_lists(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_service_template,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.ContactLists.client_method',
|
|
||||||
return_value=[],
|
|
||||||
)
|
|
||||||
page = client_request.get(
|
|
||||||
'main.choose_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert [
|
|
||||||
normalize_spaces(p.text) for p in page.select('main p')
|
|
||||||
] == [
|
|
||||||
'You have not saved any lists of phone numbers yet.',
|
|
||||||
'To upload and save an emergency contact list, go to the uploads page.',
|
|
||||||
]
|
|
||||||
assert page.select_one('main p a')['href'] == url_for(
|
|
||||||
'main.uploads',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
assert not page.select('table')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Need to figure out how to handle cross-bucket copies.")
|
|
||||||
def test_send_from_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_get_contact_list,
|
|
||||||
):
|
|
||||||
new_uuid = uuid.uuid4()
|
|
||||||
mock_download = mocker.patch('app.models.contact_list.get_s3_contents', return_value='contents')
|
|
||||||
mock_get_metadata = mocker.patch('app.models.contact_list.get_s3_metadata', return_value={
|
|
||||||
'example_key': 'example value',
|
|
||||||
})
|
|
||||||
mock_upload = mocker.patch('app.models.contact_list.s3upload', return_value=new_uuid)
|
|
||||||
mock_set_metadata = mocker.patch('app.models.contact_list.set_metadata_on_csv_upload')
|
|
||||||
client_request.get(
|
|
||||||
'main.send_from_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
_expected_status=302,
|
|
||||||
_expected_redirect=url_for(
|
|
||||||
'main.check_messages',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
template_id=fake_uuid,
|
|
||||||
upload_id=new_uuid,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mock_download.assert_called_once()
|
|
||||||
mock_get_metadata.assert_called_once()
|
|
||||||
mock_upload.assert_called_once_with(
|
|
||||||
SERVICE_ONE_ID, {'data': 'contents'}, ANY
|
|
||||||
)
|
|
||||||
mock_set_metadata.assert_called_once_with(
|
|
||||||
SERVICE_ONE_ID, new_uuid, example_key='example value'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_send_to_myself_sets_placeholder_and_redirects_for_email(
|
def test_send_to_myself_sets_placeholder_and_redirects_for_email(
|
||||||
mocker, client_request, fake_uuid, mock_get_service_email_template
|
mocker, client_request, fake_uuid, mock_get_service_email_template
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,714 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from io import BytesIO
|
|
||||||
from unittest.mock import ANY
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from flask import url_for
|
|
||||||
from freezegun import freeze_time
|
|
||||||
|
|
||||||
from app.formatters import normalize_spaces
|
|
||||||
from tests import contact_list_json
|
|
||||||
from tests.conftest import SERVICE_ONE_ID
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_contact_list_page(client_request):
|
|
||||||
page = client_request.get(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
assert 'action' not in page.select_one('form')
|
|
||||||
assert page.select_one('form input')['name'] == 'file'
|
|
||||||
assert page.select_one('form input')['type'] == 'file'
|
|
||||||
assert page.select_one('form input')['accept'] == '.csv,.xlsx,.xls,.ods,.xlsm,.tsv'
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select('.spreadsheet')[0].text) == (
|
|
||||||
'Example A '
|
|
||||||
'1 email address '
|
|
||||||
'2 test@example.gsa.gov'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select('.spreadsheet')[1].text) == (
|
|
||||||
'Example A '
|
|
||||||
'1 phone number '
|
|
||||||
'2 555-867-5309'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('file_contents, expected_error, expected_thead, expected_tbody,', [
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
telephone,name
|
|
||||||
+12028675109
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'Your file has too many columns '
|
|
||||||
'It needs to have 1 column, called ‘email address’ or ‘phone number’. '
|
|
||||||
'Right now it has 2 columns called ‘telephone’ and ‘name’.'
|
|
||||||
),
|
|
||||||
'Row in file 1 telephone name',
|
|
||||||
'2 +12028675109',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number, email address
|
|
||||||
+12028675109, test@example.com
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'Your file has too many columns '
|
|
||||||
'It needs to have 1 column, called ‘email address’ or ‘phone number’. '
|
|
||||||
'Right now it has 2 columns called ‘phone number’ and ‘email address’.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number email address',
|
|
||||||
'2 +12028675109 test@example.com',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
email address
|
|
||||||
+12028675109
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'There’s a problem with invalid.csv '
|
|
||||||
'You need to fix 1 email address.'
|
|
||||||
),
|
|
||||||
'Row in file 1 email address',
|
|
||||||
'2 Not a valid email address +12028675109',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number
|
|
||||||
test@example.com
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'There’s a problem with invalid.csv '
|
|
||||||
'You need to fix 1 phone number.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number',
|
|
||||||
'2 The string supplied did not seem to be a phone number. test@example.com',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number, phone number, PHONE_NUMBER
|
|
||||||
+12027900111,+12027900222,+12027900333,
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'Your file has too many columns '
|
|
||||||
'It needs to have 1 column, called ‘email address’ or ‘phone number’. '
|
|
||||||
'Right now it has 3 columns called ‘phone number’, ‘phone number’ and ‘PHONE_NUMBER’.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number phone number PHONE_NUMBER',
|
|
||||||
'2 +12027900333 +12027900333 +12027900333',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'Your file is missing some rows '
|
|
||||||
'It needs at least one row of data.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number',
|
|
||||||
'',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"+12028675109",
|
|
||||||
(
|
|
||||||
'Your file is missing some rows '
|
|
||||||
'It needs at least one row of data, in a column called '
|
|
||||||
'‘email address’ or ‘phone number’.'
|
|
||||||
),
|
|
||||||
'Row in file 1 +12028675109',
|
|
||||||
'',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"",
|
|
||||||
(
|
|
||||||
'Your file is missing some rows '
|
|
||||||
'It needs at least one row of data, in a column called '
|
|
||||||
'‘email address’ or ‘phone number’.'
|
|
||||||
),
|
|
||||||
'Row in file 1',
|
|
||||||
'',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number
|
|
||||||
+12028675109
|
|
||||||
|
|
||||||
+12028675109
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'There’s a problem with invalid.csv '
|
|
||||||
'You need to enter missing data in 1 row.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number',
|
|
||||||
(
|
|
||||||
'3 Missing'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
phone number
|
|
||||||
+12027900
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'There’s a problem with invalid.csv '
|
|
||||||
'You need to fix 1 phone number.'
|
|
||||||
),
|
|
||||||
'Row in file 1 phone number',
|
|
||||||
'2 Not enough digits +12027900',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"""
|
|
||||||
email address
|
|
||||||
ok@example.com
|
|
||||||
bad@example1
|
|
||||||
bad@example2
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
'There’s a problem with invalid.csv '
|
|
||||||
'You need to fix 2 email addresses.'
|
|
||||||
),
|
|
||||||
'Row in file 1 email address',
|
|
||||||
(
|
|
||||||
'3 Not a valid email address bad@example1 '
|
|
||||||
'4 Not a valid email address bad@example2'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
def test_upload_csv_file_shows_error_banner(
|
|
||||||
client_request,
|
|
||||||
mocker,
|
|
||||||
notify_admin,
|
|
||||||
mock_s3_upload,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
fake_uuid,
|
|
||||||
file_contents,
|
|
||||||
expected_error,
|
|
||||||
expected_thead,
|
|
||||||
expected_tbody,
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.uuid4', return_value=fake_uuid)
|
|
||||||
mock_upload = mocker.patch('app.models.contact_list.utils_s3upload')
|
|
||||||
mock_download = mocker.patch(
|
|
||||||
'app.models.contact_list.get_s3_contents',
|
|
||||||
return_value=file_contents,
|
|
||||||
)
|
|
||||||
mock_set_metadata = mocker.patch('app.models.contact_list.set_s3_metadata')
|
|
||||||
mock_get_metadata = mocker.patch(
|
|
||||||
'app.models.contact_list.get_s3_metadata',
|
|
||||||
return_value={'original_file_name': 'invalid.csv'},
|
|
||||||
)
|
|
||||||
|
|
||||||
page = client_request.post(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
_data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')},
|
|
||||||
_follow_redirects=True,
|
|
||||||
)
|
|
||||||
bucket_creds = notify_admin.config['CONTACT_LIST_BUCKET']
|
|
||||||
mock_upload.assert_called_once_with(
|
|
||||||
filedata='',
|
|
||||||
region=bucket_creds['region'],
|
|
||||||
bucket_name=bucket_creds['bucket'],
|
|
||||||
file_location=f"service-{SERVICE_ONE_ID}-notify/{fake_uuid}.csv",
|
|
||||||
access_key=bucket_creds['access_key_id'],
|
|
||||||
secret_key=bucket_creds['secret_access_key'],
|
|
||||||
)
|
|
||||||
mock_set_metadata.assert_called_once_with(
|
|
||||||
ANY,
|
|
||||||
original_file_name='invalid.csv'
|
|
||||||
)
|
|
||||||
mock_download.assert_called_once()
|
|
||||||
mock_get_metadata.assert_called_once()
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('.banner-dangerous').text) == expected_error
|
|
||||||
|
|
||||||
assert page.select_one('form')['action'] == url_for(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
assert page.select_one('form input')['type'] == 'file'
|
|
||||||
assert page.select_one('form input')['accept'] == '.csv,.xlsx,.xls,.ods,.xlsm,.tsv'
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('thead').text) == expected_thead
|
|
||||||
assert normalize_spaces(page.select_one('tbody').text) == expected_tbody
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_csv_file_shows_error_banner_for_too_many_rows(
|
|
||||||
client_request,
|
|
||||||
mocker,
|
|
||||||
mock_s3_upload,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.utils_s3upload', return_value=fake_uuid)
|
|
||||||
mocker.patch('app.models.contact_list.set_s3_metadata')
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value='\n'.join(
|
|
||||||
['phone number'] + (['2028675309'] * 100_001)
|
|
||||||
))
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_metadata',
|
|
||||||
return_value={'original_file_name': 'invalid.csv'})
|
|
||||||
|
|
||||||
page = client_request.post(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
_data={'file': (BytesIO(''.encode('utf-8')), 'invalid.csv')},
|
|
||||||
_follow_redirects=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('.banner-dangerous').text) == (
|
|
||||||
'Your file has too many rows '
|
|
||||||
'Notify can store files up to 100,000 rows in size. '
|
|
||||||
'Your file has 100,001 rows.'
|
|
||||||
)
|
|
||||||
assert len(page.select('tbody tr')) == 50
|
|
||||||
assert normalize_spaces(page.select_one('.table-show-more-link').text) == (
|
|
||||||
'Only showing the first 50 rows'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_csv_shows_error_with_invalid_extension(
|
|
||||||
client_request,
|
|
||||||
):
|
|
||||||
page = client_request.post(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
_data={'file': (BytesIO(''.encode('utf-8')), 'invalid.txt')},
|
|
||||||
_follow_redirects=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('.banner-dangerous').text) == (
|
|
||||||
"invalid.txt is not a spreadsheet that Notify can read"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_csv_file_sanitises_and_truncates_file_name_in_metadata(
|
|
||||||
client_request,
|
|
||||||
mocker,
|
|
||||||
mock_s3_upload,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.utils_s3upload', return_value=fake_uuid)
|
|
||||||
mock_set_metadata = mocker.patch('app.models.contact_list.set_s3_metadata')
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value='\n'.join(
|
|
||||||
['phone number'] + (['2028675309'] * 100_001)
|
|
||||||
))
|
|
||||||
|
|
||||||
filename = f"😁{'a' * 2000}.csv"
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_metadata',
|
|
||||||
return_value={'original_file_name': filename})
|
|
||||||
client_request.post(
|
|
||||||
'main.upload_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
_data={'file': (BytesIO(''.encode('utf-8')), filename)},
|
|
||||||
_follow_redirects=False
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(
|
|
||||||
mock_set_metadata.call_args_list[0][1]['original_file_name']
|
|
||||||
) < len(filename)
|
|
||||||
|
|
||||||
assert mock_set_metadata.call_args_list[0][1]['original_file_name'].startswith('?')
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_csv_shows_trial_mode_error(
|
|
||||||
client_request,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
fake_uuid,
|
|
||||||
mocker
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.utils_s3upload', return_value=fake_uuid)
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value=(
|
|
||||||
'phone number\n'
|
|
||||||
'2028675209' # Not in team
|
|
||||||
))
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_metadata',
|
|
||||||
return_value={'original_file_name': 'invalid.csv'})
|
|
||||||
|
|
||||||
page = client_request.get(
|
|
||||||
'main.check_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
_test_page_title=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('.banner-dangerous').text) == (
|
|
||||||
'You cannot save this phone number '
|
|
||||||
'In trial mode you can only send to yourself and members of your team'
|
|
||||||
)
|
|
||||||
assert page.select_one('.banner-dangerous a')['href'] == url_for(
|
|
||||||
'main.trial_mode_new'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_upload_csv_shows_ok_page(
|
|
||||||
client_request,
|
|
||||||
mock_get_live_service,
|
|
||||||
mock_get_users_by_service,
|
|
||||||
mock_get_job_doesnt_exist,
|
|
||||||
fake_uuid,
|
|
||||||
mocker
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value='\n'.join(
|
|
||||||
['email address'] + ['test@example.com'] * 51
|
|
||||||
))
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_metadata',
|
|
||||||
return_value={'original_file_name': 'good times.xlsx'})
|
|
||||||
mock_metadata_set = mocker.patch('app.models.contact_list.set_s3_metadata')
|
|
||||||
|
|
||||||
page = client_request.get(
|
|
||||||
'main.check_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
_test_page_title=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_metadata_set.assert_called_once_with(
|
|
||||||
mocker.ANY,
|
|
||||||
row_count=51,
|
|
||||||
original_file_name='good times.xlsx',
|
|
||||||
template_type='email',
|
|
||||||
valid=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('h1').text) == (
|
|
||||||
'good times.xlsx'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('main p').text) == (
|
|
||||||
'51 email addresses found'
|
|
||||||
)
|
|
||||||
assert page.select_one('form')['action'] == url_for(
|
|
||||||
'main.save_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert normalize_spaces(page.select_one('form [type=submit]').text) == (
|
|
||||||
'Save contact list'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('thead').text) == (
|
|
||||||
'Row in file 1 email address'
|
|
||||||
)
|
|
||||||
assert len(page.select('tbody tr')) == 50
|
|
||||||
assert normalize_spaces(page.select_one('tbody tr').text) == (
|
|
||||||
'2 test@example.com'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('.table-show-more-link').text) == (
|
|
||||||
'Only showing the first 50 rows'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_create_contact_list,
|
|
||||||
):
|
|
||||||
mock_get_metadata = mocker.patch('app.models.contact_list.get_s3_metadata', return_value={
|
|
||||||
'row_count': 999,
|
|
||||||
'valid': True,
|
|
||||||
'original_file_name': 'example.csv',
|
|
||||||
'template_type': 'email'
|
|
||||||
})
|
|
||||||
client_request.post(
|
|
||||||
'main.save_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
_expected_status=302,
|
|
||||||
_expected_redirect=url_for(
|
|
||||||
'main.uploads',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mock_get_metadata.assert_called_once()
|
|
||||||
mock_create_contact_list.assert_called_once_with(
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
original_file_name='example.csv',
|
|
||||||
row_count=999,
|
|
||||||
template_type='email',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cant_save_bad_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_create_contact_list,
|
|
||||||
):
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_metadata', return_value={
|
|
||||||
'row_count': 999,
|
|
||||||
'valid': False,
|
|
||||||
'original_file_name': 'example.csv',
|
|
||||||
'template_type': 'email'
|
|
||||||
})
|
|
||||||
client_request.post(
|
|
||||||
'main.save_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
upload_id=fake_uuid,
|
|
||||||
_expected_status=403,
|
|
||||||
)
|
|
||||||
assert mock_create_contact_list.called is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('has_jobs, expected_empty_message', [
|
|
||||||
(False, 'Not used yet.'),
|
|
||||||
(True, 'Not used in the last 7 days.'),
|
|
||||||
])
|
|
||||||
@freeze_time('2020-06-13 16:51:56')
|
|
||||||
def test_view_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_contact_list,
|
|
||||||
mock_get_no_jobs,
|
|
||||||
mock_get_service_data_retention,
|
|
||||||
fake_uuid,
|
|
||||||
has_jobs,
|
|
||||||
expected_empty_message,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.contact_list_api_client.get_contact_list',
|
|
||||||
return_value=contact_list_json(
|
|
||||||
created_at='2020-03-03T17:12:12.000000Z',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
has_jobs=has_jobs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value='\n'.join(
|
|
||||||
['email address'] + [
|
|
||||||
f'test-{i}@example.com' for i in range(51)
|
|
||||||
]
|
|
||||||
))
|
|
||||||
page = client_request.get(
|
|
||||||
'main.contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
mock_get_no_jobs.assert_called_once_with(
|
|
||||||
SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
limit_days=7,
|
|
||||||
statuses={
|
|
||||||
'finished',
|
|
||||||
'in progress',
|
|
||||||
'pending',
|
|
||||||
'ready to send',
|
|
||||||
'scheduled',
|
|
||||||
'sending limits exceeded',
|
|
||||||
'sent to dvla',
|
|
||||||
},
|
|
||||||
page=1,
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('h1').text) == (
|
|
||||||
'EmergencyContactList.xls'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select('main p')[0].text) == (
|
|
||||||
'Uploaded by Test User on 3 March at 12:12pm.'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select('main p')[1].text) == (
|
|
||||||
expected_empty_message
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('main h2').text) == (
|
|
||||||
'51 saved email addresses'
|
|
||||||
)
|
|
||||||
assert page.select_one('.js-stick-at-bottom-when-scrolling a[download]')['href'] == url_for(
|
|
||||||
'main.download_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert len(page.select('tbody tr')) == 50
|
|
||||||
assert [
|
|
||||||
normalize_spaces(page.select('tbody tr')[0].text),
|
|
||||||
normalize_spaces(page.select('tbody tr')[1].text),
|
|
||||||
|
|
||||||
normalize_spaces(page.select('tbody tr')[48].text),
|
|
||||||
normalize_spaces(page.select('tbody tr')[49].text),
|
|
||||||
] == [
|
|
||||||
'test-0@example.com',
|
|
||||||
'test-1@example.com',
|
|
||||||
|
|
||||||
'test-48@example.com',
|
|
||||||
'test-49@example.com',
|
|
||||||
]
|
|
||||||
assert 'test-50@example.com' not in page.select_one('tbody').text
|
|
||||||
assert normalize_spaces(page.select_one('.table-show-more-link').text) == (
|
|
||||||
'Only showing the first 50 rows'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@freeze_time('2015-12-31 21:51:56')
|
|
||||||
def test_view_jobs_for_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_get_service_data_retention,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.contact_list_api_client.get_contact_list',
|
|
||||||
return_value={
|
|
||||||
'created_at': '2015-12-31 17:12:12',
|
|
||||||
'created_by': 'Test User',
|
|
||||||
'id': fake_uuid,
|
|
||||||
'original_file_name': 'EmergencyContactList.xls',
|
|
||||||
'row_count': 100,
|
|
||||||
'recent_job_count': 0,
|
|
||||||
'has_jobs': True,
|
|
||||||
'service_id': SERVICE_ONE_ID,
|
|
||||||
'template_type': 'email',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
mocker.patch('app.models.contact_list.get_s3_contents', return_value='\n'.join(
|
|
||||||
['email address'] + ['test@example.com'] * 51
|
|
||||||
))
|
|
||||||
page = client_request.get(
|
|
||||||
'main.contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('h1').text) == (
|
|
||||||
'EmergencyContactList.xls'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select('main p')[0].text) == (
|
|
||||||
'Uploaded by Test User today at 12:12pm.'
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select('main p')[1].text) == (
|
|
||||||
'Used 6 times in the last 7 days.'
|
|
||||||
)
|
|
||||||
assert [
|
|
||||||
normalize_spaces(row.text)
|
|
||||||
for row in page.select_one('table').select('tr')
|
|
||||||
] == [
|
|
||||||
'Template Status',
|
|
||||||
(
|
|
||||||
'Template Y '
|
|
||||||
'Sending tomorrow at 6:09pm '
|
|
||||||
'1 text message waiting to send'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Template Z '
|
|
||||||
'Sending tomorrow at 6:09am '
|
|
||||||
'1 text message waiting to send'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Template A '
|
|
||||||
'Sent today at 4:51pm '
|
|
||||||
'1 sending 0 delivered 0 failed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Template B '
|
|
||||||
'Sent today at 4:51pm '
|
|
||||||
'1 sending 0 delivered 0 failed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Template C '
|
|
||||||
'Sent today at 4:51pm '
|
|
||||||
'1 sending 0 delivered 0 failed'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Template D '
|
|
||||||
'Sent today at 4:51pm '
|
|
||||||
'1 sending 0 delivered 0 failed'
|
|
||||||
),
|
|
||||||
]
|
|
||||||
assert page.select_one('table a')['href'] == url_for(
|
|
||||||
'main.view_job',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
job_id=fake_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_view_contact_list_404s_for_non_existing_list(
|
|
||||||
client_request,
|
|
||||||
mock_get_no_contact_list,
|
|
||||||
fake_uuid,
|
|
||||||
):
|
|
||||||
client_request.get(
|
|
||||||
'main.contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=uuid.uuid4(),
|
|
||||||
_expected_status=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_get_contact_list,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.get_s3_contents',
|
|
||||||
return_value='phone number\n2028675209'
|
|
||||||
)
|
|
||||||
response = client_request.get_response(
|
|
||||||
'main.download_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert response.headers['Content-Type'] == (
|
|
||||||
'text/csv; '
|
|
||||||
'charset=utf-8'
|
|
||||||
)
|
|
||||||
assert response.headers['Content-Disposition'] == (
|
|
||||||
'attachment; '
|
|
||||||
'filename=EmergencyContactList.csv'
|
|
||||||
)
|
|
||||||
assert response.get_data(as_text=True) == (
|
|
||||||
'phone number\n'
|
|
||||||
'2028675209'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_confirm_delete_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_get_service_data_retention,
|
|
||||||
mock_get_contact_list,
|
|
||||||
):
|
|
||||||
mocker.patch(
|
|
||||||
'app.models.contact_list.get_s3_contents',
|
|
||||||
return_value='phone number\n2028675209'
|
|
||||||
)
|
|
||||||
page = client_request.get(
|
|
||||||
'main.delete_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
assert normalize_spaces(page.select_one('.banner-dangerous').text) == (
|
|
||||||
'Are you sure you want to delete ‘EmergencyContactList.xls’? '
|
|
||||||
'Yes, delete'
|
|
||||||
)
|
|
||||||
assert 'action' not in page.select_one('form')
|
|
||||||
assert page.select_one('form')['method'] == 'post'
|
|
||||||
assert page.select_one('form button')['type'] == 'submit'
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_contact_list(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
fake_uuid,
|
|
||||||
mock_get_contact_list,
|
|
||||||
):
|
|
||||||
mock_delete = mocker.patch(
|
|
||||||
'app.models.contact_list.contact_list_api_client.delete_contact_list'
|
|
||||||
)
|
|
||||||
client_request.post(
|
|
||||||
'main.delete_contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
_expected_redirect=url_for(
|
|
||||||
'main.uploads',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mock_delete.assert_called_once_with(
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id=fake_uuid,
|
|
||||||
)
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import url_for
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from app.formatters import normalize_spaces
|
from app.formatters import normalize_spaces
|
||||||
@@ -9,31 +6,9 @@ from tests.conftest import (
|
|||||||
SERVICE_ONE_ID,
|
SERVICE_ONE_ID,
|
||||||
create_active_caseworking_user,
|
create_active_caseworking_user,
|
||||||
create_active_user_with_permissions,
|
create_active_user_with_permissions,
|
||||||
create_platform_admin_user,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Not sure that TTS needs this")
|
|
||||||
@pytest.mark.parametrize('user', (
|
|
||||||
create_platform_admin_user(),
|
|
||||||
create_active_user_with_permissions(),
|
|
||||||
))
|
|
||||||
def test_all_users_have_upload_contact_list(
|
|
||||||
client_request,
|
|
||||||
mock_get_uploads,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_get_no_contact_lists,
|
|
||||||
user,
|
|
||||||
):
|
|
||||||
client_request.login(user)
|
|
||||||
page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID)
|
|
||||||
button = page.find('a', text=re.compile('Upload an emergency contact list'))
|
|
||||||
assert button
|
|
||||||
assert button['href'] == url_for(
|
|
||||||
'main.upload_contact_list', service_id=SERVICE_ONE_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('extra_permissions, expected_empty_message', (
|
@pytest.mark.parametrize('extra_permissions, expected_empty_message', (
|
||||||
([], (
|
([], (
|
||||||
'You have not uploaded any files recently.'
|
'You have not uploaded any files recently.'
|
||||||
@@ -44,7 +19,6 @@ def test_get_upload_hub_with_no_uploads(
|
|||||||
client_request,
|
client_request,
|
||||||
service_one,
|
service_one,
|
||||||
mock_get_no_uploads,
|
mock_get_no_uploads,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
extra_permissions,
|
extra_permissions,
|
||||||
expected_empty_message,
|
expected_empty_message,
|
||||||
):
|
):
|
||||||
@@ -63,7 +37,6 @@ def test_get_upload_hub_page(
|
|||||||
client_request,
|
client_request,
|
||||||
service_one,
|
service_one,
|
||||||
mock_get_uploads,
|
mock_get_uploads,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
):
|
):
|
||||||
mocker.patch('app.job_api_client.get_jobs', return_value={'data': []})
|
mocker.patch('app.job_api_client.get_jobs', return_value={'data': []})
|
||||||
page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID)
|
page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID)
|
||||||
@@ -93,7 +66,6 @@ def test_uploads_page_shows_scheduled_jobs(
|
|||||||
client_request,
|
client_request,
|
||||||
mock_get_no_uploads,
|
mock_get_no_uploads,
|
||||||
mock_get_jobs,
|
mock_get_jobs,
|
||||||
mock_get_no_contact_lists,
|
|
||||||
user,
|
user,
|
||||||
):
|
):
|
||||||
client_request.login(user)
|
client_request.login(user)
|
||||||
@@ -117,53 +89,3 @@ def test_uploads_page_shows_scheduled_jobs(
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
assert not page.select('.table-empty-message')
|
assert not page.select('.table-empty-message')
|
||||||
|
|
||||||
|
|
||||||
@freeze_time('2020-03-15')
|
|
||||||
def test_uploads_page_shows_contact_lists_first(
|
|
||||||
mocker,
|
|
||||||
client_request,
|
|
||||||
mock_get_no_uploads,
|
|
||||||
mock_get_jobs,
|
|
||||||
mock_get_contact_lists,
|
|
||||||
mock_get_service_data_retention,
|
|
||||||
):
|
|
||||||
page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID)
|
|
||||||
|
|
||||||
assert [
|
|
||||||
normalize_spaces(row.text) for row in page.select('tr')
|
|
||||||
] == [
|
|
||||||
(
|
|
||||||
'File Status'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'phone number list.csv '
|
|
||||||
'Used twice in the last 7 days '
|
|
||||||
'123 saved phone numbers'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'EmergencyContactList.xls '
|
|
||||||
'Not used in the last 7 days '
|
|
||||||
'100 saved email addresses'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'UnusedList.tsv '
|
|
||||||
'Not used yet '
|
|
||||||
'1 saved phone number'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'even_later.csv '
|
|
||||||
'Sending 1 January 2016 at 6:09pm '
|
|
||||||
'1 text message waiting to send'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'send_me_later.csv '
|
|
||||||
'Sending 1 January 2016 at 6:09am '
|
|
||||||
'1 text message waiting to send'
|
|
||||||
),
|
|
||||||
]
|
|
||||||
assert page.select_one('.file-list-filename-large')['href'] == url_for(
|
|
||||||
'main.contact_list',
|
|
||||||
service_id=SERVICE_ONE_ID,
|
|
||||||
contact_list_id='d7b0bd1a-d1c7-4621-be5c-3c1b4278a2ad',
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
from app.models.contact_list import ContactList
|
|
||||||
from app.models.job import PaginatedJobs
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_jobs(mock_get_jobs):
|
|
||||||
contact_list = ContactList({'id': 'a', 'service_id': 'b'})
|
|
||||||
assert isinstance(contact_list.get_jobs(page=123), PaginatedJobs)
|
|
||||||
# mock_get_jobs mocks the underlying API client method, not
|
|
||||||
# contact_list.get_jobs
|
|
||||||
mock_get_jobs.assert_called_once_with(
|
|
||||||
'b',
|
|
||||||
contact_list_id='a',
|
|
||||||
statuses={
|
|
||||||
'finished',
|
|
||||||
'sending limits exceeded',
|
|
||||||
'ready to send',
|
|
||||||
'scheduled',
|
|
||||||
'sent to dvla',
|
|
||||||
'pending',
|
|
||||||
'in progress',
|
|
||||||
},
|
|
||||||
page=123,
|
|
||||||
limit_days=None,
|
|
||||||
)
|
|
||||||
@@ -50,21 +50,6 @@ def test_client_schedules_job(mocker, fake_uuid):
|
|||||||
assert mock_post.call_args[1]['data']['scheduled_for'] == when
|
assert mock_post.call_args[1]['data']['scheduled_for'] == when
|
||||||
|
|
||||||
|
|
||||||
def test_client_links_job_to_contact_list(mocker, fake_uuid):
|
|
||||||
|
|
||||||
mocker.patch('app.notify_client.current_user', id='1')
|
|
||||||
|
|
||||||
contact_list_id = uuid.uuid4()
|
|
||||||
|
|
||||||
mock_post = mocker.patch('app.notify_client.job_api_client.JobApiClient.post')
|
|
||||||
|
|
||||||
JobApiClient().create_job(
|
|
||||||
fake_uuid, 1, contact_list_id=contact_list_id
|
|
||||||
)
|
|
||||||
|
|
||||||
assert mock_post.call_args[1]['data']['contact_list_id'] == contact_list_id
|
|
||||||
|
|
||||||
|
|
||||||
def test_client_gets_job_by_service_and_job(mocker):
|
def test_client_gets_job_by_service_and_job(mocker):
|
||||||
service_id = 'service_id'
|
service_id = 'service_id'
|
||||||
job_id = 'job_id'
|
job_id = 'job_id'
|
||||||
|
|||||||
@@ -37,12 +37,10 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
|||||||
'change_user_auth',
|
'change_user_auth',
|
||||||
'check_and_resend_text_code',
|
'check_and_resend_text_code',
|
||||||
'check_and_resend_verification_code',
|
'check_and_resend_verification_code',
|
||||||
'check_contact_list',
|
|
||||||
'check_messages',
|
'check_messages',
|
||||||
'check_notification',
|
'check_notification',
|
||||||
'check_tour_notification',
|
'check_tour_notification',
|
||||||
'choose_account',
|
'choose_account',
|
||||||
'choose_from_contact_list',
|
|
||||||
'choose_service',
|
'choose_service',
|
||||||
'choose_template',
|
'choose_template',
|
||||||
'choose_template_to_copy',
|
'choose_template_to_copy',
|
||||||
@@ -50,7 +48,6 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
|||||||
'confirm_edit_user_email',
|
'confirm_edit_user_email',
|
||||||
'confirm_edit_user_mobile_number',
|
'confirm_edit_user_mobile_number',
|
||||||
'confirm_redact_template',
|
'confirm_redact_template',
|
||||||
'contact_list',
|
|
||||||
'conversation',
|
'conversation',
|
||||||
'conversation_reply',
|
'conversation_reply',
|
||||||
'conversation_reply_with_template',
|
'conversation_reply_with_template',
|
||||||
@@ -62,14 +59,12 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
|||||||
'create_api_key',
|
'create_api_key',
|
||||||
'create_email_branding',
|
'create_email_branding',
|
||||||
'data_retention',
|
'data_retention',
|
||||||
'delete_contact_list',
|
|
||||||
'delete_service_template',
|
'delete_service_template',
|
||||||
'delete_template_folder',
|
'delete_template_folder',
|
||||||
'delivery_and_failure',
|
'delivery_and_failure',
|
||||||
'delivery_status_callback',
|
'delivery_status_callback',
|
||||||
'design_content',
|
'design_content',
|
||||||
'documentation',
|
'documentation',
|
||||||
'download_contact_list',
|
|
||||||
'download_notifications_csv',
|
'download_notifications_csv',
|
||||||
'download_organisation_usage_report',
|
'download_organisation_usage_report',
|
||||||
'edit_and_format_messages',
|
'edit_and_format_messages',
|
||||||
@@ -178,12 +173,10 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
|||||||
'revalidate_email_sent',
|
'revalidate_email_sent',
|
||||||
'revoke_api_key',
|
'revoke_api_key',
|
||||||
'roadmap',
|
'roadmap',
|
||||||
'save_contact_list',
|
|
||||||
'security',
|
'security',
|
||||||
'security_policy',
|
'security_policy',
|
||||||
'send_files_by_email',
|
'send_files_by_email',
|
||||||
'send_files_by_email_contact_details',
|
'send_files_by_email_contact_details',
|
||||||
'send_from_contact_list',
|
|
||||||
'send_messages',
|
'send_messages',
|
||||||
'send_notification',
|
'send_notification',
|
||||||
'send_one_off',
|
'send_one_off',
|
||||||
@@ -250,7 +243,6 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
|
|||||||
'two_factor_email_sent',
|
'two_factor_email_sent',
|
||||||
'two_factor_webauthn',
|
'two_factor_webauthn',
|
||||||
'update_email_branding',
|
'update_email_branding',
|
||||||
'upload_contact_list',
|
|
||||||
'uploads',
|
'uploads',
|
||||||
'usage',
|
'usage',
|
||||||
'user_information',
|
'user_information',
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from . import (
|
|||||||
TestClient,
|
TestClient,
|
||||||
api_key_json,
|
api_key_json,
|
||||||
assert_url_expected,
|
assert_url_expected,
|
||||||
contact_list_json,
|
|
||||||
generate_uuid,
|
generate_uuid,
|
||||||
inbound_sms_json,
|
inbound_sms_json,
|
||||||
invite_json,
|
invite_json,
|
||||||
@@ -1275,7 +1274,7 @@ def mock_check_verify_code_code_expired(mocker):
|
|||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_create_job(mocker, api_user_active):
|
def mock_create_job(mocker, api_user_active):
|
||||||
def _create(job_id, service_id, scheduled_for=None, contact_list_id=None):
|
def _create(job_id, service_id, scheduled_for=None):
|
||||||
return job_json(
|
return job_json(
|
||||||
service_id,
|
service_id,
|
||||||
api_user_active,
|
api_user_active,
|
||||||
@@ -1367,7 +1366,7 @@ def mock_has_no_jobs(mocker):
|
|||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_get_jobs(mocker, api_user_active, fake_uuid):
|
def mock_get_jobs(mocker, api_user_active, fake_uuid):
|
||||||
def _get_jobs(service_id, limit_days=None, statuses=None, contact_list_id=None, page=1):
|
def _get_jobs(service_id, limit_days=None, statuses=None, page=1):
|
||||||
if statuses is None:
|
if statuses is None:
|
||||||
statuses = ['', 'scheduled', 'pending', 'cancelled', 'finished']
|
statuses = ['', 'scheduled', 'pending', 'cancelled', 'finished']
|
||||||
|
|
||||||
@@ -1462,98 +1461,6 @@ def mock_get_no_jobs(mocker, api_user_active):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_create_contact_list(mocker, api_user_active):
|
|
||||||
def _create(
|
|
||||||
service_id,
|
|
||||||
upload_id,
|
|
||||||
original_file_name,
|
|
||||||
row_count,
|
|
||||||
template_type,
|
|
||||||
):
|
|
||||||
return {
|
|
||||||
'service_id': service_id,
|
|
||||||
'upload_id': upload_id,
|
|
||||||
'original_file_name': original_file_name,
|
|
||||||
'row_count': row_count,
|
|
||||||
'template_type': template_type,
|
|
||||||
}
|
|
||||||
|
|
||||||
return mocker.patch(
|
|
||||||
'app.contact_list_api_client.create_contact_list',
|
|
||||||
side_effect=_create,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_get_contact_lists(mocker, api_user_active, fake_uuid):
|
|
||||||
def _get(service_id, template_type=None):
|
|
||||||
return [
|
|
||||||
contact_list_json(
|
|
||||||
id_=fake_uuid,
|
|
||||||
created_at='2020-06-13T09:59:56.000000Z',
|
|
||||||
service_id=service_id,
|
|
||||||
),
|
|
||||||
contact_list_json(
|
|
||||||
id_='d7b0bd1a-d1c7-4621-be5c-3c1b4278a2ad',
|
|
||||||
created_at='2020-06-13T12:00:00.000000Z',
|
|
||||||
service_id=service_id,
|
|
||||||
original_file_name='phone number list.csv',
|
|
||||||
row_count=123,
|
|
||||||
recent_job_count=2,
|
|
||||||
template_type='sms',
|
|
||||||
),
|
|
||||||
contact_list_json(
|
|
||||||
id_=fake_uuid,
|
|
||||||
created_at='2020-05-02T01:00:00.000000Z',
|
|
||||||
original_file_name='UnusedList.tsv',
|
|
||||||
row_count=1,
|
|
||||||
has_jobs=False,
|
|
||||||
service_id=service_id,
|
|
||||||
template_type='sms',
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
return mocker.patch(
|
|
||||||
'app.models.contact_list.ContactLists.client_method',
|
|
||||||
side_effect=_get,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_get_contact_list(mocker, api_user_active, fake_uuid):
|
|
||||||
def _get(*, service_id, contact_list_id):
|
|
||||||
return contact_list_json(
|
|
||||||
id_=fake_uuid,
|
|
||||||
created_at='2020-06-13T09:59:56.000000Z',
|
|
||||||
service_id=service_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return mocker.patch(
|
|
||||||
'app.models.contact_list.contact_list_api_client.get_contact_list',
|
|
||||||
side_effect=_get,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_get_no_contact_list(mocker, api_user_active, fake_uuid):
|
|
||||||
def _get(*, service_id, contact_list_id):
|
|
||||||
raise HTTPError(response=Mock(status_code=404))
|
|
||||||
|
|
||||||
return mocker.patch(
|
|
||||||
'app.models.contact_list.contact_list_api_client.get_contact_list',
|
|
||||||
side_effect=_get,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def mock_get_no_contact_lists(mocker):
|
|
||||||
return mocker.patch(
|
|
||||||
'app.models.contact_list.ContactLists.client_method',
|
|
||||||
return_value=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def mock_get_notifications(
|
def mock_get_notifications(
|
||||||
mocker,
|
mocker,
|
||||||
|
|||||||
Reference in New Issue
Block a user