Merge remote-tracking branch 'origin' into install-uswds

This commit is contained in:
Jonathan Bobel
2023-04-24 15:50:49 -04:00
57 changed files with 107 additions and 2749 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.billing_api_client import billing_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.events_api_client import events_api_client
from app.notify_client.inbound_number_client import inbound_number_client
@@ -155,35 +154,27 @@ def _csp(config):
"'self'",
asset_domain
],
"frame-ancestors": "'none'",
"form-action": "'self'",
"script-src": [
"'self'",
"'unsafe-eval'",
asset_domain,
"*.google-analytics.com",
"'unsafe-eval'",
"https://js-agent.newrelic.com",
"https://*.nr-data.net",
"data:"
"https://gov-bam.nr-data.net",
],
"connect-src": [
"'self'",
"*.google-analytics.com",
"https://*.nr-data.net"
"https://gov-bam.nr-data.net"
],
"style-src": [
"'self'",
asset_domain
],
"font-src": [
"'self'",
asset_domain,
"data:"
],
"img-src": [
"'self'",
asset_domain,
logo_domain,
"*.google-analytics.com",
"data:"
logo_domain
]
}
@@ -216,7 +207,6 @@ def create_app(application):
# API clients
api_key_api_client,
billing_api_client,
contact_list_api_client,
complaint_api_client,
email_branding_client,
events_api_client,
@@ -247,6 +237,22 @@ def create_app(application):
application,
content_security_policy=_csp(application.config),
content_security_policy_nonce_in=['style-src', 'script-src'],
permissions_policy={
'accelerometer': '()',
'ambient-light-sensor': '()',
'autoplay': '()',
'battery': '()',
'camera': '()',
'document-domain': '()',
'geolocation': '()',
'gyroscope': '()',
'local-fonts': '()',
'magnetometer': '()',
'microphone': '()',
'midi': '()',
'payment': '()',
'screen-wake-lock': '()'
},
frame_options='deny',
force_https=(application.config['HTTP_PROTOCOL'] == 'https')
)

View File

