mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-19 05:59:44 -04:00
Merge remote-tracking branch 'origin' into install-uswds
This commit is contained in:
@@ -94,7 +94,6 @@ from app.notify_client import InviteTokenError
|
||||
from app.notify_client.api_key_api_client import api_key_api_client
|
||||
from app.notify_client.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')
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
# Don’t offer the ‘not sure’ choice
|
||||
self.crown_status.choices = self.crown_status.choices[:-1]
|
||||
|
||||
|
||||
class AdminServiceSMSAllowanceForm(StripWhitespaceForm):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 doesn’t 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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
from functools import partial
|
||||
from os import path
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import abort, current_app
|
||||
from notifications_utils.formatters import strip_all_whitespace
|
||||
from notifications_utils.recipients import RecipientCSV
|
||||
from notifications_utils.s3 import s3upload as utils_s3upload
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.models import JSONModel, ModelList
|
||||
from app.models.job import PaginatedJobsAndScheduledJobs
|
||||
from app.notify_client.contact_list_api_client import contact_list_api_client
|
||||
from app.s3_client import (
|
||||
get_s3_contents,
|
||||
get_s3_metadata,
|
||||
get_s3_object,
|
||||
set_s3_metadata,
|
||||
)
|
||||
from app.s3_client.s3_csv_client import s3upload, set_metadata_on_csv_upload
|
||||
from app.utils.templates import get_sample_template
|
||||
|
||||
|
||||
class ContactList(JSONModel):
|
||||
|
||||
ALLOWED_PROPERTIES = {
|
||||
'id',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'has_jobs',
|
||||
'recent_job_count',
|
||||
'service_id',
|
||||
'original_file_name',
|
||||
'row_count',
|
||||
'template_type',
|
||||
}
|
||||
|
||||
upload_type = 'contact_list'
|
||||
|
||||
@classmethod
|
||||
def from_id(cls, contact_list_id, *, service_id):
|
||||
return cls(contact_list_api_client.get_contact_list(
|
||||
service_id=service_id,
|
||||
contact_list_id=contact_list_id,
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def get_bucket_credentials(key):
|
||||
return current_app.config['CONTACT_LIST_BUCKET'][key]
|
||||
|
||||
@staticmethod
|
||||
def get_bucket_name():
|
||||
return ContactList.get_bucket_credentials('bucket')
|
||||
|
||||
@staticmethod
|
||||
def get_access_key():
|
||||
return ContactList.get_bucket_credentials('access_key_id')
|
||||
|
||||
@staticmethod
|
||||
def get_secret_key():
|
||||
return ContactList.get_bucket_credentials('secret_access_key')
|
||||
|
||||
@staticmethod
|
||||
def get_region():
|
||||
return ContactList.get_bucket_credentials('region')
|
||||
|
||||
@staticmethod
|
||||
def get_filename(service_id, upload_id):
|
||||
return f"service-{service_id}-notify/{upload_id}.csv"
|
||||
|
||||
@staticmethod
|
||||
def get_s3_arguments(service_id, upload_id):
|
||||
return (
|
||||
ContactList.get_bucket_name(),
|
||||
ContactList.get_filename(service_id, upload_id),
|
||||
ContactList.get_access_key(),
|
||||
ContactList.get_secret_key(),
|
||||
ContactList.get_region(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def upload(service_id, file_dict):
|
||||
upload_id = str(uuid4())
|
||||
utils_s3upload(
|
||||
filedata=file_dict['data'],
|
||||
region=ContactList.get_region(),
|
||||
bucket_name=ContactList.get_bucket_name(),
|
||||
file_location=ContactList.get_filename(service_id, upload_id),
|
||||
access_key=ContactList.get_access_key(),
|
||||
secret_key=ContactList.get_secret_key(),
|
||||
)
|
||||
return upload_id
|
||||
|
||||
@staticmethod
|
||||
def download(service_id, upload_id):
|
||||
return strip_all_whitespace(
|
||||
get_s3_contents(
|
||||
get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id))))
|
||||
|
||||
@staticmethod
|
||||
def set_metadata(service_id, upload_id, **kwargs):
|
||||
return set_s3_metadata(get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id)), **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_metadata(service_id, upload_id):
|
||||
return get_s3_metadata(get_s3_object(*ContactList.get_s3_arguments(service_id, upload_id)))
|
||||
|
||||
def copy_to_uploads(self):
|
||||
raise RuntimeError("RCA probably an issue with copying between buckets")
|
||||
metadata = self.get_metadata(self.service_id, self.id)
|
||||
new_upload_id = s3upload(
|
||||
self.service_id,
|
||||
{'data': self.contents},
|
||||
ContactList.get_region(),
|
||||
)
|
||||
set_metadata_on_csv_upload(
|
||||
self.service_id,
|
||||
new_upload_id,
|
||||
**metadata,
|
||||
)
|
||||
return new_upload_id
|
||||
|
||||
@classmethod
|
||||
def create(cls, service_id, upload_id):
|
||||
|
||||
metadata = cls.get_metadata(service_id, upload_id)
|
||||
|
||||
if not metadata.get('valid'):
|
||||
abort(403)
|
||||
|
||||
return cls(contact_list_api_client.create_contact_list(
|
||||
service_id=service_id,
|
||||
upload_id=upload_id,
|
||||
original_file_name=metadata['original_file_name'],
|
||||
row_count=int(metadata['row_count']),
|
||||
template_type=metadata['template_type'],
|
||||
))
|
||||
|
||||
def delete(self):
|
||||
contact_list_api_client.delete_contact_list(
|
||||
service_id=self.service_id,
|
||||
contact_list_id=self.id,
|
||||
)
|
||||
|
||||
@property
|
||||
def contents(self):
|
||||
return self.download(self.service_id, self.id)
|
||||
|
||||
@cached_property
|
||||
def recipients(self):
|
||||
return RecipientCSV(
|
||||
self.contents,
|
||||
template=get_sample_template(self.template_type),
|
||||
allow_international_sms=True,
|
||||
max_initial_rows_shown=50,
|
||||
)
|
||||
|
||||
@property
|
||||
def saved_file_name(self):
|
||||
file_name, extention = path.splitext(self.original_file_name)
|
||||
return f'{file_name}.csv'
|
||||
|
||||
def get_jobs(self, *, page, limit_days=None):
|
||||
return PaginatedJobsAndScheduledJobs(
|
||||
self.service_id,
|
||||
contact_list_id=self.id,
|
||||
page=page,
|
||||
limit_days=limit_days,
|
||||
)
|
||||
|
||||
|
||||
class ContactLists(ModelList):
|
||||
|
||||
client_method = contact_list_api_client.get_contact_lists
|
||||
model = ContactList
|
||||
sort_function = partial(
|
||||
sorted,
|
||||
key=lambda item: item['created_at'],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def __init__(self, service_id, template_type=None):
|
||||
super().__init__(service_id)
|
||||
self.items = self.sort_function([
|
||||
item for item in self.items
|
||||
if template_type in {item['template_type'], None}
|
||||
])
|
||||
|
||||
|
||||
class ContactListsAlphabetical(ContactLists):
|
||||
|
||||
sort_function = partial(
|
||||
sorted,
|
||||
key=lambda item: item['original_file_name'].lower(),
|
||||
)
|
||||
@@ -172,10 +172,9 @@ class PaginatedJobs(PaginatedModelList, ImmediateJobs):
|
||||
client_method = job_api_client.get_page_of_jobs
|
||||
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,
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||
|
||||
|
||||
class ContactListApiClient(NotifyAdminAPIClient):
|
||||
|
||||
def create_contact_list(
|
||||
self,
|
||||
*,
|
||||
service_id,
|
||||
upload_id,
|
||||
original_file_name,
|
||||
row_count,
|
||||
template_type,
|
||||
):
|
||||
data = {
|
||||
"id": upload_id,
|
||||
"original_file_name": original_file_name,
|
||||
"row_count": row_count,
|
||||
"template_type": template_type,
|
||||
}
|
||||
|
||||
data = _attach_current_user(data)
|
||||
job = self.post(url='/service/{}/contact-list'.format(service_id), data=data)
|
||||
|
||||
return job
|
||||
|
||||
def get_contact_lists(self, service_id):
|
||||
return self.get(f'/service/{service_id}/contact-list')
|
||||
|
||||
def get_contact_list(self, *, service_id, contact_list_id):
|
||||
return self.get(f'/service/{service_id}/contact-list/{contact_list_id}')
|
||||
|
||||
def delete_contact_list(self, *, service_id, contact_list_id):
|
||||
return self.delete(f'/service/{service_id}/contact-list/{contact_list_id}')
|
||||
|
||||
|
||||
contact_list_api_client = ContactListApiClient()
|
||||
@@ -25,14 +25,12 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
|
||||
return job
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -9,7 +9,7 @@ Service: {{ service.name }}
|
||||
Organisation type: {{ service.organisation_type_label }}
|
||||
Agreement signed: {{ organisation.agreement_signed|format_yes_no(none='Can’t 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 %}
|
||||
|
||||
@@ -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 you’re not sure
|
||||
we’ll help you work it out.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
{% call form_wrapper() %}
|
||||
{{ form.name }}
|
||||
{{ form.organisation_type }}
|
||||
{{ form.crown_status }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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(
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/big-number.html" import big_number -%}
|
||||
{% from "components/list.html" import list_of_placeholders %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Choose an emergency contact list
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('.send_one_off', service_id=current_service.id, template_id=template.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Choose an emergency contact list') }}
|
||||
|
||||
{% if template.placeholders %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-five-sixths">
|
||||
<p class="govuk-body">
|
||||
You cannot use an emergency contact list with this template because it
|
||||
is personalized with {{ list_of_placeholders(template.placeholders) }}.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
Emergency contact lists can only include email addresses or phone
|
||||
numbers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif contact_lists %}
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
contact_lists,
|
||||
caption="Emergency contact lists",
|
||||
caption_visible=False,
|
||||
empty_message=(
|
||||
'You have not saved any contact lists yet.'
|
||||
),
|
||||
field_headings=[
|
||||
'File',
|
||||
'Status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<div class="file-list">
|
||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('main.send_from_contact_list', service_id=current_service.id, template_id=template.id, contact_list_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||
<span class="file-list-hint-large">
|
||||
Uploaded {{ item.created_at|format_datetime_relative }}
|
||||
</span>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ big_number(
|
||||
item.row_count,
|
||||
smallest=True,
|
||||
label=item.row_count|recipient_count_label(item.template_type),
|
||||
) }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-five-sixths">
|
||||
<p class="govuk-body">
|
||||
You have not saved any lists of {{ 99|recipient_count_label(template.template_type) }} yet.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
To upload and save an emergency contact list, go to the <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.uploads', service_id=current_service.id) }}">uploads</a> page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -31,9 +31,6 @@
|
||||
<div class="govuk-grid-column-full">
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/radios.html" import radio_select %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="bottom-gutter-1-2">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
|
||||
{% if recipients.too_many_rows %}
|
||||
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
||||
Your file has too many rows
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
Notify can store files up to
|
||||
{{ "{:,}".format(recipients.max_rows) }} rows in size. Your
|
||||
file has {{ "{:,}".format(recipients|length) }} rows.
|
||||
</p>
|
||||
|
||||
{% elif not recipients.allowed_to_send_to %}
|
||||
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Trial mode: bad recipients" data-error-label="{{ upload_id }}">
|
||||
You cannot save
|
||||
{{ 'this' if recipients|length == 1 else 'these' }}
|
||||
{{ recipients|length|recipient_count_label(recipients.template_type) }}
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
In <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.trial_mode_new') }}">trial mode</a> you can only
|
||||
send to yourself and members of your team
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
||||
button_text='Upload your file again'
|
||||
) }}
|
||||
</div>
|
||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
||||
</div>
|
||||
|
||||
<h2 class="heading-medium" id="file-preview">{{ original_file_name }}</h2>
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
||||
] + recipients.column_headers
|
||||
) %}
|
||||
{{ index_field(item.index + 2) }}
|
||||
{% for column in recipients.column_headers %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
|
||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,142 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
{% from "components/radios.html" import radio_select %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading, row, row_heading %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/uk_components/button/macro.njk" import govukButton %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ contact_list.original_file_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('main.uploads', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(contact_list.original_file_name) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
Uploaded by {{ contact_list.created_by }} {{ contact_list.created_at|format_datetime_human }}.
|
||||
</p>
|
||||
|
||||
{% if jobs %}
|
||||
<p class="govuk-body">
|
||||
Used {{ jobs|length|iteration_count }}
|
||||
in the last {{ current_service.get_days_of_retention(contact_list.template_type) }}
|
||||
days.
|
||||
</p>
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
jobs,
|
||||
caption="Messages sent from this contact list",
|
||||
caption_visible=False,
|
||||
empty_message='',
|
||||
field_headings=[
|
||||
'Template',
|
||||
'Status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<div class="file-list">
|
||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.template_name }}</a>
|
||||
{% if item.scheduled %}
|
||||
<span class="file-list-hint-large">
|
||||
Sending {{
|
||||
item.scheduled_for|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="file-list-hint-large">
|
||||
Sent {{
|
||||
(item.scheduled_for or item.created_at)|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{% if item.scheduled %}
|
||||
{{ big_number(
|
||||
item.notification_count,
|
||||
smallest=True,
|
||||
label=item.notification_count|message_count_label(
|
||||
item.template_type,
|
||||
suffix='waiting to send'
|
||||
)
|
||||
) }}
|
||||
{% else %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-one-third">
|
||||
{{ big_number(
|
||||
item.notifications_sending,
|
||||
smallest=True,
|
||||
label='sending',
|
||||
) }}
|
||||
</div>
|
||||
<div class="govuk-grid-column-one-third">
|
||||
{{ big_number(item.notifications_delivered, smallest=True, label='delivered') }}
|
||||
</div>
|
||||
<div class="govuk-grid-column-one-third">
|
||||
{{ big_number(item.notifications_failed, smallest=True, label='failed') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="govuk-body">
|
||||
{% if contact_list.has_jobs %}
|
||||
Not used in the last {{ current_service.get_days_of_retention(contact_list.template_type) }} days.
|
||||
{% else %}
|
||||
Not used yet.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="govuk-heading-m govuk-!-margin-bottom-2">
|
||||
{{ contact_list.recipients|length|format_thousands }} saved {{ contact_list.recipients|length|recipient_count_label(contact_list.template_type) }}
|
||||
</h2>
|
||||
|
||||
{% set recipient_column = contact_list.recipients.column_headers[0] %}
|
||||
|
||||
<div class="body-copy-table">
|
||||
{% call(item, row_number) list_table(
|
||||
contact_list.recipients.displayed_rows,
|
||||
caption=contact_list.recipients|length|recipient_count_label(contact_list.template_type)|capitalize,
|
||||
caption_visible=False,
|
||||
field_headings=[recipient_column],
|
||||
field_headings_visible=False,
|
||||
) %}
|
||||
{% if not loop.first %}
|
||||
{{ text_field(item[recipient_column].data) }}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if contact_list.recipients.displayed_rows|list|length < contact_list.recipients|length %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ contact_list.recipients.displayed_rows|list|length }} rows
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
<div class="page-footer">
|
||||
{% if not confirm_delete_banner %}
|
||||
<span class="page-footer-link page-footer-delete-link-without-button">
|
||||
<a class="govuk-link govuk-link--destructive" href="{{ url_for('main.delete_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Delete this contact list</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<a class="govuk-link govuk-link--no-visited-state page-footer-right-aligned-link-without-button" download href="{{ url_for('main.download_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Download this contact list (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,64 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/radios.html" import radio_select %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/uk_components/button/macro.njk" import govukButton %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ original_file_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(original_file_name) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
{{ recipients|length|recipient_count(recipients.template_type) }} found
|
||||
</p>
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
<form method="post" enctype="multipart/form-data" action="{{ url_for('main.save_contact_list', service_id=current_service.id, upload_id=upload_id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{{ govukButton({ "text": "Save contact list" }) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
File preview
|
||||
</h2>
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
||||
] + recipients.column_headers
|
||||
) %}
|
||||
{{ index_field(item.index + 2) }}
|
||||
{% for column in recipients.column_headers %}
|
||||
{% if item[column].ignore %}
|
||||
{{ text_field(item[column].data or '', status='default') }}
|
||||
{% else %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
{{ text_field(column, status='default') }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,102 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/radios.html" import radio_select %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="bottom-gutter-1-2">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% if row_errors|length == 1 %}
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
|
||||
There’s a problem with {{ original_file_name }}
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
You need to {{ row_errors[0] }}.
|
||||
</p>
|
||||
{% else %}
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Bad rows" data-error-label="{{ upload_id }}">
|
||||
There are some problems with {{ original_file_name }}
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
You need to:
|
||||
</p>
|
||||
<ul class="list-bullet">
|
||||
{% for error in row_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
||||
button_text='Upload your file again'
|
||||
) }}
|
||||
</div>
|
||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
||||
</div>
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true" class="table-field-invisible-error">1</span>'|safe
|
||||
] + recipients.column_headers
|
||||
) %}
|
||||
{% call index_field() %}
|
||||
<span class="{% if item.has_errors %}table-field-error{% endif %}">
|
||||
{{ item.index + 2 }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% for column in recipients.column_headers %}
|
||||
{% if item[column].error and not recipients.missing_column_headers %}
|
||||
{% call field() %}
|
||||
<span>
|
||||
<span class="table-field-error-label">{{ item[column].error }}</span>
|
||||
{{ item[column].data if item[column].data != None }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% elif item[column].ignore %}
|
||||
{{ text_field(item[column].data or '', status='default') }}
|
||||
{% else %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
{{ text_field(column, status='default') }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
||||
{% if recipients.displayed_rows|list|length < recipients.rows_with_errors|list|length %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows with errors
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing rows with errors
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,104 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/radios.html" import radio_select %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="bottom-gutter-1-2">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
|
||||
{% if not recipients|length %}
|
||||
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="No rows" data-error-label="{{ upload_id }}">
|
||||
Your file is missing some rows
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
It needs at least one row of data
|
||||
{%- if template_type %}.{% else %}, in a column called ‘email address’ or ‘phone number’.{% endif %}
|
||||
</p>
|
||||
|
||||
{% elif recipients.column_headers|length == 1 %}
|
||||
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
||||
Your file needs a column called ‘email address’ or ‘phone number’.
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
Right now it has 1 column called ‘{{ recipients._raw_column_headers[0] }}’.
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
|
||||
<h1 class='banner-title' data-module="track-error" data-error-type="Too many rows" data-error-label="{{ upload_id }}">
|
||||
Your file has too many columns
|
||||
</h1>
|
||||
<p class="govuk-body">
|
||||
It needs to have 1 column, called ‘email address’ or ‘phone number’.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
Right now it has {{ recipients._raw_column_headers|length }} columns called {{ recipients._raw_column_headers | formatted_list }}.
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
action=url_for('.upload_contact_list', service_id=current_service.id),
|
||||
button_text='Upload your file again'
|
||||
) }}
|
||||
</div>
|
||||
<a href="#content" class="govuk-link govuk-link--no-visited-state back-to-top-link">Back to top</a>
|
||||
</div>
|
||||
|
||||
{% set column_headers = recipients._raw_column_headers if recipients.duplicate_recipient_column_headers else recipients.column_headers %}
|
||||
|
||||
<h2 class="heading-medium" id="file-preview">{{ original_file_name }}</h2>
|
||||
|
||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="govuk-visually-hidden">Row in file</span> <span aria-hidden="true">1</span>'|safe
|
||||
] + recipients._raw_column_headers
|
||||
) %}
|
||||
{{ index_field(item.index + 2) }}
|
||||
{% for column in column_headers %}
|
||||
{% if item[column].ignore %}
|
||||
{{ text_field(item[column].data or '', status='default') }}
|
||||
{% else %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
{{ text_field(column, status='default') }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% if recipients.displayed_rows|list|length < recipients|length %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ recipients.displayed_rows|list|length }} rows
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,96 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/table.html" import list_table, text_field, index_field, index_field_heading %}
|
||||
{% from "components/uk_components/back-link/macro.njk" import govukBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Upload an emergency contact list
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{% if not error %}
|
||||
{{ govukBackLink({ "href": url_for('main.uploads', service_id=current_service.id) }) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{% if error %}
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
<h1 class="banner-title">{{ error.title }}</h1>
|
||||
{% if error.detail %}
|
||||
<p class="govuk-body">{{ error.detail | safe }}</p>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{{ page_header('Upload an emergency contact list') }}
|
||||
<p class="govuk-body">
|
||||
Save a list of staff email addresses or phone numbers in Notify.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
In an emergency, you can send a message to everyone on the list.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
Do not include contact details for members of the public.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-gutter">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
button_text='Choose file',
|
||||
show_errors=False,
|
||||
)}}
|
||||
</div>
|
||||
|
||||
<h2 class="heading-medium">Your file needs to look like one of these examples</h2>
|
||||
|
||||
<p class="hint">
|
||||
Save your file as a
|
||||
<acronym title="Comma Separated Values">CSV</acronym>,
|
||||
<acronym title="Tab Separated Values">TSV</acronym>,
|
||||
<acronym title="Open Document Spreadsheet">ODS</acronym>,
|
||||
or Microsoft Excel spreadsheet
|
||||
</p>
|
||||
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-one-half">
|
||||
<div class="spreadsheet">
|
||||
{% call(item, row_number) list_table(
|
||||
[
|
||||
['email address'],
|
||||
['test@example.gsa.gov'],
|
||||
],
|
||||
caption="Example",
|
||||
caption_visible=False,
|
||||
field_headings=['', 'A']
|
||||
) %}
|
||||
{{ index_field(row_number - 1) }}
|
||||
{% for column in item %}
|
||||
{{ text_field(column) }}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="govuk-grid-column-one-half">
|
||||
<div class="spreadsheet">
|
||||
{% call(item, row_number) list_table(
|
||||
[
|
||||
['phone number'],
|
||||
['555-867-5309'],
|
||||
],
|
||||
caption="Example",
|
||||
caption_visible=False,
|
||||
field_headings=['', 'A']
|
||||
) %}
|
||||
{{ index_field(row_number - 1) }}
|
||||
{% for column in item %}
|
||||
{{ text_field(column) }}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user