merge from main

This commit is contained in:
Kenneth Kehl
2023-04-20 11:19:55 -07:00
35 changed files with 32 additions and 2447 deletions

View File

@@ -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,

View File

@@ -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')}")

View File

@@ -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 doesnt already exist, so the # The happy path is that the job doesnt 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)

View File

@@ -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,
)

View File

@@ -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(),
)

View File

@@ -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,

View File

@@ -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

View File

@@ -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',
}, },
} }

View File

@@ -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()

View File

@@ -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)

View File

@@ -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>

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 }}">
Theres 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
### The Public Benefits Studio # The Public Benefits Studio
The Public Benefits Studio is a team inside of GSAs Technology The Public Benefits Studio is a team inside of GSAs 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 @@ Were 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 WERE CURRENTLY EXPLORING ## WHAT WERE 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 WERE AT ## WHERE WERE AT
**Were in the early stages of assessing this products market fit and **Were in the early stages of assessing this products 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
View 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. |

View File

@@ -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:

View File

@@ -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}

View File

@@ -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,
}

View File

@@ -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
): ):

View File

@@ -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
""",
(
'Theres 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
""",
(
'Theres 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
""",
(
'Theres 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
""",
(
'Theres 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
""",
(
'Theres 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,
)

View File

@@ -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',
)

View File

@@ -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,
)

View File

@@ -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'

View File

@@ -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',

View File

@@ -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,