@@ -41,14 +41,14 @@
<a href="/accounts-or-dashboard" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<svg role="presentation" focusable="false" class="govuk-header__logotype-crown"
<svg role="presentation" focusable="false" class="govuk-header__logotype"
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 132 97" height="32" width="36">
<path fill="currentColor" fill-rule="evenodd"
d="M25 30.2c3.5 1.5 7.7-.2 9.1-3.7 1.5-3.6-.2-7.8-3.9-9.2-3.6-1.4-7.6.3-9.1 3.9-1.4 3.5.3 7.5 3.9 9zM9 39.5c3.6 1.5 7.8-.2 9.2-3.7 1.5-3.6-.2-7.8-3.9-9.1-3.6-1.5-7.6.2-9.1 3.8-1.4 3.5.3 7.5 3.8 9zM4.4 57.2c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.5-1.5-7.6.3-9.1 3.8-1.4 3.5.3 7.6 3.9 9.1zm38.3-21.4c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.6-1.5-7.6.3-9.1 3.8-1.3 3.6.4 7.7 3.9 9.1zm64.4-5.6c-3.6 1.5-7.8-.2-9.1-3.7-1.5-3.6.2-7.8 3.8-9.2 3.6-1.4 7.7.3 9.2 3.9 1.3 3.5-.4 7.5-3.9 9zm15.9 9.3c-3.6 1.5-7.7-.2-9.1-3.7-1.5-3.6.2-7.8 3.7-9.1 3.6-1.5 7.7.2 9.2 3.8 1.5 3.5-.3 7.5-3.8 9zm4.7 17.7c-3.6 1.5-7.8-.2-9.2-3.8-1.5-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.3 3.5-.4 7.6-3.9 9.1zM89.3 35.8c-3.6 1.5-7.8-.2-9.2-3.8-1.4-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.4 3.6-.3 7.7-3.9 9.1zM69.7 17.7l8.9 4.7V9.3l-8.9 2.8c-.2-.3-.5-.6-.9-.9L72.4 0H59.6l3.5 11.2c-.3.3-.6.5-.9.9l-8.8-2.8v13.1l8.8-4.7c.3.3.6.7.9.9l-5 15.4v.1c-.2.8-.4 1.6-.4 2.4 0 4.1 3.1 7.5 7 8.1h.2c.3 0 .7.1 1 .1.4 0 .7 0 1-.1h.2c4-.6 7.1-4.1 7.1-8.1 0-.8-.1-1.7-.4-2.4V34l-5.1-15.4c.4-.2.7-.6 1-.9zM66 92.8c16.9 0 32.8 1.1 47.1 3.2 4-16.9 8.9-26.7 14-33.5l-9.6-3.4c1 4.9 1.1 7.2 0 10.2-1.5-1.4-3-4.3-4.2-8.7L108.6 76c2.8-2 5-3.2 7.5-3.3-4.4 9.4-10 11.9-13.6 11.2-4.3-.8-6.3-4.6-5.6-7.9 1-4.7 5.7-5.9 8-.5 4.3-8.7-3-11.4-7.6-8.8 7.1-7.2 7.9-13.5 2.1-21.1-8 6.1-8.1 12.3-4.5 20.8-4.7-5.4-12.1-2.5-9.5 6.2 3.4-5.2 7.9-2 7.2 3.1-.6 4.3-6.4 7.8-13.5 7.2-10.3-.9-10.9-8-11.2-13.8 2.5-.5 7.1 1.8 11 7.3L80.2 60c-4.1 4.4-8 5.3-12.3 5.4 1.4-4.4 8-11.6 8-11.6H55.5s6.4 7.2 7.9 11.6c-4.2-.1-8-1-12.3-5.4l1.4 16.4c3.9-5.5 8.5-7.7 10.9-7.3-.3 5.8-.9 12.8-11.1 13.8-7.2.6-12.9-2.9-13.5-7.2-.7-5 3.8-8.3 7.1-3.1 2.7-8.7-4.6-11.6-9.4-6.2 3.7-8.5 3.6-14.7-4.6-20.8-5.8 7.6-5 13.9 2.2 21.1-4.7-2.6-11.9.1-7.7 8.8 2.3-5.5 7.1-4.2 8.1.5.7 3.3-1.3 7.1-5.7 7.9-3.5.7-9-1.8-13.5-11.2 2.5.1 4.7 1.3 7.5 3.3l-4.7-15.4c-1.2 4.4-2.7 7.2-4.3 8.7-1.1-3-.9-5.3 0-10.2l-9.5 3.4c5 6.9 9.9 16.7 14 33.5 14.8-2.1 30.8-3.2 47.7-3.2z">
</path>
<image src="https://images.emojiterra.com/twitter/v14.0/512px/1f1fa-1f1f8.png" xlink:href=""
class="govuk-header__logotype-crown-fallback-image" width="36" height="32"></image>
class="govuk-header__logotype-fallback-image" width="36" height="32"></image>
</svg>
<span class="govuk-header__logotype-text">
US
@@ -108,11 +108,6 @@
Government Licence v3.0</a>, except where otherwise stated
</span>
</div>
<!-- <div class="govuk-footer__meta-item">
<a class="govuk-footer__link govuk-footer__copyright-logo"
href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">©
US copyright</a>
</div> -->
</div>
</div>
</footer>

View File

