2020-03-12 16:13:18 +00:00
|
|
|
|
import itertools
|
2020-05-11 10:52:30 +01:00
|
|
|
|
from datetime import datetime
|
2020-03-16 13:47:10 +00:00
|
|
|
|
from io import BytesIO
|
2020-03-12 16:13:18 +00:00
|
|
|
|
from zipfile import BadZipFile
|
2019-08-09 08:44:11 +01:00
|
|
|
|
|
2022-12-05 15:33:44 -05:00
|
|
|
|
from flask import flash, redirect, render_template, request, send_file, url_for
|
2022-02-04 10:43:36 +00:00
|
|
|
|
from notifications_utils.insensitive_dict import InsensitiveDict
|
2020-03-12 16:13:18 +00:00
|
|
|
|
from notifications_utils.recipients import RecipientCSV
|
|
|
|
|
|
from notifications_utils.sanitise_text import SanitiseASCII
|
|
|
|
|
|
from xlrd.biffh import XLRDError
|
|
|
|
|
|
from xlrd.xldate import XLDateError
|
2019-09-06 10:39:23 +01:00
|
|
|
|
|
2022-12-05 15:33:44 -05:00
|
|
|
|
from app import current_service
|
2019-08-09 08:44:11 +01:00
|
|
|
|
from app.main import main
|
2022-12-05 15:33:44 -05:00
|
|
|
|
from app.main.forms import CsvUploadForm
|
2020-03-13 13:53:18 +00:00
|
|
|
|
from app.models.contact_list import ContactList
|
2021-06-14 11:00:05 +01:00
|
|
|
|
from app.utils import unicode_truncate
|
2021-06-09 15:15:35 +01:00
|
|
|
|
from app.utils.csv import Spreadsheet, get_errors_for_csv
|
2022-11-28 15:53:56 -05:00
|
|
|
|
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
2022-12-05 15:33:44 -05:00
|
|
|
|
from app.utils.templates import get_sample_template
|
2021-06-09 13:19:05 +01:00
|
|
|
|
from app.utils.user import user_has_permissions
|
2019-08-09 08:44:11 +01:00
|
|
|
|
|
2019-09-06 11:13:05 +01:00
|
|
|
|
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
|
|
|
|
|
|
2019-08-09 08:44:11 +01:00
|
|
|
|
|
2019-11-04 11:07:55 +00:00
|
|
|
|
@main.route("/services/<uuid:service_id>/uploads")
|
2019-10-09 10:56:08 +01:00
|
|
|
|
@user_has_permissions()
|
2019-08-09 08:44:11 +01:00
|
|
|
|
def uploads(service_id):
|
2019-12-05 16:14:30 +00:00
|
|
|
|
# No tests have been written, this has been quickly prepared for user research.
|
|
|
|
|
|
# It's also very like that a new view will be created to show uploads.
|
2020-01-08 14:29:56 +00:00
|
|
|
|
uploads = current_service.get_page_of_uploads(page=request.args.get('page'))
|
2019-12-05 16:14:30 +00:00
|
|
|
|
|
|
|
|
|
|
prev_page = None
|
2020-02-27 17:51:01 +00:00
|
|
|
|
if uploads.prev_page:
|
2020-01-08 14:29:56 +00:00
|
|
|
|
prev_page = generate_previous_dict('main.uploads', service_id, uploads.current_page)
|
2019-12-05 16:14:30 +00:00
|
|
|
|
next_page = None
|
2020-02-27 17:51:01 +00:00
|
|
|
|
if uploads.next_page:
|
2020-01-08 14:29:56 +00:00
|
|
|
|
next_page = generate_next_dict('main.uploads', service_id, uploads.current_page)
|
2019-12-05 16:14:30 +00:00
|
|
|
|
|
2020-02-27 10:49:23 +00:00
|
|
|
|
if uploads.current_page == 1:
|
2020-03-16 10:55:58 +00:00
|
|
|
|
listed_uploads = (
|
|
|
|
|
|
current_service.contact_lists +
|
|
|
|
|
|
current_service.scheduled_jobs +
|
|
|
|
|
|
uploads
|
|
|
|
|
|
)
|
2020-02-27 10:49:23 +00:00
|
|
|
|
else:
|
|
|
|
|
|
listed_uploads = uploads
|
|
|
|
|
|
|
2019-12-05 16:14:30 +00:00
|
|
|
|
return render_template(
|
|
|
|
|
|
'views/jobs/jobs.html',
|
2020-02-27 10:49:23 +00:00
|
|
|
|
jobs=listed_uploads,
|
2019-12-05 16:14:30 +00:00
|
|
|
|
prev_page=prev_page,
|
|
|
|
|
|
next_page=next_page,
|
2020-05-11 10:52:30 +01:00
|
|
|
|
now=datetime.utcnow().isoformat(),
|
2019-12-05 16:14:30 +00:00
|
|
|
|
)
|
2019-09-06 10:39:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
2020-03-13 15:39:51 +00:00
|
|
|
|
@main.route("/services/<uuid:service_id>/upload-contact-list", methods=['GET', 'POST'])
|
2020-03-12 16:13:18 +00:00
|
|
|
|
@user_has_permissions('send_messages')
|
|
|
|
|
|
def upload_contact_list(service_id):
|
|
|
|
|
|
form = CsvUploadForm()
|
|
|
|
|
|
|
|
|
|
|
|
if form.validate_on_submit():
|
|
|
|
|
|
try:
|
2020-03-13 13:53:18 +00:00
|
|
|
|
upload_id = ContactList.upload(
|
|
|
|
|
|
current_service.id,
|
|
|
|
|
|
Spreadsheet.from_file_form(form).as_dict,
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
2020-10-19 15:27:30 +01:00
|
|
|
|
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
|
|
|
|
|
|
)
|
2020-03-12 16:13:18 +00:00
|
|
|
|
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
|
|
|
|
|
|
))
|
2021-12-03 18:14:02 +00:00
|
|
|
|
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])
|
2020-03-12 16:13:18 +00:00
|
|
|
|
|
|
|
|
|
|
return render_template(
|
|
|
|
|
|
'views/uploads/contact-list/upload.html',
|
|
|
|
|
|
form=form,
|
2020-12-17 10:47:52 +00:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS,
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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()
|
|
|
|
|
|
|
2020-03-13 13:53:18 +00:00
|
|
|
|
contents = ContactList.download(service_id, upload_id)
|
2020-03-12 16:13:18 +00:00
|
|
|
|
first_row = contents.splitlines()[0].strip().rstrip(',') if contents else ''
|
2020-10-27 10:35:24 +00:00
|
|
|
|
original_file_name = ContactList.get_metadata(service_id, upload_id).get('original_file_name', '')
|
2020-03-12 16:13:18 +00:00
|
|
|
|
|
2022-02-04 10:43:36 +00:00
|
|
|
|
template_type = InsensitiveDict({
|
|
|
|
|
|
'email address': 'email',
|
|
|
|
|
|
'phone number': 'sms',
|
|
|
|
|
|
}).get(first_row)
|
2020-03-12 16:13:18 +00:00
|
|
|
|
|
|
|
|
|
|
recipients = RecipientCSV(
|
|
|
|
|
|
contents,
|
2020-04-19 22:46:13 +01:00
|
|
|
|
template=get_sample_template(template_type or 'sms'),
|
2021-12-10 16:35:40 +00:00
|
|
|
|
guestlist=itertools.chain.from_iterable(
|
2020-03-12 16:13:18 +00:00
|
|
|
|
[user.name, user.mobile_number, user.email_address]
|
|
|
|
|
|
for user in current_service.active_users
|
|
|
|
|
|
) if current_service.trial_mode else None,
|
2020-04-27 10:57:25 +01:00
|
|
|
|
allow_international_sms=current_service.has_permission('international_sms'),
|
2020-03-12 16:13:18 +00:00
|
|
|
|
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,
|
2020-12-17 10:47:52 +00:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2020-12-17 10:47:52 +00:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
2020-12-17 10:47:52 +00:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if recipients.has_errors:
|
|
|
|
|
|
return render_template(
|
|
|
|
|
|
'views/uploads/contact-list/column-errors.html',
|
|
|
|
|
|
recipients=recipients,
|
|
|
|
|
|
original_file_name=original_file_name,
|
|
|
|
|
|
form=form,
|
2020-12-17 10:47:52 +00:00
|
|
|
|
allowed_file_extensions=Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
2020-03-12 16:13:18 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
metadata_kwargs = {
|
|
|
|
|
|
'row_count': len(recipients),
|
|
|
|
|
|
'valid': True,
|
2020-10-27 10:35:24 +00:00
|
|
|
|
'original_file_name': original_file_name,
|
2020-03-12 16:13:18 +00:00
|
|
|
|
'template_type': template_type
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-03-13 13:53:18 +00:00
|
|
|
|
ContactList.set_metadata(service_id, upload_id, **metadata_kwargs)
|
2020-03-12 16:13:18 +00:00
|
|
|
|
|
|
|
|
|
|
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):
|
2020-03-13 13:53:18 +00:00
|
|
|
|
ContactList.create(current_service.id, upload_id)
|
2020-03-12 16:13:18 +00:00
|
|
|
|
return redirect(url_for(
|
2020-03-16 15:07:34 +00:00
|
|
|
|
'.uploads',
|
2020-03-12 16:13:18 +00:00
|
|
|
|
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):
|
2020-05-12 17:31:01 +01:00
|
|
|
|
contact_list = ContactList.from_id(contact_list_id, service_id=service_id)
|
2020-03-13 16:58:43 +00:00
|
|
|
|
return render_template(
|
|
|
|
|
|
'views/uploads/contact-list/contact-list.html',
|
2020-05-12 17:31:01 +01:00
|
|
|
|
contact_list=contact_list,
|
2020-12-01 13:34:31 +00:00
|
|
|
|
jobs=contact_list.get_jobs(
|
|
|
|
|
|
page=1,
|
|
|
|
|
|
limit_days=current_service.get_days_of_retention(contact_list.template_type),
|
|
|
|
|
|
),
|
2020-03-13 16:58:43 +00:00
|
|
|
|
)
|
2020-03-15 19:13:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
2020-03-18 13:37:39 +00:00
|
|
|
|
@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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-03-15 19:13:07 +00:00
|
|
|
|
@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(
|
2022-05-27 15:33:48 +01:00
|
|
|
|
path_or_file=BytesIO(contact_list.contents.encode('utf-8')),
|
2020-03-15 19:13:07 +00:00
|
|
|
|
attachment_filename=contact_list.saved_file_name,
|
|
|
|
|
|
as_attachment=True,
|
|
|
|
|
|
)
|