mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 02:23:19 -04:00
Merge branch 'master' into flask-login-again
This commit is contained in:
@@ -64,6 +64,7 @@ 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
|
||||
@@ -91,6 +92,7 @@ from app.url_converters import (
|
||||
LetterFileExtensionConverter,
|
||||
SimpleDateTypeConverter,
|
||||
TemplateTypeConverter,
|
||||
TicketTypeConverter,
|
||||
)
|
||||
from app.utils import format_thousands, get_logo_cdn_domain, id_safe
|
||||
|
||||
@@ -140,6 +142,7 @@ 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,
|
||||
@@ -225,6 +228,7 @@ def init_app(application):
|
||||
|
||||
application.url_map.converters['uuid'].to_python = lambda self, value: value
|
||||
application.url_map.converters['template_type'] = TemplateTypeConverter
|
||||
application.url_map.converters['ticket_type'] = TicketTypeConverter
|
||||
application.url_map.converters['letter_file_extension'] = LetterFileExtensionConverter
|
||||
application.url_map.converters['simple_date'] = SimpleDateTypeConverter
|
||||
|
||||
|
||||
@@ -89,3 +89,8 @@ $iso-paper-ratio: 141.42135624%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.letter-recipient-summary {
|
||||
line-height: 28px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
text-align: left;
|
||||
|
||||
&-bar {
|
||||
@include bold-27;
|
||||
@include bold-27($tabular-numbers: true);
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
@@ -83,6 +83,17 @@
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
&-hint-large {
|
||||
@include core-19;
|
||||
display: block;
|
||||
color: $secondary-text-colour;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.failure-highlight {
|
||||
|
||||
@@ -68,6 +68,7 @@ class Config(object):
|
||||
WTF_CSRF_ENABLED = True
|
||||
WTF_CSRF_TIME_LIMIT = None
|
||||
CSV_UPLOAD_BUCKET_NAME = 'local-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'local-contact-list'
|
||||
ACTIVITY_STATS_LIMIT_DAYS = 7
|
||||
TEST_MESSAGE_FILENAME = 'Report'
|
||||
|
||||
@@ -98,6 +99,7 @@ class Development(Config):
|
||||
SESSION_PROTECTION = None
|
||||
STATSD_ENABLED = False
|
||||
CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'development-contact-list'
|
||||
LOGO_UPLOAD_BUCKET_NAME = 'public-logos-tools'
|
||||
MOU_BUCKET_NAME = 'notify.tools-mou'
|
||||
TRANSIENT_UPLOADED_LETTERS = 'development-transient-uploaded-letters'
|
||||
@@ -121,6 +123,7 @@ class Test(Development):
|
||||
STATSD_ENABLED = False
|
||||
WTF_CSRF_ENABLED = False
|
||||
CSV_UPLOAD_BUCKET_NAME = 'test-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'test-contact-list'
|
||||
LOGO_UPLOAD_BUCKET_NAME = 'public-logos-test'
|
||||
MOU_BUCKET_NAME = 'test-mou'
|
||||
TRANSIENT_UPLOADED_LETTERS = 'test-transient-uploaded-letters'
|
||||
@@ -140,6 +143,7 @@ class Preview(Config):
|
||||
HEADER_COLOUR = '#F499BE' # $baby-pink
|
||||
STATSD_ENABLED = True
|
||||
CSV_UPLOAD_BUCKET_NAME = 'preview-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'preview-contact-list'
|
||||
LOGO_UPLOAD_BUCKET_NAME = 'public-logos-preview'
|
||||
MOU_BUCKET_NAME = 'notify.works-mou'
|
||||
TRANSIENT_UPLOADED_LETTERS = 'preview-transient-uploaded-letters'
|
||||
@@ -158,6 +162,7 @@ class Staging(Config):
|
||||
HEADER_COLOUR = '#6F72AF' # $mauve
|
||||
STATSD_ENABLED = True
|
||||
CSV_UPLOAD_BUCKET_NAME = 'staging-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'staging-contact-list'
|
||||
LOGO_UPLOAD_BUCKET_NAME = 'public-logos-staging'
|
||||
MOU_BUCKET_NAME = 'staging-notify.works-mou'
|
||||
TRANSIENT_UPLOADED_LETTERS = 'staging-transient-uploaded-letters'
|
||||
@@ -173,6 +178,7 @@ class Live(Config):
|
||||
HTTP_PROTOCOL = 'https'
|
||||
STATSD_ENABLED = True
|
||||
CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload'
|
||||
CONTACT_LIST_UPLOAD_BUCKET_NAME = 'production-contact-list'
|
||||
LOGO_UPLOAD_BUCKET_NAME = 'public-logos-production'
|
||||
MOU_BUCKET_NAME = 'notifications.service.gov.uk-mou'
|
||||
TRANSIENT_UPLOADED_LETTERS = 'production-transient-uploaded-letters'
|
||||
|
||||
+78
-10
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import weakref
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import chain
|
||||
@@ -9,12 +10,17 @@ from flask_wtf import FlaskForm as Form
|
||||
from flask_wtf.file import FileAllowed
|
||||
from flask_wtf.file import FileField as FileField_wtf
|
||||
from notifications_utils.columns import Columns
|
||||
from notifications_utils.formatters import strip_whitespace
|
||||
from notifications_utils.formatters import (
|
||||
normalise_whitespace_and_newlines,
|
||||
remove_whitespace_before_punctuation,
|
||||
strip_whitespace,
|
||||
)
|
||||
from notifications_utils.recipients import (
|
||||
InvalidPhoneError,
|
||||
normalise_phone_number,
|
||||
validate_phone_number,
|
||||
)
|
||||
from notifications_utils.take import Take
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
DateField,
|
||||
@@ -48,6 +54,7 @@ from app.main.validators import (
|
||||
ValidEmail,
|
||||
ValidGovEmail,
|
||||
)
|
||||
from app.models.feedback import PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE
|
||||
from app.models.organisation import Organisation
|
||||
from app.models.roles_and_permissions import permissions, roles
|
||||
from app.utils import guess_name_from_email_address
|
||||
@@ -361,6 +368,23 @@ class StripWhitespaceStringField(StringField):
|
||||
super(StringField, self).__init__(label, **kwargs)
|
||||
|
||||
|
||||
class StripWhitespaceTextAreaField(TextAreaField):
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist:
|
||||
self.data = Take(
|
||||
valuelist[0]
|
||||
).then(
|
||||
remove_whitespace_before_punctuation
|
||||
).then(
|
||||
normalise_whitespace_and_newlines
|
||||
).then(
|
||||
# similar to normalise_multiple_newlines but taking everything down to one `\n` instead of two
|
||||
lambda value: re.compile(r'\n{2,}').sub('\n', value)
|
||||
).then(
|
||||
str.strip
|
||||
)
|
||||
|
||||
|
||||
class OnOffField(RadioField):
|
||||
|
||||
def __init__(self, label, choices=None, *args, **kwargs):
|
||||
@@ -740,6 +764,43 @@ class SMSTemplateForm(BaseTemplateForm):
|
||||
OnlySMSCharacters()(None, field)
|
||||
|
||||
|
||||
class LetterAddressForm(StripWhitespaceForm):
|
||||
MIN_ADDRESS_LINES = 3
|
||||
MAX_ADDRESS_LINES = 7
|
||||
|
||||
address = StripWhitespaceTextAreaField(
|
||||
'Address',
|
||||
validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
|
||||
def validate_address(self, field):
|
||||
lines = field.data.splitlines()
|
||||
if len(lines) < self.MIN_ADDRESS_LINES:
|
||||
raise ValidationError('Address must be at least 3 lines long')
|
||||
if len(lines) > self.MAX_ADDRESS_LINES:
|
||||
raise ValidationError('Address must be no more than 7 lines long')
|
||||
|
||||
@property
|
||||
def as_address_lines_1_to_7_with_postcode(self):
|
||||
lines = self.address.data.splitlines()
|
||||
placeholders = {}
|
||||
|
||||
# set all placeholders to empty strings, or all_placeholders_in_session will always return false.
|
||||
# note that it must be `address line #` with spaces, not underscores or dashes
|
||||
for i in range(1, 7):
|
||||
placeholders[f'address line {i}'] = ''
|
||||
|
||||
# unroll the address into lines, and place into the session in the underlying placeholder names
|
||||
# postcode is required so make sure we put the last value in that
|
||||
# TODO: When postcode is no longer a required field, remove this special case and just use `address line #`
|
||||
address_lines, last_address_line = lines[:-1], lines[-1]
|
||||
for i, line in enumerate(address_lines, start=1):
|
||||
placeholders[f'address line {i}'] = line
|
||||
placeholders['postcode'] = last_address_line
|
||||
|
||||
return placeholders
|
||||
|
||||
|
||||
class EmailTemplateForm(BaseTemplateForm):
|
||||
subject = TextAreaField(
|
||||
u'Subject',
|
||||
@@ -885,23 +946,30 @@ class SupportType(StripWhitespaceForm):
|
||||
support_type = RadioField(
|
||||
'How can we help you?',
|
||||
choices=[
|
||||
('report-problem', 'Report a problem'),
|
||||
('ask-question-give-feedback', 'Ask a question or give feedback'),
|
||||
(PROBLEM_TICKET_TYPE, 'Report a problem'),
|
||||
(QUESTION_TICKET_TYPE, 'Ask a question or give feedback'),
|
||||
],
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
|
||||
|
||||
class Feedback(StripWhitespaceForm):
|
||||
name = StringField('Name')
|
||||
email_address = email_address(label='Email address', gov_user=False, required=False)
|
||||
class SupportRedirect(StripWhitespaceForm):
|
||||
who = RadioField(
|
||||
'What do you need help with?',
|
||||
choices=[
|
||||
('public-sector', 'I work in the public sector and need to send emails, text messages or letters'),
|
||||
('public', 'I’m a member of the public with a question for the government'),
|
||||
],
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
|
||||
|
||||
class FeedbackOrProblem(StripWhitespaceForm):
|
||||
name = StringField('Name (optional)')
|
||||
email_address = email_address(label='Email address', gov_user=False, required=True)
|
||||
feedback = TextAreaField('Your message', validators=[DataRequired(message="Cannot be empty")])
|
||||
|
||||
|
||||
class Problem(Feedback):
|
||||
email_address = email_address(label='Email address', gov_user=False)
|
||||
|
||||
|
||||
class Triage(StripWhitespaceForm):
|
||||
severe = RadioField(
|
||||
'Is it an emergency?',
|
||||
|
||||
@@ -43,11 +43,10 @@ class ValidGovEmail:
|
||||
return
|
||||
|
||||
from flask import url_for
|
||||
message = (
|
||||
'Enter a government email address.'
|
||||
' If you think you should have access'
|
||||
' <a class="govuk-link govuk-link--no-visited-state" href="{}">contact us</a>'
|
||||
).format(url_for('main.support'))
|
||||
message = '''
|
||||
Enter a public sector email address or
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{}">find out who can use Notify</a>
|
||||
'''.format(url_for('main.who_its_for'))
|
||||
if not is_gov_user(field.data.lower()):
|
||||
raise ValidationError(message)
|
||||
|
||||
|
||||
@@ -309,10 +309,6 @@ def get_dashboard_partials(service_id):
|
||||
[row['count'] for row in template_statistics] or [0]
|
||||
),
|
||||
),
|
||||
'jobs': render_template(
|
||||
'views/dashboard/_jobs.html',
|
||||
jobs=current_service.immediate_jobs,
|
||||
),
|
||||
'usage': render_template(
|
||||
'views/dashboard/_usage.html',
|
||||
**calculate_usage(yearly_usage, free_sms_allowance),
|
||||
|
||||
+65
-47
@@ -1,67 +1,79 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from flask import abort, redirect, render_template, request, session, url_for
|
||||
from flask import redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import convert_to_boolean, current_service, service_api_client
|
||||
from app.extensions import zendesk_client
|
||||
from app.main import main
|
||||
from app.main.forms import Feedback, Problem, SupportType, Triage
|
||||
|
||||
QUESTION_TICKET_TYPE = 'ask-question-give-feedback'
|
||||
PROBLEM_TICKET_TYPE = "report-problem"
|
||||
|
||||
|
||||
def get_prefilled_message():
|
||||
return {
|
||||
'agreement': (
|
||||
'Please can you tell me if there’s an agreement in place '
|
||||
'between GOV.UK Notify and my organisation?'
|
||||
),
|
||||
'letter-branding': (
|
||||
'I would like my own logo on my letter templates.'
|
||||
),
|
||||
}.get(
|
||||
request.args.get('body'), ''
|
||||
)
|
||||
from app.main.forms import (
|
||||
FeedbackOrProblem,
|
||||
SupportRedirect,
|
||||
SupportType,
|
||||
Triage,
|
||||
)
|
||||
from app.models.feedback import (
|
||||
GENERAL_TICKET_TYPE,
|
||||
PROBLEM_TICKET_TYPE,
|
||||
QUESTION_TICKET_TYPE,
|
||||
)
|
||||
|
||||
|
||||
@main.route('/support', methods=['GET', 'POST'])
|
||||
def support():
|
||||
form = SupportType()
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=form.support_type.data,
|
||||
))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form = SupportType()
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=form.support_type.data,
|
||||
))
|
||||
else:
|
||||
form = SupportRedirect()
|
||||
if form.validate_on_submit():
|
||||
if form.who.data == 'public':
|
||||
return redirect(url_for(
|
||||
'.support_public'
|
||||
))
|
||||
else:
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=GENERAL_TICKET_TYPE,
|
||||
))
|
||||
|
||||
return render_template('views/support/index.html', form=form)
|
||||
|
||||
|
||||
@main.route('/support/public')
|
||||
def support_public():
|
||||
return render_template('views/support/public.html')
|
||||
|
||||
|
||||
@main.route('/support/triage', methods=['GET', 'POST'])
|
||||
def triage():
|
||||
@main.route('/support/triage/<ticket_type:ticket_type>', methods=['GET', 'POST'])
|
||||
def triage(ticket_type=PROBLEM_TICKET_TYPE):
|
||||
form = Triage()
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for(
|
||||
'.feedback',
|
||||
ticket_type=PROBLEM_TICKET_TYPE,
|
||||
ticket_type=ticket_type,
|
||||
severe=form.severe.data
|
||||
))
|
||||
return render_template(
|
||||
'views/support/triage.html',
|
||||
form=form
|
||||
form=form,
|
||||
page_title={
|
||||
PROBLEM_TICKET_TYPE: 'Report a problem',
|
||||
GENERAL_TICKET_TYPE: 'Contact GOV.UK Notify support',
|
||||
}.get(ticket_type)
|
||||
)
|
||||
|
||||
|
||||
@main.route('/support/<ticket_type>', methods=['GET', 'POST'])
|
||||
@main.route('/support/<ticket_type:ticket_type>', methods=['GET', 'POST'])
|
||||
def feedback(ticket_type):
|
||||
try:
|
||||
form = {
|
||||
QUESTION_TICKET_TYPE: Feedback,
|
||||
PROBLEM_TICKET_TYPE: Problem,
|
||||
}[ticket_type]()
|
||||
except KeyError:
|
||||
abort(404)
|
||||
form = FeedbackOrProblem()
|
||||
|
||||
if not form.feedback.data:
|
||||
form.feedback.data = session.pop('feedback_message', '')
|
||||
@@ -72,14 +84,14 @@ def feedback(ticket_type):
|
||||
severe = None
|
||||
|
||||
out_of_hours_emergency = all((
|
||||
ticket_type == PROBLEM_TICKET_TYPE,
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
not in_business_hours(),
|
||||
severe,
|
||||
))
|
||||
|
||||
if needs_triage(ticket_type, severe):
|
||||
session['feedback_message'] = form.feedback.data
|
||||
return redirect(url_for('.triage'))
|
||||
return redirect(url_for('.triage', ticket_type=ticket_type))
|
||||
|
||||
if needs_escalation(ticket_type, severe):
|
||||
return redirect(url_for('.bat_phone'))
|
||||
@@ -99,10 +111,9 @@ def feedback(ticket_type):
|
||||
else:
|
||||
service_string = ''
|
||||
|
||||
feedback_msg = '{}\n{}{}'.format(
|
||||
feedback_msg = '{}\n{}'.format(
|
||||
form.feedback.data,
|
||||
service_string,
|
||||
'' if user_email else '{} (no email address supplied)'.format(form.name.data)
|
||||
)
|
||||
|
||||
zendesk_client.create_ticket(
|
||||
@@ -121,13 +132,20 @@ def feedback(ticket_type):
|
||||
),
|
||||
))
|
||||
|
||||
if not form.feedback.data:
|
||||
form.feedback.data = get_prefilled_message()
|
||||
|
||||
return render_template(
|
||||
'views/support/{}.html'.format(ticket_type),
|
||||
'views/support/form.html',
|
||||
form=form,
|
||||
ticket_type=ticket_type,
|
||||
back_link=(
|
||||
url_for('.support')
|
||||
if severe is None else
|
||||
url_for('.triage', ticket_type=ticket_type)
|
||||
),
|
||||
show_status_page_banner=(ticket_type == PROBLEM_TICKET_TYPE),
|
||||
page_title={
|
||||
GENERAL_TICKET_TYPE: 'Contact GOV.UK Notify support',
|
||||
PROBLEM_TICKET_TYPE: 'Report a problem',
|
||||
QUESTION_TICKET_TYPE: 'Ask a question or give feedback',
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@@ -237,7 +255,7 @@ def has_live_services(user_id):
|
||||
|
||||
def needs_triage(ticket_type, severe):
|
||||
return all((
|
||||
ticket_type == PROBLEM_TICKET_TYPE,
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe is None,
|
||||
(
|
||||
not current_user.is_authenticated or has_live_services(current_user.id)
|
||||
@@ -248,7 +266,7 @@ def needs_triage(ticket_type, severe):
|
||||
|
||||
def needs_escalation(ticket_type, severe):
|
||||
return all((
|
||||
ticket_type == PROBLEM_TICKET_TYPE,
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe,
|
||||
not current_user.is_authenticated,
|
||||
not in_business_hours(),
|
||||
|
||||
@@ -15,12 +15,12 @@ from notifications_utils.template import HTMLEmailTemplate, LetterImageTemplate
|
||||
from app import email_branding_client, letter_branding_client, status_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import FieldWithNoneOption, SearchByNameForm
|
||||
from app.main.views.feedback import QUESTION_TICKET_TYPE
|
||||
from app.main.views.sub_navigation_dictionaries import (
|
||||
features_nav,
|
||||
pricing_nav,
|
||||
using_notify_nav,
|
||||
)
|
||||
from app.models.feedback import QUESTION_TICKET_TYPE
|
||||
from app.utils import get_logo_cdn_domain
|
||||
|
||||
|
||||
@@ -320,6 +320,14 @@ def get_started():
|
||||
)
|
||||
|
||||
|
||||
@main.route('/using-notify/who-its-for')
|
||||
def who_its_for():
|
||||
return render_template(
|
||||
'views/guidance/who-its-for.html',
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route('/trial-mode')
|
||||
@main.route('/features/trial-mode')
|
||||
def trial_mode():
|
||||
|
||||
+4
-20
@@ -42,26 +42,10 @@ from app.utils import (
|
||||
@main.route("/services/<uuid:service_id>/jobs")
|
||||
@user_has_permissions()
|
||||
def view_jobs(service_id):
|
||||
jobs = current_service.get_page_of_jobs(page=request.args.get('page'))
|
||||
|
||||
prev_page = None
|
||||
if jobs.prev_page:
|
||||
prev_page = generate_previous_dict('main.view_jobs', service_id, jobs.current_page)
|
||||
next_page = None
|
||||
if jobs.next_page:
|
||||
next_page = generate_next_dict('main.view_jobs', service_id, jobs.current_page)
|
||||
|
||||
return render_template(
|
||||
'views/jobs/jobs.html',
|
||||
jobs=jobs,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
show_scheduled_jobs=(
|
||||
jobs.current_page == 1
|
||||
and not current_user.has_permissions('view_activity')
|
||||
and current_service.scheduled_jobs
|
||||
),
|
||||
)
|
||||
return redirect(url_for(
|
||||
'main.uploads',
|
||||
service_id=current_service.id,
|
||||
))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>")
|
||||
|
||||
@@ -42,6 +42,14 @@ ZERO_FAILURE_THRESHOLD = 0
|
||||
|
||||
@main.route("/platform-admin")
|
||||
@user_is_platform_admin
|
||||
def platform_admin_splash_page():
|
||||
return render_template(
|
||||
'views/platform-admin/splash-page.html',
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/summary")
|
||||
@user_is_platform_admin
|
||||
def platform_admin():
|
||||
form = DateFilterForm(request.args, meta={'csrf': False})
|
||||
api_args = {}
|
||||
@@ -360,6 +368,9 @@ def platform_admin_returned_letters():
|
||||
|
||||
try:
|
||||
letter_jobs_client.submit_returned_letters(references)
|
||||
redis_client.delete_cache_keys_by_pattern(
|
||||
'service-????????-????-????-????-????????????-returned-letters-statistics'
|
||||
)
|
||||
redis_client.delete_cache_keys_by_pattern(
|
||||
'service-????????-????-????-????-????????????-returned-letters-summary'
|
||||
)
|
||||
@@ -397,6 +408,8 @@ def clear_cache():
|
||||
'service-????????-????-????-????-????????????-templates',
|
||||
'service-????????-????-????-????-????????????-data-retention',
|
||||
'service-????????-????-????-????-????????????-template-folders',
|
||||
'service-????????-????-????-????-????????????-returned-letters-statistics',
|
||||
'service-????????-????-????-????-????????????-returned-letters-summary',
|
||||
]),
|
||||
('template', [
|
||||
'service-????????-????-????-????-????????????-templates',
|
||||
|
||||
+118
-2
@@ -39,9 +39,11 @@ from app.main import main, no_cookie
|
||||
from app.main.forms import (
|
||||
ChooseTimeForm,
|
||||
CsvUploadForm,
|
||||
LetterAddressForm,
|
||||
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 (
|
||||
s3download,
|
||||
@@ -151,7 +153,7 @@ def send_messages(service_id, template_id):
|
||||
try:
|
||||
upload_id = s3upload(
|
||||
service_id,
|
||||
Spreadsheet.from_file(form.file.data, filename=form.file.data.filename).as_dict,
|
||||
Spreadsheet.from_file_form(form).as_dict,
|
||||
current_app.config['AWS_REGION']
|
||||
)
|
||||
return redirect(url_for(
|
||||
@@ -306,6 +308,11 @@ def send_test(service_id, template_id):
|
||||
return_to='view_template',
|
||||
template_id=template_id))
|
||||
|
||||
if db_template['template_type'] == 'letter':
|
||||
return redirect(
|
||||
url_for('.send_one_off_letter_address', service_id=service_id, template_id=template_id)
|
||||
)
|
||||
|
||||
return redirect(url_for(
|
||||
{
|
||||
'main.send_test': '.send_test_step',
|
||||
@@ -328,6 +335,72 @@ def get_notification_check_endpoint(service_id, template):
|
||||
))
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/send/<uuid:template_id>/one-off/address",
|
||||
methods=['GET', 'POST']
|
||||
)
|
||||
@user_has_permissions('send_messages', restrict_admin_usage=True)
|
||||
def send_one_off_letter_address(service_id, template_id):
|
||||
if {'recipient', 'placeholders'} - set(session.keys()):
|
||||
# if someone has come here via a bookmark or back button they might have some stuff still in their session
|
||||
return redirect(url_for('.send_one_off', service_id=service_id, template_id=template_id))
|
||||
|
||||
db_template = current_service.get_template_with_user_permission_or_403(template_id, current_user)
|
||||
|
||||
session['send_test_letter_page_count'] = get_page_count_for_letter(db_template)
|
||||
|
||||
template = get_template(
|
||||
db_template,
|
||||
current_service,
|
||||
show_recipient=True,
|
||||
letter_preview_url=url_for(
|
||||
'no_cookie.send_test_preview',
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
filetype='png',
|
||||
),
|
||||
page_count=session['send_test_letter_page_count'],
|
||||
email_reply_to=None,
|
||||
sms_sender=None
|
||||
)
|
||||
|
||||
form = LetterAddressForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
session['placeholders'].update(form.as_address_lines_1_to_7_with_postcode)
|
||||
|
||||
placeholders = fields_to_fill_in(
|
||||
template,
|
||||
prefill_current_user=(request.endpoint == 'main.send_test_step'),
|
||||
)
|
||||
if all_placeholders_in_session(placeholders):
|
||||
return get_notification_check_endpoint(service_id, template)
|
||||
|
||||
first_non_address_placeholder_index = len(first_column_headings['letter'])
|
||||
return redirect(url_for(
|
||||
'main.send_one_off_step',
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=first_non_address_placeholder_index,
|
||||
))
|
||||
|
||||
return render_template(
|
||||
'views/send-one-off-letter-address.html',
|
||||
page_title=get_send_test_page_title(
|
||||
template_type='letter',
|
||||
help_argument=None,
|
||||
entering_recipient=True,
|
||||
name=template.name,
|
||||
),
|
||||
template=template,
|
||||
form=form,
|
||||
optional_placeholder=False,
|
||||
back_link=get_back_link(service_id, template, 0),
|
||||
help=False,
|
||||
link_to_upload=True,
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/send/<uuid:template_id>/test/step-<int:step_index>",
|
||||
methods=['GET', 'POST'],
|
||||
@@ -491,6 +564,48 @@ def send_test_preview(service_id, template_id, filetype):
|
||||
return TemplatePreview.from_utils_template(template, filetype, page=request.args.get('page'))
|
||||
|
||||
|
||||
@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(),
|
||||
original_file_name=contact_list.original_file_name,
|
||||
contact_list_id=contact_list.id,
|
||||
))
|
||||
|
||||
|
||||
def _check_messages(service_id, template_id, upload_id, preview_row, letters_as_pdf=False):
|
||||
|
||||
try:
|
||||
@@ -700,7 +815,8 @@ def start_job(service_id, upload_id):
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=request.form.get('scheduled_for', '')
|
||||
scheduled_for=request.form.get('scheduled_for', ''),
|
||||
contact_list_id=request.form.get('contact_list_id', ''),
|
||||
)
|
||||
|
||||
session.pop('sender_id', None)
|
||||
|
||||
@@ -391,9 +391,8 @@ def service_add_email_reply_to(service_id):
|
||||
service_id, form.email_address.data
|
||||
)["data"]["id"]
|
||||
except HTTPError as e:
|
||||
error_msg = "Your service already uses '{}' as an email reply-to address.".format(form.email_address.data)
|
||||
if e.status_code == 400 and error_msg == e.message:
|
||||
flash(error_msg, 'error')
|
||||
if e.status_code == 409 or e.status_code == 400:
|
||||
flash(e.message, 'error')
|
||||
return redirect(url_for('.service_email_reply_to', service_id=service_id))
|
||||
else:
|
||||
raise e
|
||||
@@ -519,9 +518,8 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
service_id, form.email_address.data
|
||||
)["data"]["id"]
|
||||
except HTTPError as e:
|
||||
error_msg = "Your service already uses ‘{}’ as a reply-to email address.".format(form.email_address.data)
|
||||
if e.status_code == 400 and error_msg == e.message:
|
||||
flash(error_msg, 'error')
|
||||
if e.status_code == 400 or e.status_code == 409:
|
||||
flash(e.message, 'error')
|
||||
return redirect(url_for('.service_email_reply_to', service_id=service_id))
|
||||
else:
|
||||
raise e
|
||||
|
||||
@@ -52,6 +52,10 @@ def using_notify_nav():
|
||||
"name": "Get started",
|
||||
"link": "main.get_started",
|
||||
},
|
||||
{
|
||||
"name": "Who it’s for",
|
||||
"link": "main.who_its_for",
|
||||
},
|
||||
{
|
||||
"name": "Trial mode",
|
||||
"link": "main.trial_mode_new",
|
||||
|
||||
@@ -111,7 +111,5 @@ def redirect_when_logged_in(platform_admin):
|
||||
next_url = request.args.get('next')
|
||||
if next_url and _is_safe_redirect_url(next_url):
|
||||
return redirect(next_url)
|
||||
if platform_admin:
|
||||
return redirect(url_for('main.platform_admin'))
|
||||
|
||||
return redirect(url_for('main.show_accounts_or_dashboard'))
|
||||
|
||||
+195
-2
@@ -1,25 +1,35 @@
|
||||
import base64
|
||||
import itertools
|
||||
import json
|
||||
import urllib
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
url_for,
|
||||
)
|
||||
from notifications_utils.columns import Columns
|
||||
from notifications_utils.pdf import pdf_page_count
|
||||
from notifications_utils.recipients import RecipientCSV
|
||||
from notifications_utils.sanitise_text import SanitiseASCII
|
||||
from PyPDF2.utils import PdfReadError
|
||||
from requests import RequestException
|
||||
from xlrd.biffh import XLRDError
|
||||
from xlrd.xldate import XLDateError
|
||||
|
||||
from app import current_service, notification_api_client, service_api_client
|
||||
from app.extensions import antivirus_client
|
||||
from app.main import main
|
||||
from app.main.forms import LetterUploadPostageForm, PDFUploadForm
|
||||
from app.main.forms import CsvUploadForm, LetterUploadPostageForm, PDFUploadForm
|
||||
from app.models.contact_list import ContactList
|
||||
from app.s3_client.s3_letter_upload_client import (
|
||||
get_letter_metadata,
|
||||
get_letter_pdf_and_metadata,
|
||||
@@ -28,10 +38,13 @@ from app.s3_client.s3_letter_upload_client import (
|
||||
)
|
||||
from app.template_previews import TemplatePreview, sanitise_letter
|
||||
from app.utils import (
|
||||
Spreadsheet,
|
||||
generate_next_dict,
|
||||
generate_previous_dict,
|
||||
get_errors_for_csv,
|
||||
get_letter_validation_error,
|
||||
get_template,
|
||||
unicode_truncate,
|
||||
user_has_permissions,
|
||||
)
|
||||
|
||||
@@ -53,7 +66,11 @@ def uploads(service_id):
|
||||
next_page = generate_next_dict('main.uploads', service_id, uploads.current_page)
|
||||
|
||||
if uploads.current_page == 1:
|
||||
listed_uploads = current_service.scheduled_jobs + uploads
|
||||
listed_uploads = (
|
||||
current_service.contact_lists +
|
||||
current_service.scheduled_jobs +
|
||||
uploads
|
||||
)
|
||||
else:
|
||||
listed_uploads = uploads
|
||||
|
||||
@@ -282,3 +299,179 @@ def send_uploaded_letter(service_id):
|
||||
service_id=service_id,
|
||||
notification_id=file_id,
|
||||
))
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
return redirect(url_for(
|
||||
'.check_contact_list',
|
||||
service_id=service_id,
|
||||
upload_id=upload_id,
|
||||
original_file_name=form.file.data.filename,
|
||||
))
|
||||
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
|
||||
))
|
||||
|
||||
return render_template(
|
||||
'views/uploads/contact-list/upload.html',
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@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 ''
|
||||
|
||||
template_type = {
|
||||
'emailaddress': 'email',
|
||||
'phonenumber': 'sms',
|
||||
}.get(Columns.make_key(first_row))
|
||||
|
||||
original_file_name = SanitiseASCII.encode(request.args.get('original_file_name', ''))
|
||||
|
||||
recipients = RecipientCSV(
|
||||
contents,
|
||||
template_type=template_type or 'sms',
|
||||
whitelist=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,
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
if recipients.has_errors:
|
||||
return render_template(
|
||||
'views/uploads/contact-list/column-errors.html',
|
||||
recipients=recipients,
|
||||
original_file_name=original_file_name,
|
||||
form=form,
|
||||
)
|
||||
|
||||
metadata_kwargs = {
|
||||
'row_count': len(recipients),
|
||||
'valid': True,
|
||||
'original_file_name': unicode_truncate(
|
||||
original_file_name,
|
||||
1600,
|
||||
),
|
||||
'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):
|
||||
return render_template(
|
||||
'views/uploads/contact-list/contact-list.html',
|
||||
contact_list=ContactList.from_id(contact_list_id, service_id=service_id),
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
filename_or_fp=BytesIO(contact_list.contents.encode('utf-8')),
|
||||
attachment_filename=contact_list.saved_file_name,
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
from functools import partial
|
||||
from os import path
|
||||
|
||||
from flask import abort, current_app
|
||||
from notifications_utils.formatters import strip_whitespace
|
||||
from notifications_utils.recipients import RecipientCSV
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.models import JSONModel, ModelList
|
||||
from app.notify_client.contact_list_api_client import contact_list_api_client
|
||||
from app.s3_client.s3_csv_client import (
|
||||
get_csv_metadata,
|
||||
s3download,
|
||||
s3upload,
|
||||
set_metadata_on_csv_upload,
|
||||
)
|
||||
|
||||
|
||||
class ContactList(JSONModel):
|
||||
|
||||
ALLOWED_PROPERTIES = {
|
||||
'id',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'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_name():
|
||||
return current_app.config['CONTACT_LIST_UPLOAD_BUCKET_NAME']
|
||||
|
||||
@staticmethod
|
||||
def upload(service_id, file_dict):
|
||||
return s3upload(
|
||||
service_id,
|
||||
file_dict,
|
||||
current_app.config['AWS_REGION'],
|
||||
bucket=ContactList.get_bucket_name(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def download(service_id, upload_id):
|
||||
return strip_whitespace(s3download(
|
||||
service_id,
|
||||
upload_id,
|
||||
bucket=ContactList.get_bucket_name(),
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def set_metadata(service_id, upload_id, **kwargs):
|
||||
return set_metadata_on_csv_upload(
|
||||
service_id,
|
||||
upload_id,
|
||||
bucket=ContactList.get_bucket_name(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_metadata(service_id, upload_id):
|
||||
return get_csv_metadata(
|
||||
service_id,
|
||||
upload_id,
|
||||
bucket=ContactList.get_bucket_name(),
|
||||
)
|
||||
|
||||
def copy_to_uploads(self):
|
||||
metadata = self.get_metadata(self.service_id, self.id)
|
||||
new_upload_id = s3upload(
|
||||
self.service_id,
|
||||
{'data': self.contents},
|
||||
current_app.config['AWS_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_type=self.template_type,
|
||||
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'
|
||||
|
||||
|
||||
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(),
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
QUESTION_TICKET_TYPE = 'ask-question-give-feedback'
|
||||
PROBLEM_TICKET_TYPE = 'report-problem'
|
||||
GENERAL_TICKET_TYPE = 'general'
|
||||
+17
-25
@@ -1,11 +1,8 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.parser import parse
|
||||
from flask import abort, current_app
|
||||
from notifications_utils.timezones import local_timezone
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.models import JSONModel
|
||||
from app.models.contact_list import ContactLists
|
||||
from app.models.job import (
|
||||
ImmediateJobs,
|
||||
PaginatedJobs,
|
||||
@@ -497,10 +494,6 @@ class Service(JSONModel):
|
||||
key=lambda folder: folder['name'].lower(),
|
||||
)
|
||||
|
||||
@property
|
||||
def can_upload_letters(self):
|
||||
return self.has_permission('letter') and self.has_permission('upload_letters')
|
||||
|
||||
@cached_property
|
||||
def all_template_folder_ids(self):
|
||||
return {folder['id'] for folder in self.all_template_folders}
|
||||
@@ -671,27 +664,26 @@ class Service(JSONModel):
|
||||
if test:
|
||||
yield BASE + '_incomplete' + tag
|
||||
|
||||
@cached_property
|
||||
def returned_letter_statistics(self):
|
||||
return service_api_client.get_returned_letter_statistics(self.id)
|
||||
|
||||
@cached_property
|
||||
def returned_letter_summary(self):
|
||||
return service_api_client.get_returned_letter_summary(self.id)
|
||||
|
||||
@property
|
||||
def most_recent_returned_letter_report(self):
|
||||
if not self.returned_letter_summary:
|
||||
return None
|
||||
return parse(
|
||||
self.returned_letter_summary[0]['reported_at'] + " 00:00:00"
|
||||
).replace(tzinfo=local_timezone)
|
||||
def count_of_returned_letters_in_last_7_days(self):
|
||||
return self.returned_letter_statistics['returned_letter_count']
|
||||
|
||||
@property
|
||||
def count_of_returned_letters_in_last_7_days(self):
|
||||
seven_days_ago = (
|
||||
datetime.now() - timedelta(days=7)
|
||||
).replace(
|
||||
hour=0, minute=0, second=0
|
||||
)
|
||||
return sum(
|
||||
report['returned_letter_count']
|
||||
for report in self.returned_letter_summary
|
||||
if parse(report['reported_at'] + " 00:00:00") >= seven_days_ago
|
||||
)
|
||||
def date_of_most_recent_returned_letter_report(self):
|
||||
return self.returned_letter_statistics['most_recent_report']
|
||||
|
||||
@property
|
||||
def has_returned_letters(self):
|
||||
return bool(self.date_of_most_recent_returned_letter_report)
|
||||
|
||||
@property
|
||||
def contact_lists(self):
|
||||
return ContactLists(self.id)
|
||||
|
||||
@@ -47,6 +47,7 @@ class HeaderNavigation(Navigation):
|
||||
'bat_phone',
|
||||
'feedback',
|
||||
'support',
|
||||
'support_public',
|
||||
'thanks',
|
||||
'triage',
|
||||
},
|
||||
@@ -101,6 +102,7 @@ class HeaderNavigation(Navigation):
|
||||
'platform_admin_list_complaints',
|
||||
'platform_admin_reports',
|
||||
'platform_admin_returned_letters',
|
||||
'platform_admin_splash_page',
|
||||
'suspend_service',
|
||||
'trial_services',
|
||||
'update_email_branding',
|
||||
@@ -149,6 +151,7 @@ class HeaderNavigation(Navigation):
|
||||
'check_notification',
|
||||
'no_cookie.check_notification_preview',
|
||||
'choose_account',
|
||||
'choose_from_contact_list',
|
||||
'choose_service',
|
||||
'choose_template',
|
||||
'choose_template_to_copy',
|
||||
@@ -169,6 +172,7 @@ class HeaderNavigation(Navigation):
|
||||
'delivery_and_failure',
|
||||
'delivery_status_callback',
|
||||
'design_content',
|
||||
'download_contact_list',
|
||||
'download_notifications_csv',
|
||||
'edit_data_retention',
|
||||
'edit_organisation_agreement',
|
||||
@@ -255,10 +259,12 @@ class HeaderNavigation(Navigation):
|
||||
'send_messages',
|
||||
'send_notification',
|
||||
'send_one_off',
|
||||
'send_one_off_letter_address',
|
||||
'send_one_off_step',
|
||||
'send_test',
|
||||
'no_cookie.send_test_preview',
|
||||
'send_test_step',
|
||||
'send_from_contact_list',
|
||||
'send_uploaded_letter',
|
||||
'service_add_email_reply_to',
|
||||
'service_add_letter_contact',
|
||||
@@ -317,6 +323,11 @@ class HeaderNavigation(Navigation):
|
||||
'template_history',
|
||||
'template_usage',
|
||||
'trial_mode',
|
||||
'upload_contact_list',
|
||||
'check_contact_list',
|
||||
'save_contact_list',
|
||||
'contact_list',
|
||||
'delete_contact_list',
|
||||
'upload_letter',
|
||||
'uploaded_letter_preview',
|
||||
'uploads',
|
||||
@@ -337,6 +348,7 @@ class HeaderNavigation(Navigation):
|
||||
'no_cookie.view_template_version_preview',
|
||||
'view_template_versions',
|
||||
'whitelist',
|
||||
'who_its_for',
|
||||
}
|
||||
|
||||
# header HTML now comes from GOVUK Frontend so requires a boolean, not an attribute
|
||||
@@ -363,6 +375,7 @@ class MainNavigation(Navigation):
|
||||
'add_service_template',
|
||||
'check_messages',
|
||||
'check_notification',
|
||||
'choose_from_contact_list',
|
||||
'choose_template',
|
||||
'choose_template_to_copy',
|
||||
'confirm_redact_template',
|
||||
@@ -374,6 +387,7 @@ class MainNavigation(Navigation):
|
||||
'manage_template_folder',
|
||||
'send_messages',
|
||||
'send_one_off',
|
||||
'send_one_off_letter_address',
|
||||
'send_one_off_step',
|
||||
'send_test',
|
||||
'no_cookie.send_test_preview',
|
||||
@@ -385,6 +399,11 @@ class MainNavigation(Navigation):
|
||||
'view_template_versions',
|
||||
},
|
||||
'uploads': {
|
||||
'upload_contact_list',
|
||||
'check_contact_list',
|
||||
'save_contact_list',
|
||||
'contact_list',
|
||||
'delete_contact_list',
|
||||
'upload_letter',
|
||||
'uploaded_letter_preview',
|
||||
'uploads',
|
||||
@@ -495,6 +514,7 @@ class MainNavigation(Navigation):
|
||||
'delivery_and_failure',
|
||||
'design_content',
|
||||
'documentation',
|
||||
'download_contact_list',
|
||||
'download_notifications_csv',
|
||||
'edit_data_retention',
|
||||
'edit_organisation_agreement',
|
||||
@@ -567,6 +587,7 @@ class MainNavigation(Navigation):
|
||||
'platform_admin_list_complaints',
|
||||
'platform_admin_reports',
|
||||
'platform_admin_returned_letters',
|
||||
'platform_admin_splash_page',
|
||||
'pricing',
|
||||
'privacy',
|
||||
'public_agreement',
|
||||
@@ -586,6 +607,7 @@ class MainNavigation(Navigation):
|
||||
'robots',
|
||||
'security',
|
||||
'send_notification',
|
||||
'send_from_contact_list',
|
||||
'send_uploaded_letter',
|
||||
'service_dashboard_updates',
|
||||
'service_delete_email_reply_to',
|
||||
@@ -603,6 +625,7 @@ class MainNavigation(Navigation):
|
||||
'start_tour',
|
||||
'styleguide',
|
||||
'support',
|
||||
'support_public',
|
||||
'suspend_service',
|
||||
'template_history',
|
||||
'terms',
|
||||
@@ -641,6 +664,7 @@ class MainNavigation(Navigation):
|
||||
'view_provider',
|
||||
'view_providers',
|
||||
'no_cookie.view_template_version_preview',
|
||||
'who_its_for',
|
||||
}
|
||||
|
||||
|
||||
@@ -648,8 +672,10 @@ class CaseworkNavigation(Navigation):
|
||||
|
||||
mapping = {
|
||||
'send-one-off': {
|
||||
'choose_from_contact_list',
|
||||
'choose_template',
|
||||
'send_one_off',
|
||||
'send_one_off_letter_address',
|
||||
'send_one_off_step',
|
||||
'send_test',
|
||||
'send_test_step',
|
||||
@@ -661,6 +687,11 @@ class CaseworkNavigation(Navigation):
|
||||
'uploads': {
|
||||
'view_jobs',
|
||||
'view_job',
|
||||
'upload_contact_list',
|
||||
'check_contact_list',
|
||||
'save_contact_list',
|
||||
'contact_list',
|
||||
'delete_contact_list',
|
||||
'upload_letter',
|
||||
'uploaded_letter_preview',
|
||||
'uploads',
|
||||
@@ -727,6 +758,7 @@ class CaseworkNavigation(Navigation):
|
||||
'delivery_status_callback',
|
||||
'design_content',
|
||||
'documentation',
|
||||
'download_contact_list',
|
||||
'download_notifications_csv',
|
||||
'edit_data_retention',
|
||||
'edit_organisation_agreement',
|
||||
@@ -808,6 +840,7 @@ class CaseworkNavigation(Navigation):
|
||||
'platform_admin_reports',
|
||||
'platform_admin_returned_letters',
|
||||
'platform_admin',
|
||||
'platform_admin_splash_page',
|
||||
'pricing',
|
||||
'privacy',
|
||||
'public_agreement',
|
||||
@@ -835,6 +868,7 @@ class CaseworkNavigation(Navigation):
|
||||
'send_messages',
|
||||
'send_notification',
|
||||
'no_cookie.send_test_preview',
|
||||
'send_from_contact_list',
|
||||
'send_uploaded_letter',
|
||||
'service_add_email_reply_to',
|
||||
'service_add_letter_contact',
|
||||
@@ -892,6 +926,7 @@ class CaseworkNavigation(Navigation):
|
||||
'styleguide',
|
||||
'submit_request_to_go_live',
|
||||
'support',
|
||||
'support_public',
|
||||
'suspend_service',
|
||||
'template_history',
|
||||
'template_usage',
|
||||
@@ -936,6 +971,7 @@ class CaseworkNavigation(Navigation):
|
||||
'no_cookie.view_template_version_preview',
|
||||
'view_template_versions',
|
||||
'whitelist',
|
||||
'who_its_for',
|
||||
}
|
||||
|
||||
|
||||
@@ -1003,6 +1039,7 @@ class OrgNavigation(Navigation):
|
||||
'check_notification',
|
||||
'no_cookie.check_notification_preview',
|
||||
'choose_account',
|
||||
'choose_from_contact_list',
|
||||
'choose_service',
|
||||
'choose_template',
|
||||
'choose_template_to_copy',
|
||||
@@ -1026,6 +1063,7 @@ class OrgNavigation(Navigation):
|
||||
'delivery_status_callback',
|
||||
'design_content',
|
||||
'documentation',
|
||||
'download_contact_list',
|
||||
'download_notifications_csv',
|
||||
'edit_data_retention',
|
||||
'edit_provider',
|
||||
@@ -1094,6 +1132,7 @@ class OrgNavigation(Navigation):
|
||||
'platform_admin_list_complaints',
|
||||
'platform_admin_reports',
|
||||
'platform_admin_returned_letters',
|
||||
'platform_admin_splash_page',
|
||||
'pricing',
|
||||
'privacy',
|
||||
'public_agreement',
|
||||
@@ -1120,10 +1159,12 @@ class OrgNavigation(Navigation):
|
||||
'send_messages',
|
||||
'send_notification',
|
||||
'send_one_off',
|
||||
'send_one_off_letter_address',
|
||||
'send_one_off_step',
|
||||
'send_test',
|
||||
'no_cookie.send_test_preview',
|
||||
'send_test_step',
|
||||
'send_from_contact_list',
|
||||
'send_uploaded_letter',
|
||||
'service_add_email_reply_to',
|
||||
'service_add_letter_contact',
|
||||
@@ -1181,6 +1222,7 @@ class OrgNavigation(Navigation):
|
||||
'styleguide',
|
||||
'submit_request_to_go_live',
|
||||
'support',
|
||||
'support_public',
|
||||
'suspend_service',
|
||||
'template_history',
|
||||
'template_usage',
|
||||
@@ -1195,6 +1237,11 @@ class OrgNavigation(Navigation):
|
||||
'two_factor_email_sent',
|
||||
'update_email_branding',
|
||||
'update_letter_branding',
|
||||
'upload_contact_list',
|
||||
'check_contact_list',
|
||||
'save_contact_list',
|
||||
'contact_list',
|
||||
'delete_contact_list',
|
||||
'upload_letter',
|
||||
'uploaded_letter_preview',
|
||||
'uploads',
|
||||
@@ -1232,4 +1279,5 @@ class OrgNavigation(Navigation):
|
||||
'no_cookie.view_template_version_preview',
|
||||
'view_template_versions',
|
||||
'whitelist',
|
||||
'who_its_for',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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()
|
||||
@@ -78,12 +78,15 @@ 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):
|
||||
def create_job(self, job_id, service_id, scheduled_for=None, contact_list_id=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)
|
||||
|
||||
|
||||
@@ -569,6 +569,10 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
def get_service_data_retention(self, service_id):
|
||||
return self.get("/service/{}/data-retention".format(service_id))
|
||||
|
||||
@cache.set('service-{service_id}-returned-letters-statistics')
|
||||
def get_returned_letter_statistics(self, service_id):
|
||||
return self.get("service/{}/returned-letter-statistics".format(service_id))
|
||||
|
||||
@cache.set('service-{service_id}-returned-letters-summary')
|
||||
def get_returned_letter_summary(self, service_id):
|
||||
return self.get("service/{}/returned-letter-summary".format(service_id))
|
||||
|
||||
@@ -9,20 +9,20 @@ from app.s3_client.s3_logo_client import get_s3_object
|
||||
FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv'
|
||||
|
||||
|
||||
def get_csv_location(service_id, upload_id):
|
||||
def get_csv_location(service_id, upload_id, bucket=None):
|
||||
return (
|
||||
current_app.config['CSV_UPLOAD_BUCKET_NAME'],
|
||||
bucket or current_app.config['CSV_UPLOAD_BUCKET_NAME'],
|
||||
FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
|
||||
)
|
||||
|
||||
|
||||
def get_csv_upload(service_id, upload_id):
|
||||
return get_s3_object(*get_csv_location(service_id, upload_id))
|
||||
def get_csv_upload(service_id, upload_id, bucket=None):
|
||||
return get_s3_object(*get_csv_location(service_id, upload_id, bucket))
|
||||
|
||||
|
||||
def s3upload(service_id, filedata, region):
|
||||
def s3upload(service_id, filedata, region, bucket=None):
|
||||
upload_id = str(uuid.uuid4())
|
||||
bucket_name, file_location = get_csv_location(service_id, upload_id)
|
||||
bucket_name, file_location = get_csv_location(service_id, upload_id, bucket)
|
||||
utils_s3upload(
|
||||
filedata=filedata['data'],
|
||||
region=region,
|
||||
@@ -32,10 +32,10 @@ def s3upload(service_id, filedata, region):
|
||||
return upload_id
|
||||
|
||||
|
||||
def s3download(service_id, upload_id):
|
||||
def s3download(service_id, upload_id, bucket=None):
|
||||
contents = ''
|
||||
try:
|
||||
key = get_csv_upload(service_id, upload_id)
|
||||
key = get_csv_upload(service_id, upload_id, bucket)
|
||||
contents = key.get()['Body'].read().decode('utf-8')
|
||||
except botocore.exceptions.ClientError as e:
|
||||
current_app.logger.error("Unable to download s3 file {}".format(
|
||||
@@ -44,14 +44,33 @@ def s3download(service_id, upload_id):
|
||||
return contents
|
||||
|
||||
|
||||
def set_metadata_on_csv_upload(service_id, upload_id, **kwargs):
|
||||
def set_metadata_on_csv_upload(service_id, upload_id, bucket=None, **kwargs):
|
||||
get_csv_upload(
|
||||
service_id, upload_id
|
||||
service_id, upload_id, bucket=bucket
|
||||
).copy_from(
|
||||
CopySource='{}/{}'.format(*get_csv_location(service_id, upload_id)),
|
||||
CopySource='{}/{}'.format(*get_csv_location(service_id, upload_id, bucket=bucket)),
|
||||
ServerSideEncryption='AES256',
|
||||
Metadata={
|
||||
key: str(value) for key, value in kwargs.items()
|
||||
},
|
||||
MetadataDirective='REPLACE',
|
||||
)
|
||||
|
||||
|
||||
def set_metadata_on_contact_list(service_id, upload_id, **kwargs):
|
||||
return set_metadata_on_csv_upload(
|
||||
service_id,
|
||||
upload_id,
|
||||
bucket=current_app.config['CONTACT_LIST_UPLOAD_BUCKET_NAME'],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def get_csv_metadata(service_id, upload_id, bucket=None):
|
||||
try:
|
||||
key = get_csv_upload(service_id, upload_id, bucket)
|
||||
return key.get()['Metadata']
|
||||
except botocore.exceptions.ClientError as e:
|
||||
current_app.logger.error("Unable to download s3 file {}".format(
|
||||
FILE_LOCATION_STRUCTURE.format(service_id, upload_id)))
|
||||
raise e
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"active": header_navigation.is_selected('user-profile')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.platform_admin'),
|
||||
"href": url_for('main.platform_admin_splash_page'),
|
||||
"text": "Platform admin",
|
||||
"active": header_navigation.is_selected('platform-admin')
|
||||
},
|
||||
@@ -217,6 +217,10 @@
|
||||
"href": url_for("main.get_started"),
|
||||
"text": "Get started"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.who_its_for"),
|
||||
"text": "Who it’s for",
|
||||
},
|
||||
{
|
||||
"href": url_for("main.trial_mode_new"),
|
||||
"text": "Trial mode"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro ajax_block(partials, url, key, interval=2, finished=False, form='') %}
|
||||
{% macro ajax_block(partials, url, key, interval=5, finished=False, form='') %}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
|
||||
@@ -8,14 +8,10 @@
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
|
||||
{% endif %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('templates') }}" href="{{ url_for('.choose_template', service_id=current_service.id) }}">Templates</a></li>
|
||||
{% if current_user.has_permissions('view_activity') %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('uploads') }}" href="{{ url_for('main.uploads', service_id=current_service.id) }}">Uploads</a></li>
|
||||
{% else %}
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% if current_service.has_jobs or current_service.can_upload_letters %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ casework_navigation.is_selected('uploads') }}" href="{{ url_for('main.uploads', service_id=current_service.id) }}">Uploads</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('uploads') }}" href="{{ url_for('main.uploads', service_id=current_service.id) }}">Uploads</a></li>
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
<li><a class="govuk-link govuk-link--no-visited-state{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id, original_file_name=original_file_name)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="help" value="{{ '3' if help else 0 }}" />
|
||||
<input type="hidden" name="contact_list_id" value="{{ request.args.get('contact_list_id', '') }}" />
|
||||
{% if choose_time_form and template.template_type != 'letter' %}
|
||||
{{ radio_select(
|
||||
choose_time_form.scheduled_for,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_service.returned_letter_summary %}
|
||||
{% if current_service.has_returned_letters %}
|
||||
<a id="total-returned-letters" class="govuk-link govuk-link--no-visited-state banner-dashboard" href="{{ url_for('main.returned_letter_summary', service_id=current_service.id) }}">
|
||||
<span class="banner-dashboard-count">
|
||||
{{ current_service.count_of_returned_letters_in_last_7_days|format_thousands }}
|
||||
@@ -26,7 +26,7 @@
|
||||
returned {{ message_count_label(current_service.count_of_returned_letters_in_last_7_days, 'letter', suffix='') }}
|
||||
</span>
|
||||
<span class="banner-dashboard-meta">
|
||||
latest report {{ current_service.most_recent_returned_letter_report|format_delta_days }}
|
||||
latest report {{ current_service.date_of_most_recent_returned_letter_report|format_delta_days }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/big-number.html" import big_number -%}
|
||||
{% from "components/message-count-label.html" import message_count_label -%}
|
||||
{% from "components/message-count-label.html" import message_count_label, recipient_count_label -%}
|
||||
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
@@ -20,17 +20,25 @@
|
||||
<div class="file-list">
|
||||
{% if item.upload_type == 'letter' %}
|
||||
<a class="file-list-filename-large govuk-link govuk-link--no-visited-state" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||
{% elif 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 %}
|
||||
{% if item.scheduled %}
|
||||
<span class="file-list-hint">
|
||||
<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">
|
||||
Uploaded {{
|
||||
item.created_at|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="file-list-hint">
|
||||
<span class="file-list-hint-large">
|
||||
Sent {{
|
||||
(item.scheduled_for or item.created_at)|format_datetime_relative
|
||||
}}
|
||||
@@ -60,8 +68,17 @@
|
||||
suffix=''
|
||||
)
|
||||
) }}
|
||||
{% elif item.upload_type == 'contact_list' %}
|
||||
{{ big_number(
|
||||
item.row_count,
|
||||
smallest=True,
|
||||
label="saved {}".format(recipient_count_label(
|
||||
item.row_count,
|
||||
item.template_type
|
||||
))
|
||||
) }}
|
||||
{% elif item.pdf_letter %}
|
||||
<p class="govuk-body govuk-!-margin-bottom-1">
|
||||
<p class="govuk-body letter-recipient-summary">
|
||||
{% for line in item.recipient.splitlines() %}
|
||||
{% if loop.index < 3 %}
|
||||
{{ line }}<br>
|
||||
|
||||
@@ -19,33 +19,25 @@
|
||||
{% include 'views/dashboard/write-first-messages.html' %}
|
||||
{% endif %}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'upcoming', interval=5) }}
|
||||
{{ ajax_block(partials, updates_url, 'upcoming', interval=20) }}
|
||||
|
||||
<h2 class="heading-medium">
|
||||
In the last 7 days
|
||||
</h2>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox', interval=5) }}
|
||||
{{ ajax_block(partials, updates_url, 'inbox', interval=20) }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'totals', interval=5) }}
|
||||
{{ ajax_block(partials, updates_url, 'totals', interval=20) }}
|
||||
{{ show_more(
|
||||
url_for('.monthly', service_id=current_service.id),
|
||||
'See messages sent per month'
|
||||
) }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics', interval=5) }}
|
||||
|
||||
{% if current_service.immediate_jobs and not current_service.has_permission('upload_letters') %}
|
||||
{{ ajax_block(partials, updates_url, 'jobs', interval=5) }}
|
||||
{{ show_more(
|
||||
url_for('.view_jobs', service_id=current_service.id),
|
||||
'See all uploaded files'
|
||||
) }}
|
||||
{% endif %}
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics', interval=20) }}
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
<h2 class='heading-medium'>This year</h2>
|
||||
{{ ajax_block(partials, updates_url, 'usage') }}
|
||||
{{ ajax_block(partials, updates_url, 'usage', interval=20) }}
|
||||
{{ show_more(
|
||||
url_for(".usage", service_id=current_service['id']),
|
||||
'See usage'
|
||||
|
||||
@@ -15,21 +15,9 @@
|
||||
<li class="get-started-list__item">
|
||||
<h2 class="get-started-list__heading">Check if GOV.UK Notify is right for you</h2>
|
||||
<p>Read about our <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.features') }}">features</a>, <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.pricing') }}">pricing</a> and <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
{{ govukDetails({
|
||||
"summaryText": "Organisations that can use Notify",
|
||||
"html": '''
|
||||
<div id="eligible-organisations">
|
||||
<p>Notify is available to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>central government departments</li>
|
||||
<li>local authorities</li>
|
||||
<li>state-funded schools</li>
|
||||
<li>the NHS</li>
|
||||
<li>companies running a service on behalf of a public sector organisation</li>
|
||||
</ul>
|
||||
<p>Notify is not currently available to charities.</p>
|
||||
</div>'''
|
||||
}) }}
|
||||
<p class="govuk-body">
|
||||
Check <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.who_its_for') }}">whether your organisation can use Notify</a>.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="get-started-list__item">
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Who it’s for
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
{{ page_header(
|
||||
'Who it’s for'
|
||||
) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
GOV.UK Notify is available to:
|
||||
</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>central government departments</li>
|
||||
<li>emergency services</li>
|
||||
<li>local authorities</li>
|
||||
<li>the armed forces</li>
|
||||
<li>the NHS and GP practices</li>
|
||||
<li>state-funded schools</li>
|
||||
</ul>
|
||||
<p class="govuk-body">
|
||||
Notify is not currently available to charities.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
If you work for one of these organisations but get an error when you try to create an account, <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.support') }}">contact support</a>.
|
||||
</p>
|
||||
|
||||
<h2 class="govuk-heading-m">Suppliers</h2>
|
||||
<p class="govuk-body">
|
||||
If you’re doing work for a public sector organisation you can use GOV.UK Notify.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
Someone from the public sector organisation you’re working with needs to set up the account. Then they can invite you as a team member.
|
||||
</p>
|
||||
|
||||
<h2 class="govuk-heading-m">Members of the public</h2>
|
||||
<p class="govuk-body">
|
||||
The GOV.UK Notify service is only for people who work in the government
|
||||
or other public sector organisations.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk">Find government services and information on GOV.UK</a>.
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -9,34 +9,38 @@
|
||||
{% block maincolumn_content %}
|
||||
<h1 class="heading-medium">Uploads</h1>
|
||||
<div class="dashboard">
|
||||
{% if show_scheduled_jobs %}
|
||||
{% with hide_heading = True %}
|
||||
{% include 'views/jobs/_scheduled.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if jobs %}
|
||||
{% include 'views/dashboard/_jobs.html' %}
|
||||
{% endif %}
|
||||
{% if not jobs and not show_scheduled_jobs %}
|
||||
{% else %}
|
||||
<p class="govuk-body">
|
||||
You have not uploaded any files recently.
|
||||
</p>
|
||||
{% if current_service.has_permission('upload_letters') %}
|
||||
{% if current_user.has_permissions('send_messages') %}
|
||||
{% if current_service.has_permission('letter') %}
|
||||
<p class="govuk-body">
|
||||
Upload a letter and Notify will print, pack and post it for you.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="govuk-body">
|
||||
Upload a letter and Notify will print, pack and post it for you.
|
||||
To upload a list of contact details, first <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.choose_template', service_id=current_service.id) }}">choose a template</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="govuk-body">
|
||||
To upload a list of contact details, first <a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.choose_template', service_id=current_service.id) }}">choose a template</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ previous_next_navigation(prev_page, next_page) }}
|
||||
{% if current_service.can_upload_letters and current_user.has_permissions('send_messages') %}
|
||||
{% if current_user.has_permissions('send_messages') %}
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{% if current_service.has_permission('letter') %}
|
||||
{{ govukButton({
|
||||
"element": "a",
|
||||
"text": "Upload a letter",
|
||||
"href": url_for('.upload_letter', service_id=current_service.id),
|
||||
"classes": "govuk-button--secondary govuk-!-margin-right-3"
|
||||
}) }}
|
||||
{% endif %}
|
||||
{{ govukButton({
|
||||
"element": "a",
|
||||
"text": "Upload a letter",
|
||||
"href": url_for('.upload_letter', service_id=current_service.id),
|
||||
"text": "Upload an emergency contact list",
|
||||
"href": url_for('.upload_contact_list', service_id=current_service.id),
|
||||
"classes": "govuk-button--secondary"
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
{{ ajax_block(
|
||||
partials,
|
||||
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status),
|
||||
'counts'
|
||||
'counts',
|
||||
interval=20
|
||||
) }}
|
||||
|
||||
{% call form_wrapper(
|
||||
@@ -82,7 +83,8 @@
|
||||
partials,
|
||||
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page),
|
||||
'notifications',
|
||||
form='search-form'
|
||||
form='search-form',
|
||||
interval=20
|
||||
) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -96,10 +96,10 @@
|
||||
</div>
|
||||
{% elif template.template_type == 'email' %}
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{{ ajax_block(partials, updates_url, 'status', finished=finished) }}
|
||||
{{ ajax_block(partials, updates_url, 'status', interval=2, finished=finished) }}
|
||||
</div>
|
||||
{% elif template.template_type == 'sms' %}
|
||||
{{ ajax_block(partials, updates_url, 'status', finished=finished) }}
|
||||
{{ ajax_block(partials, updates_url, 'status', interval=2, finished=finished) }}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has_permissions('send_messages') and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Summary
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
<h1 class="heading-large">
|
||||
Summary
|
||||
</h1>
|
||||
|
||||
<p class="govuk-body">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.platform_admin') }}">Load summary</a>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -14,7 +14,7 @@ Create an account
|
||||
<h1 class="heading-large">Create an account</h1>
|
||||
{% call form_wrapper(autocomplete=True) %}
|
||||
{{ textbox(form.name, width='3-4') }}
|
||||
{{ textbox(form.email_address, hint="Must be from a government organisation", width='3-4', safe_error_message=True, autocomplete='email') }}
|
||||
{{ textbox(form.email_address, hint="Must be from a public sector organisation", width='3-4', safe_error_message=True, autocomplete='email') }}
|
||||
<div class="extra-tracking">
|
||||
{{ textbox(form.mobile_number, width='3-4', hint='We’ll send you a security code by text message') }}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/big-number.html" import big_number -%}
|
||||
{% from "components/list.html" import list_of_placeholders %}
|
||||
{% from "components/message-count-label.html" import recipient_count_label %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Choose a saved contact list
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
'Choose a saved contact list',
|
||||
back_link=url_for('.send_one_off', service_id=current_service.id, template_id=template.id)
|
||||
) }}
|
||||
|
||||
{% if template.placeholders %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-five-sixths">
|
||||
<p class="govuk-body">
|
||||
You cannot use a saved contact list with this template because it
|
||||
is personalised with {{ list_of_placeholders(template.placeholders) }}.
|
||||
</p>
|
||||
<p>
|
||||
Saved contact lists can only store 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="Existing contact lists",
|
||||
caption_visible=False,
|
||||
empty_message=(
|
||||
'You don’t have 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=recipient_count_label(
|
||||
item.row_count,
|
||||
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 {{ recipient_count_label(99, template.template_type) }} yet.
|
||||
</p>
|
||||
<p class="govuk-body">
|
||||
To upload and save a new 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 %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/message-count-label.html" import recipient_count_label %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
page_title,
|
||||
back_link=back_link
|
||||
) }}
|
||||
|
||||
{% call form_wrapper(
|
||||
class='send-one-off-form',
|
||||
module="autofocus",
|
||||
data_kwargs={'force-focus': True}
|
||||
) %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
{{ textbox(
|
||||
form.address,
|
||||
rows=4,
|
||||
width='1-1',
|
||||
autofocus=True,
|
||||
autosize=True,
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">
|
||||
Upload a list of {{ recipient_count_label(999, template.template_type) }}
|
||||
</a>
|
||||
</p>
|
||||
{{ page_footer('Continue') }}
|
||||
{% endcall %}
|
||||
|
||||
{{ template|string }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -22,24 +22,30 @@
|
||||
data_kwargs={'force-focus': True}
|
||||
) %}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
<div class="govuk-grid-column-full {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
hint='Optional' if optional_placeholder else None,
|
||||
width='1-1',
|
||||
) }}
|
||||
</div>
|
||||
{% if skip_link %}
|
||||
<div class="govuk-grid-column-one-third">
|
||||
<a href="{{ skip_link[1] }}" class="govuk-link govuk-link--no-visited-state top-gutter-4-3">{{ skip_link[0] }}</a>
|
||||
{% if skip_link or link_to_upload %}
|
||||
<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 {{ recipient_count_label(999, 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 a saved 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if link_to_upload %}
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.send_messages', service_id=current_service.id, template_id=template.id) }}">
|
||||
Upload a list of {{ recipient_count_label(999, template.template_type) }}
|
||||
</a>
|
||||
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ page_footer('Continue') }}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/page-footer.html" import sticky_page_footer %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Ask a question or give feedback
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
'Ask a question or give feedback',
|
||||
back_link=url_for('.support')
|
||||
) }}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
{% call form_wrapper() %}
|
||||
{{ textbox(form.feedback, width='1-1', hint='', rows=10, autosize=True) }}
|
||||
{% if not current_user.is_authenticated %}
|
||||
<h3 class="heading-medium">Do you want a reply?</h3>
|
||||
<p>Leave your details below if you’d like a response.</p>
|
||||
{{ textbox(form.name, width='1-1') }}
|
||||
{{ textbox(form.email_address, width='1-1') }}
|
||||
{% else %}
|
||||
<p>We’ll reply to {{ current_user.email_address }}</p>
|
||||
{% endif %}
|
||||
{{ sticky_page_footer('Send') }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Out of hours emergencies
|
||||
@@ -8,9 +9,10 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="heading-large">
|
||||
Out of hours emergencies
|
||||
</h1>
|
||||
{{ page_header(
|
||||
'Out of hours emergencies',
|
||||
url_for('.support')
|
||||
)}}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
<p>
|
||||
@@ -36,9 +38,6 @@
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.feedback', ticket_type='report-problem', severe='no') }}">Fill in this form</a>
|
||||
and we’ll get back to you by the next working day.
|
||||
</p>
|
||||
<p>
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('.support') }}">Back to support</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+11
-9
@@ -6,23 +6,25 @@
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Report a problem
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
'Report a problem',
|
||||
back_link=url_for('.support')
|
||||
page_title,
|
||||
back_link=back_link
|
||||
) }}
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
<div class="panel panel-border-wide">
|
||||
<p>
|
||||
Check our <a class="govuk-link govuk-link--no-visited-state" href="https://status.notifications.service.gov.uk">system status</a>
|
||||
page to see if there are any known issues with GOV.UK Notify.
|
||||
</p>
|
||||
</div>
|
||||
{% if show_status_page_banner %}
|
||||
<div class="panel panel-border-wide">
|
||||
<p>
|
||||
Check our <a class="govuk-link govuk-link--no-visited-state" href="https://status.notifications.service.gov.uk">system status</a>
|
||||
page to see if there are any known issues with GOV.UK Notify.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% call form_wrapper() %}
|
||||
{{ textbox(form.feedback, width='1-1', hint='', rows=10, autosize=True) }}
|
||||
{% if not current_user.is_authenticated %}
|
||||
@@ -9,19 +9,25 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="heading-large">Support</h1>
|
||||
<p>We provide 24-hour online support for teams with a live service on GOV.UK Notify.</p>
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{{ radios(form.support_type) }}
|
||||
{% else %}
|
||||
<p class="govuk-body">
|
||||
What do you need help with?
|
||||
</p>
|
||||
{{ radios(form.who, hide_legend=True) }}
|
||||
{% endif %}
|
||||
{{ page_footer('Continue') }}
|
||||
{% endcall %}
|
||||
|
||||
<p>You can also <a class="govuk-link govuk-link--no-visited-state" href="https://ukgovernmentdigital.slack.com/messages/C0E1ADVPC">contact us on Slack</a>.</p>
|
||||
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
|
||||
<h1 class="heading-large">Support</h1>
|
||||
<p>We provide 24-hour online support for teams with a live service on GOV.UK Notify.</p>
|
||||
|
||||
{% call form_wrapper(class="bottom-gutter-2") %}
|
||||
{{ radios(form.support_type) }}
|
||||
{{ page_footer('Continue') }}
|
||||
{% endcall %}
|
||||
|
||||
<p>You can also <a class="govuk-link govuk-link--no-visited-state" href="https://ukgovernmentdigital.slack.com/messages/C0E1ADVPC">contact us on Slack</a>.</p>
|
||||
|
||||
<h2 class="heading-medium">Office hours</h2>
|
||||
<p>Our office hours are 9:30am to 5:30pm, Monday to Friday.</p>
|
||||
<p>When you report a problem in office hours, we’ll aim to read it within 30 minutes and reply within one working day.</p>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
|
||||
{% block per_page_title %}
|
||||
The GOV.UK Notify team can’t give advice to members of the public
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
|
||||
{{ page_header(
|
||||
'The GOV.UK Notify service is for people who work in the government',
|
||||
back_link=url_for('.support')
|
||||
) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
We can’t give advice to the public. We don’t have access to information about you held by government departments.
|
||||
</p>
|
||||
|
||||
<p class="govuk-body">
|
||||
There are other pages on GOV.UK where you can get help:
|
||||
</p>
|
||||
|
||||
<h2 class="govuk-heading-m govuk-!-margin-bottom-1">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/coronavirus">Coronavirus (COVID-19)</a>
|
||||
</h2>
|
||||
<p class="govuk-body">
|
||||
What you need to do
|
||||
</p>
|
||||
<h2 class="govuk-heading-m govuk-!-margin-bottom-1">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/contact">Contact the government</a>
|
||||
</h2>
|
||||
<p class="govuk-body">
|
||||
Ask about benefits, driving, transport, tax, and more
|
||||
</p>
|
||||
<h2 class="govuk-heading-m govuk-!-margin-bottom-1">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="https://www.gov.uk/report-suspicious-emails-websites-phishing">Avoid and report internet scams and phishing</a>
|
||||
</h2>
|
||||
<p class="govuk-body">
|
||||
Advice on suspicious emails and text messages
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -5,7 +5,7 @@
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Feedback
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="govuk-grid-row">
|
||||
<div class="govuk-grid-column-two-thirds">
|
||||
{{ page_header(
|
||||
'Report a problem',
|
||||
page_title,
|
||||
back_link=url_for('.support')
|
||||
) }}
|
||||
{% call form_wrapper() %}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
<div class="bottom-gutter-2-3">
|
||||
<div class="govuk-grid-row">
|
||||
{% if template.template_type == 'letter' %}
|
||||
{% if letter_too_long %}
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/letter-too-long.html" %}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% if letter_too_long %}
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/letter-too-long.html" %}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('send_messages', restrict_admin_usage=True) and not letter_too_long %}
|
||||
<div class="govuk-grid-column-one-half">
|
||||
<a href="{{ url_for(".set_sender", service_id=current_service.id, template_id=template.id) }}" class="govuk-link govuk-link--no-visited-state pill-separate-item">
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{% 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/back-link/macro.njk" import govukBackLink %}
|
||||
{% from "components/message-count-label.html" import message_count_label, recipient_count_label %}
|
||||
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
{% macro skip_to_file_contents() %}
|
||||
<p class="govuk-visually-hidden">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="#{{ file_contents_header_id }}">Skip to file contents</a>
|
||||
</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
|
||||
<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>
|
||||
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' }}
|
||||
{{ recipient_count_label(recipients|length, recipients.template_type) }}
|
||||
</h1>
|
||||
<p>
|
||||
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 %}
|
||||
|
||||
{{ skip_to_file_contents() }}
|
||||
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
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_contents_header_id }}">{{ 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 %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% 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/message-count-label.html" import message_count_label, recipient_count_label %}
|
||||
{% from "components/button/macro.njk" import govukButton %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ contact_list.original_file_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
contact_list.original_file_name,
|
||||
back_link=url_for('main.uploads', service_id=current_service.id)
|
||||
) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
Uploaded by {{ contact_list.created_by }} {{ contact_list.created_at|format_datetime_human }}
|
||||
</p>
|
||||
|
||||
<p class="govuk-body">
|
||||
<a class="govuk-link govuk-link--no-visited-state heading-small" download href="{{ url_for('main.download_contact_list', service_id=current_service.id, contact_list_id=contact_list.id) }}">Download this list</a> 
|
||||
{{ contact_list.recipients|length|format_thousands }}
|
||||
{{ recipient_count_label(contact_list.recipients|length, contact_list.recipients.template_type) }}
|
||||
</p>
|
||||
|
||||
{% set recipient_column = contact_list.recipients.column_headers[0] %}
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
contact_list.recipients.displayed_rows,
|
||||
caption=recipient_count_label(contact_list.recipients|length, contact_list.template_type)|capitalize,
|
||||
caption_visible=False,
|
||||
field_headings=['1', recipient_column],
|
||||
) %}
|
||||
{{ index_field(row_number) }}
|
||||
{{ text_field(item[recipient_column].data) }}
|
||||
{% 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 %}
|
||||
|
||||
{% if not confirm_delete_banner %}
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
<div class="page-footer">
|
||||
<span class="page-footer-delete-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>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% 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/message-count-label.html" import message_count_label, recipient_count_label %}
|
||||
{% from "components/button/macro.njk" import govukButton %}
|
||||
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
{% macro skip_to_file_contents() %}
|
||||
<p class="govuk-visually-hidden">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="#{{ file_contents_header_id }}">Skip to file contents</a>
|
||||
</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ original_file_name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(
|
||||
original_file_name,
|
||||
back_link=url_for('main.upload_contact_list', service_id=current_service.id)
|
||||
) }}
|
||||
|
||||
<p class="govuk-body">
|
||||
{{ recipients|length|format_thousands }} {{ recipient_count_label(recipients|length, 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 %}
|
||||
@@ -0,0 +1,108 @@
|
||||
{% 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/back-link/macro.njk" import govukBackLink %}
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
{% macro skip_to_file_contents() %}
|
||||
<p class="govuk-visually-hidden">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="#{{ file_contents_header_id }}">Skip to file contents</a>
|
||||
</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
|
||||
<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>
|
||||
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>
|
||||
You need to:
|
||||
</p>
|
||||
<ul class="list-bullet">
|
||||
{% for error in row_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{{ skip_to_file_contents() }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
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 %}
|
||||
@@ -0,0 +1,112 @@
|
||||
{% 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/back-link/macro.njk" import govukBackLink %}
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
{% macro skip_to_file_contents() %}
|
||||
<p class="govuk-visually-hidden">
|
||||
<a class="govuk-link govuk-link--no-visited-state" href="#{{ file_contents_header_id }}">Skip to file contents</a>
|
||||
</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ govukBackLink({ "href": url_for('main.upload_contact_list', service_id=current_service.id) }) }}
|
||||
|
||||
<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>
|
||||
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>
|
||||
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>
|
||||
It needs to have 1 column, called ‘email address’ or ‘phone number’.
|
||||
</p>
|
||||
<p>
|
||||
Right now it has {{ recipients._raw_column_headers|length }} columns called {{ recipients._raw_column_headers | formatted_list }}.
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{{ skip_to_file_contents() }}
|
||||
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{{ file_upload(
|
||||
form.file,
|
||||
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_contents_header_id }}">{{ 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 %}
|
||||
@@ -0,0 +1,91 @@
|
||||
{% 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 %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Upload an emergency contact list
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{% if error %}
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
<h1 class="banner-title">{{ error.title }}</h1>
|
||||
{% if error.detail %}
|
||||
<p>{{ error.detail | safe }}</p>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{{ page_header(
|
||||
'Upload an emergency contact list',
|
||||
back_link=url_for('main.uploads', service_id=current_service.id)
|
||||
) }}
|
||||
<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,
|
||||
button_text='Upload your file again' if error else '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.gov.uk'],
|
||||
],
|
||||
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'],
|
||||
['07700 900123'],
|
||||
],
|
||||
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 %}
|
||||
@@ -1,11 +1,22 @@
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from app.models.feedback import (
|
||||
GENERAL_TICKET_TYPE,
|
||||
PROBLEM_TICKET_TYPE,
|
||||
QUESTION_TICKET_TYPE,
|
||||
)
|
||||
|
||||
|
||||
class TemplateTypeConverter(BaseConverter):
|
||||
|
||||
regex = '(?:email|sms|letter)'
|
||||
|
||||
|
||||
class TicketTypeConverter(BaseConverter):
|
||||
|
||||
regex = f'(?:{PROBLEM_TICKET_TYPE}|{QUESTION_TICKET_TYPE}|{GENERAL_TICKET_TYPE})'
|
||||
|
||||
|
||||
class LetterFileExtensionConverter(BaseConverter):
|
||||
|
||||
regex = '(?:pdf|png)'
|
||||
|
||||
@@ -323,6 +323,13 @@ class Spreadsheet():
|
||||
pyexcel.free_resources()
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_file_form(cls, form):
|
||||
return cls.from_file(
|
||||
form.file.data,
|
||||
filename=form.file.data.filename,
|
||||
)
|
||||
|
||||
@property
|
||||
def as_rows(self):
|
||||
if not self._rows:
|
||||
|
||||
Reference in New Issue
Block a user