@@ -94,7 +94,6 @@ class Development(Config):
# Buckets
CSV_UPLOAD_BUCKET = _s3_credentials_from_env('CSV')
CONTACT_LIST_BUCKET = _s3_credentials_from_env('CONTACT')
LOGO_UPLOAD_BUCKET = _s3_credentials_from_env('LOGO')
# credential overrides
@@ -127,8 +126,6 @@ class Production(Config):
# buckets
CSV_UPLOAD_BUCKET = cloud_config.s3_credentials(
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(
f"notify-admin-logo-upload-bucket-{getenv('NOTIFY_ENVIRONMENT')}")

View File

@@ -1145,18 +1145,6 @@ class OrganisationOrganisationTypeForm(StripWhitespaceForm):
organisation_type = OrganisationTypeField('What type of organization is this?')
class OrganisationCrownStatusForm(StripWhitespaceForm):
crown_status = GovukRadiosField(
'Is this organization a crown body?',
choices=[
('crown', 'Yes'),
('non-crown', 'No'),
('unknown', 'Not sure'),
],
thing='whether this organization is a crown body',
)
class OrganisationAgreementSignedForm(StripWhitespaceForm):
agreement_signed = GovukRadiosField(
'Has this organization signed the agreement?',
@@ -1209,13 +1197,10 @@ class CreateServiceForm(StripWhitespaceForm):
class AdminNewOrganisationForm(
RenameOrganisationForm,
OrganisationOrganisationTypeForm,
OrganisationCrownStatusForm,
OrganisationOrganisationTypeForm
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Dont offer the not sure choice
self.crown_status.choices = self.crown_status.choices[:-1]
class AdminServiceSMSAllowanceForm(StripWhitespaceForm):

View File

@@ -23,8 +23,6 @@ def service_agreement(service_id):
return redirect(
url_for('main.add_organisation_from_nhs_local_service', service_id=current_service.id)
)
if current_service.organisation.crown is None:
return render_template('views/agreement/service-agreement-choose.html')
if current_service.organisation.agreement_signed:
return render_template('views/agreement/service-agreement-signed.html')
return render_template('views/agreement/service-agreement.html')
@@ -33,9 +31,7 @@ def service_agreement(service_id):
@main.route('/services/<uuid:service_id>/agreement.pdf')
@user_has_permissions('manage_service')
def service_download_agreement(service_id):
return send_file(**get_mou(
current_service.organisation.crown_status_or_404
))
return send_file(**get_mou())
@main.route('/services/<uuid:service_id>/agreement/accept', methods=['GET', 'POST'])
@@ -85,14 +81,13 @@ def service_confirm_agreement(service_id):
@main.route('/agreement/<variant>', endpoint='public_agreement')
@main.route('/agreement/<variant>.pdf', endpoint='public_download_agreement')
def public_agreement(variant):
if variant not in {'crown', 'non-crown'}:
# originally we returned 404 if variant was not in ['crown', 'not_crown']. Will we be using agreement.pdf?
# for now this is just to keep tests working as expected.
if variant != "agreement":
abort(404)
if request.endpoint == 'main.public_download_agreement':
return send_file(**get_mou(
organisation_is_crown=(variant == 'crown')
))
return send_file(**get_mou())
return render_template(
'views/agreement/agreement-public.html',

View File

@@ -23,7 +23,6 @@ from app.main.forms import (
AdminSetEmailBrandingForm,
InviteOrgUserForm,
OrganisationAgreementSignedForm,
OrganisationCrownStatusForm,
OrganisationOrganisationTypeForm,
RenameOrganisationForm,
SearchByNameForm,
@@ -278,35 +277,6 @@ def edit_organisation_type(org_id):
)
@main.route("/organisations/<uuid:org_id>/settings/edit-crown-status", methods=['GET', 'POST'])
@user_is_platform_admin
def edit_organisation_crown_status(org_id):
form = OrganisationCrownStatusForm(
crown_status={
True: 'crown',
False: 'non-crown',
None: 'unknown',
}.get(current_organisation.crown)
)
if form.validate_on_submit():
organisations_client.update_organisation(
current_organisation.id,
crown={
'crown': True,
'non-crown': False,
'unknown': None,
}.get(form.crown_status.data),
)
return redirect(url_for('.organisation_settings', org_id=org_id))
return render_template(
'views/organisations/organisation/settings/edit-crown-status.html',
form=form,
)
@main.route("/organisations/<uuid:org_id>/settings/edit-agreement", methods=['GET', 'POST'])
@user_is_platform_admin
def edit_organisation_agreement(org_id):
@@ -499,6 +469,4 @@ def organisation_billing(org_id):
@main.route('/organisations/<uuid:org_id>/agreement.pdf')
@user_is_platform_admin
def organisation_download_agreement(org_id):
return send_file(**get_mou(
current_organisation.crown_status_or_404
))
return send_file(**get_mou())

View File

@@ -35,7 +35,6 @@ from app.main.forms import (
SetSenderForm,
get_placeholder_form_instance,
)
from app.models.contact_list import ContactList, ContactListsAlphabetical
from app.models.user import Users
from app.s3_client.s3_csv_client import (
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):
try:
# 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,
service_id,
scheduled_for=request.form.get('scheduled_for', ''),
contact_list_id=request.form.get('contact_list_id', ''),
)
session.pop('sender_id', None)

View File

@@ -1,23 +1,10 @@
import itertools
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 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 flask import render_template, request
from app import current_service
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.templates import get_sample_template
from app.utils.user import user_has_permissions
MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB
@@ -39,7 +26,6 @@ def uploads(service_id):
if uploads.current_page == 1:
listed_uploads = (
current_service.contact_lists +
current_service.scheduled_jobs +
uploads
)
@@ -53,198 +39,3 @@ def uploads(service_id):
next_page=next_page,
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
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__(
service_id,
contact_list_id=contact_list_id,
statuses=self.statuses,
page=page,
limit_days=limit_days,

View File

@@ -1,6 +1,5 @@
from collections import OrderedDict
from flask import abort
from werkzeug.utils import cached_property
from app.models import (
@@ -29,7 +28,6 @@ class Organisation(JSONModel, SortByNameMixin):
'id',
'name',
'active',
'crown',
'organisation_type',
'email_branding_id',
'agreement_signed',
@@ -66,19 +64,13 @@ class Organisation(JSONModel, SortByNameMixin):
def create_from_form(cls, form):
return cls.create(
name=form.name.data,
crown={
'crown': True,
'non-crown': False,
'unknown': None,
}.get(form.crown_status.data),
organisation_type=form.organisation_type.data,
)
@classmethod
def create(cls, name, crown, organisation_type, agreement_signed=False):
def create(cls, name, organisation_type, agreement_signed=False):
return cls(organisations_client.create_organisation(
name=name,
crown=crown,
organisation_type=organisation_type,
agreement_signed=agreement_signed,
))
@@ -89,7 +81,6 @@ class Organisation(JSONModel, SortByNameMixin):
if self._dict == {}:
self.name = None
self.crown = None
self.agreement_signed = None
self.domains = []
self.organisation_type = None
@@ -100,12 +91,6 @@ class Organisation(JSONModel, SortByNameMixin):
def organisation_type_label(self):
return self.TYPE_LABELS.get(self.organisation_type)
@property
def crown_status_or_404(self):
if self.crown is None:
abort(404)
return self.crown
@property
def billing_details(self):
billing_details = [

View File

@@ -3,7 +3,6 @@ from notifications_utils.serialised_model import SerialisedModelCollection
from werkzeug.utils import cached_property
from app.models import JSONModel, SortByNameMixin
from app.models.contact_list import ContactLists
from app.models.job import (
ImmediateJobs,
PaginatedJobs,
@@ -527,10 +526,6 @@ class Service(JSONModel, SortByNameMixin):
}
)
@property
def contact_lists(self):
return ContactLists(self.id)
class Services(SerialisedModelCollection):
model = Service

View File

@@ -141,7 +141,6 @@ class MainNavigation(Navigation):
'add_service_template',
'check_messages',
'check_notification',
'choose_from_contact_list',
'choose_template',
'choose_template_to_copy',
'confirm_redact_template',
@@ -161,11 +160,6 @@ class MainNavigation(Navigation):
'view_template_versions',
},
'uploads': {
'upload_contact_list',
'check_contact_list',
'save_contact_list',
'contact_list',
'delete_contact_list',
'uploads',
'view_job',
'view_jobs',
@@ -244,7 +238,6 @@ class CaseworkNavigation(Navigation):
mapping = {
'send-one-off': {
'choose_from_contact_list',
'choose_template',
'send_one_off',
'send_one_off_step',
@@ -257,11 +250,6 @@ class CaseworkNavigation(Navigation):
'uploads': {
'view_jobs',
'view_job',
'upload_contact_list',
'check_contact_list',
'save_contact_list',
'contact_list',
'delete_contact_list',
'uploads',
},
}
@@ -276,7 +264,6 @@ class OrgNavigation(Navigation):
'settings': {
'edit_organisation_agreement',
'edit_organisation_billing_details',
'edit_organisation_crown_status',
'edit_organisation_domains',
'edit_organisation_email_branding',
'edit_organisation_domains',

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
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}
if limit_days is not None:
params['limit_days'] = limit_days
if statuses is not None:
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)
@@ -53,12 +51,11 @@ class JobApiClient(NotifyAdminAPIClient):
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(
service_id,
statuses=statuses or self.NON_SCHEDULED_JOB_STATUSES,
page=page,
contact_list_id=contact_list_id,
limit_days=limit_days,
)
@@ -88,15 +85,12 @@ class JobApiClient(NotifyAdminAPIClient):
def has_jobs(self, service_id):
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}
if 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)
job = self.post(url='/service/{}/job'.format(service_id), data=data)

View File

@@ -37,12 +37,11 @@ class OrganisationsClient(NotifyAdminAPIClient):
raise error
@cache.delete('organisations')
def create_organisation(self, name, crown, organisation_type, agreement_signed):
def create_organisation(self, name, organisation_type, agreement_signed):
return self.post(
url="/organisations",
data={
"name": name,
"crown": crown,
"organisation_type": organisation_type,
"agreement_signed": agreement_signed,
}

View File

@@ -4,17 +4,15 @@ from flask import current_app
from app.s3_client.s3_logo_client import get_s3_object
def get_mou(organisation_is_crown):
def get_mou():
bucket = current_app.config['MOU_BUCKET_NAME']
filename = 'crown.pdf' if organisation_is_crown else 'non-crown.pdf'
attachment_filename = 'U.S. Notify data sharing and financial agreement{}.pdf'.format(
'' if organisation_is_crown else ' (non-crown)'
)
filename = 'agreement.pdf'
attachment_filename = 'U.S. Notify data sharing and financial agreement.pdf'
try:
key = get_s3_object(bucket, filename)
return {
'path_or_file': key.get()['Body'],
'attachment_filename': attachment_filename,
'download_name': attachment_filename,
'as_attachment': True,
}
except botocore.exceptions.ClientError as exception:

View File

@@ -45,13 +45,7 @@
margin-right: govuk-spacing(1);
}
.govuk-header__logotype-crown {
margin-right: 1px;
fill: currentColor;
vertical-align: middle;
}
.govuk-header__logotype-crown-fallback-image {
.govuk-header__logotype-fallback-image {
width: 36px;
height: 32px;
border: 0;
@@ -269,8 +263,8 @@
background: transparent;
}
// Hide the inverted crown when printing in browsers that don't support SVG.
.govuk-header__logotype-crown-fallback-image {
// Hide the inverted logo when printing in browsers that don't support SVG.
.govuk-header__logotype-fallback-image {
display: none;
}
@@ -289,7 +283,7 @@
// Begin adjustments for font baseline offset
// These should be removed when the font is updated with the correct baseline
.govuk-header__logotype-crown {
.govuk-header__logotype {
position: relative;
top: -4px;
}

View File

@@ -5,7 +5,7 @@
<div class="govuk-header__logo">
<a href="{{ params.homepageUrl | default('/') }}" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<image src="{{ params.assetsPath | default('/assets/images') }}/email-template/us-flag.png" alt="United States Flag" xlink:href="" class="govuk-header__logotype-crown-fallback-image" width="36" height="32"></image>
<image src="{{ params.assetsPath | default('/assets/images') }}/email-template/us-flag.png" alt="United States Flag" xlink:href="" class="govuk-header__logotype-fallback-image" width="36" height="32"></image>
</svg>
<span class="govuk-header__logotype-text">
U.S.

View File

@@ -9,7 +9,7 @@ Service: {{ service.name }}
Organisation type: {{ service.organisation_type_label }}
Agreement signed: {{ organisation.agreement_signed|format_yes_no(none='Cant tell') }}
{%- if organisation.agreement_signed %}, for {{ current_service.organisation.name }}
{%- elif organisation.name %} (organisation is {{ organisation.name }}, {{ organisation.crown|format_yes_no(yes='a crown body', no='a non-crown body', none='crown status unknown') }})
{%- elif organisation.name %} (organisation is {{ organisation.name }})
{%- else %} (domain is {{ user.email_domain }})
{%- endif %}.
{%- if organisation.request_to_go_live_notes %} {{ organisation.request_to_go_live_notes }}{% endif %}

View File

@@ -1,41 +0,0 @@
{% extends "withnav_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
{% block service_page_title %}
Accept the data sharing and financial agreement
{% endblock %}
{% block backLink %}
{{ govukBackLink({ "href": url_for('main.request_to_go_live', service_id=current_service.id) }) }}
{% endblock %}
{% block maincolumn_content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-five-sixths">
{{ page_header('Accept the data sharing and financial agreement')}}
<p class="govuk-body">
Before you can use U.S. Notify, you need to accept our data sharing and financial agreement.
</p>
<p class="govuk-body">
This must be done by, or on behalf of, someone with the authority to sign contracts for your organization.
</p>
<p class="govuk-body">
Once accepted, the agreement covers all Notify services from your organization.
</p>
<p class="govuk-body">
There are different agreements for crown and non-crown organizations.
</p>
<p class="govuk-body">
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.support') }}">Contact us</a> to tell us
whether or not you work for a crown organization. If youre not sure
well help you work it out.
</p>
</div>
</div>
{% endblock %}

View File

@@ -30,7 +30,6 @@
<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'>
<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 %}
{{ choose_time_form.scheduled_for(param_extensions={
'formGroup': {'classes': 'bottom-gutter-2-3'},
@@ -41,12 +40,12 @@
}
}) }}
{% endif %}
{% set button_text %}
Send {{ count_of_recipients|message_count(template.template_type) }}
{% endset %}
{{ govukButton({ "text": button_text }) }}
</form>
</div>

View File

@@ -17,31 +17,13 @@
) %}
{% call row_heading() %}
<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('.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 %}
<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>
{% if item.scheduled %}
<span class="file-list-hint-large">
Sending {{
item.scheduled_for|format_datetime_relative
}}
</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 %}
<span class="file-list-hint-large">
Sent {{
@@ -62,12 +44,6 @@
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 %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-one-third">

View File

@@ -17,15 +17,5 @@
</p>
{% endif %}
{{ 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>
{% endblock %}

View File

@@ -26,7 +26,6 @@
{% call form_wrapper() %}
{{ form.name }}
{{ form.organisation_type }}
{{ form.crown_status }}
{{ page_footer('Save') }}
{% endcall %}
{% endblock %}

View File

@@ -1,25 +0,0 @@
{% extends "org_template.html" %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/page-header.html" import page_header %}
{% from "components/form.html" import form_wrapper %}
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
{% block org_page_title %}
Crown organization
{% endblock %}
{% block backLink %}
{{ govukBackLink({ "href": url_for('.organisation_settings', org_id=current_org.id) }) }}
{% endblock %}
{% block maincolumn_content %}
{{ page_header("Crown organization") }}
<div class="govuk-grid-row">
<div class="govuk-grid-column-five-sixths">
{% call form_wrapper() %}
{{ form.crown_status }}
{{ page_footer('Save') }}
{% endcall %}
</div>
</div>
{% endblock %}

View File

@@ -34,22 +34,6 @@
)
}}
{% endcall %}
{% call row() %}
{{ text_field('Crown organization') }}
{{ optional_text_field(
{
True: 'Yes',
False: 'No',
}.get(current_org.crown),
default='Not sure'
) }}
{{ edit_field(
'Change',
url_for('.edit_organisation_crown_status', org_id=current_org.id),
suffix='organization crown status'
)
}}
{% endcall %}
{% call row() %}
{{ text_field('Data sharing and financial agreement') }}
{{ text_field(

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">
{% 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>
{% 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 %}
{% if skip_link %}
<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') }}
{% endcall %}
{% 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 %}