mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-20 22:40:31 -04:00
Merge branch 'master' into reinstate-new-rate-api
Conflicts: app/main/views/dashboard.py
This commit is contained in:
BIN
app/assets/images/email-template/crests/data_gov_uk_x2.png
Normal file
BIN
app/assets/images/email-template/crests/data_gov_uk_x2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -26,6 +26,19 @@
|
||||
@include grid-column(7/8);
|
||||
}
|
||||
|
||||
%top-gutter,
|
||||
.top-gutter {
|
||||
@extend %contain-floats;
|
||||
display: block;
|
||||
margin-top: $gutter;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.top-gutter-4-3 {
|
||||
@extend %top-gutter;
|
||||
margin-top: $gutter * 4 / 3;
|
||||
}
|
||||
|
||||
%bottom-gutter,
|
||||
.bottom-gutter {
|
||||
@extend %contain-floats;
|
||||
|
||||
@@ -129,3 +129,26 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.big-number-meta-wrapper {
|
||||
|
||||
position: relative;
|
||||
margin: $gutter-half 0 $gutter 0;
|
||||
background: $govuk-blue;
|
||||
|
||||
.big-number-meta {
|
||||
|
||||
padding: ($gutter / 3) $gutter-half;
|
||||
color: $white;
|
||||
pointer-events: none;
|
||||
|
||||
@include media(desktop) {
|
||||
position: absolute;
|
||||
bottom: 7px;
|
||||
right: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,3 +59,21 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.align-button-with-textbox {
|
||||
|
||||
.button {
|
||||
|
||||
@include media(desktop) {
|
||||
position: relative;
|
||||
top: 32px;
|
||||
left: -30px;
|
||||
width: 100%;
|
||||
margin-right: -30px;
|
||||
padding-top: 8px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
%sms-message-wrapper,
|
||||
$tail-angle: 20deg;
|
||||
|
||||
.sms-message-wrapper {
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 464px;
|
||||
box-sizing: border-box;
|
||||
@@ -13,21 +15,20 @@
|
||||
clear: both;
|
||||
word-wrap: break-word;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -20px;
|
||||
border: 10px solid transparent;
|
||||
border-left-width: 13px;
|
||||
border-right-width: 13px;
|
||||
border-bottom-color: $panel-colour;
|
||||
border-left-color: $panel-colour;
|
||||
transform: rotate($tail-angle);
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.sms-message-wrapper-with-radio {
|
||||
@extend %sms-message-wrapper;
|
||||
padding-left: 45px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sms-message-recipient {
|
||||
@@ -35,22 +36,3 @@
|
||||
color: $secondary-text-colour;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.sms-message-name {
|
||||
@include bold-24;
|
||||
margin: 20px 0 5px 0;
|
||||
}
|
||||
|
||||
.sms-message-picker {
|
||||
display: block;
|
||||
margin: 7px 0 0 0;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sms-message-from {
|
||||
@include bold-19;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
|
||||
}
|
||||
|
||||
&-invisible-error {
|
||||
border-left: 5px solid transparent;
|
||||
padding-left: 7px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-status {
|
||||
|
||||
&-default {
|
||||
@@ -123,6 +129,7 @@
|
||||
}
|
||||
|
||||
&-index {
|
||||
@include bold-16;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
@@ -132,6 +139,10 @@
|
||||
|
||||
}
|
||||
|
||||
.table-font-xsmall td.table-field-index { // overrides GOV.UK Elements
|
||||
@include bold-16;
|
||||
}
|
||||
|
||||
.table-field-headings,
|
||||
.table-field-headings-visible {
|
||||
|
||||
@@ -199,3 +210,11 @@ a.table-show-more-link {
|
||||
border-bottom: 1px solid $border-colour;
|
||||
padding: 0.75em 0 0.5625em 0;
|
||||
}
|
||||
|
||||
.wide-left-hand-column {
|
||||
display: block;
|
||||
max-width: 560px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -94,3 +94,7 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.align-with-message-body {
|
||||
margin-top: $gutter * 5 / 6;
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ class Config(object):
|
||||
CSV_UPLOAD_BUCKET_NAME = 'local-notifications-csv-upload'
|
||||
DESKPRO_PERSON_EMAIL = 'donotreply@notifications.service.gov.uk'
|
||||
ACTIVITY_STATS_LIMIT_DAYS = 7
|
||||
TEST_MESSAGE_FILENAME = 'Test message'
|
||||
|
||||
SMS_FREE_TIER_AMOUNT = 250000
|
||||
TEST_MESSAGE_FILENAME = 'Report'
|
||||
|
||||
STATSD_ENABLED = False
|
||||
STATSD_HOST = "statsd.hostedgraphite.com"
|
||||
@@ -83,6 +81,8 @@ class Config(object):
|
||||
r"hmcts\.net",
|
||||
r"scotent\.co\.uk",
|
||||
r"assembly\.wales",
|
||||
r"cjsm\.net",
|
||||
r"cqc\.org\.uk",
|
||||
]
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ class Test(Development):
|
||||
WTF_CSRF_ENABLED = False
|
||||
CSV_UPLOAD_BUCKET_NAME = 'test-notifications-csv-upload'
|
||||
NOTIFY_ENVIRONMENT = 'test'
|
||||
TEMPLATE_PREVIEW_API_HOST = 'http://localhost:9999'
|
||||
|
||||
|
||||
class Preview(Config):
|
||||
|
||||
@@ -7,6 +7,7 @@ from notifications_utils.recipients import (
|
||||
validate_phone_number,
|
||||
InvalidPhoneError
|
||||
)
|
||||
from notifications_utils.columns import Columns
|
||||
from wtforms import (
|
||||
validators,
|
||||
StringField,
|
||||
@@ -102,11 +103,26 @@ class UKMobileNumber(TelField):
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
def mobile_number():
|
||||
return UKMobileNumber('Mobile number',
|
||||
class InternationalPhoneNumber(TelField):
|
||||
def pre_validate(self, form):
|
||||
try:
|
||||
validate_phone_number(self.data, international=True)
|
||||
except InvalidPhoneError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
|
||||
def mobile_number(label='Mobile number'):
|
||||
return UKMobileNumber(label,
|
||||
validators=[DataRequired(message='Can’t be empty')])
|
||||
|
||||
|
||||
def international_phone_number(label='Mobile number'):
|
||||
return InternationalPhoneNumber(
|
||||
label,
|
||||
validators=[DataRequired(message='Can’t be empty')]
|
||||
)
|
||||
|
||||
|
||||
def password(label='Password'):
|
||||
return PasswordField(label,
|
||||
validators=[DataRequired(message='Can’t be empty'),
|
||||
@@ -502,7 +518,11 @@ class ServiceSmsSender(Form):
|
||||
|
||||
|
||||
class ServiceLetterContactBlock(Form):
|
||||
letter_contact_block = TextAreaField()
|
||||
letter_contact_block = TextAreaField(
|
||||
validators=[
|
||||
NoCommasInPlaceHolders()
|
||||
]
|
||||
)
|
||||
|
||||
def validate_letter_contact_block(form, field):
|
||||
line_count = field.data.strip().count('\n')
|
||||
@@ -621,6 +641,11 @@ class SearchTemplatesForm(Form):
|
||||
search = SearchField('Search by name')
|
||||
|
||||
|
||||
class SearchNotificationsForm(Form):
|
||||
|
||||
to = SearchField('Search by phone number or email address')
|
||||
|
||||
|
||||
class PlaceholderForm(Form):
|
||||
|
||||
pass
|
||||
@@ -629,15 +654,25 @@ class PlaceholderForm(Form):
|
||||
def get_placeholder_form_instance(
|
||||
placeholder_name,
|
||||
dict_to_populate_from,
|
||||
optional_placeholder=False
|
||||
optional_placeholder=False,
|
||||
allow_international_phone_numbers=False,
|
||||
):
|
||||
|
||||
PlaceholderForm.placeholder_value = StringField(
|
||||
placeholder_name,
|
||||
validators=[
|
||||
if Columns.make_key(placeholder_name) == 'emailaddress':
|
||||
field = email_address(label=placeholder_name, gov_user=False)
|
||||
elif Columns.make_key(placeholder_name) == 'phonenumber':
|
||||
if allow_international_phone_numbers:
|
||||
field = international_phone_number(label=placeholder_name)
|
||||
else:
|
||||
field = mobile_number(label=placeholder_name)
|
||||
elif optional_placeholder:
|
||||
field = StringField(placeholder_name)
|
||||
else:
|
||||
field = StringField(placeholder_name, validators=[
|
||||
DataRequired(message='Can’t be empty')
|
||||
] if not optional_placeholder else []
|
||||
)
|
||||
])
|
||||
|
||||
PlaceholderForm.placeholder_value = field
|
||||
|
||||
return PlaceholderForm(
|
||||
placeholder_value=dict_to_populate_from.get(placeholder_name, '')
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime
|
||||
from functools import partial
|
||||
from flask import (
|
||||
render_template,
|
||||
current_app,
|
||||
url_for,
|
||||
session,
|
||||
jsonify,
|
||||
@@ -13,6 +12,7 @@ from flask_login import login_required
|
||||
|
||||
from app.main import main
|
||||
from app import (
|
||||
current_service,
|
||||
job_api_client,
|
||||
service_api_client,
|
||||
template_statistics_client
|
||||
@@ -136,6 +136,20 @@ def monthly(service_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/inbox")
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def inbox(service_id):
|
||||
|
||||
if 'inbound_sms' not in current_service['permissions']:
|
||||
abort(403)
|
||||
|
||||
return render_template(
|
||||
'views/dashboard/inbox.html',
|
||||
messages=service_api_client.get_inbound_sms(service_id),
|
||||
)
|
||||
|
||||
|
||||
def aggregate_usage(template_statistics, sort_key='count'):
|
||||
return sorted(
|
||||
template_statistics,
|
||||
@@ -167,6 +181,13 @@ def get_dashboard_partials(service_id):
|
||||
'views/dashboard/_upcoming.html',
|
||||
scheduled_jobs=scheduled_jobs
|
||||
),
|
||||
'inbox': render_template(
|
||||
'views/dashboard/_inbox.html',
|
||||
inbound_sms_summary=(
|
||||
service_api_client.get_inbound_sms_summary(service_id)
|
||||
if 'inbound_sms' in current_service['permissions'] else None
|
||||
),
|
||||
),
|
||||
'totals': render_template(
|
||||
'views/dashboard/_totals.html',
|
||||
service_id=service_id,
|
||||
@@ -213,7 +234,8 @@ def calculate_free_tier_usage(usage, service):
|
||||
|
||||
|
||||
def calculate_usage(usage):
|
||||
sms_free_allowance = current_app.config['SMS_FREE_TIER_AMOUNT']
|
||||
# TODO: Don't hardcode these - get em from the API
|
||||
sms_free_allowance = 250000
|
||||
|
||||
sms_rate = 0 if len(usage) == 0 else usage[0].get("rate", 0)
|
||||
sms_sent = get_sum_billing_units(breakdown for breakdown in usage if breakdown['notification_type'] == 'sms')
|
||||
|
||||
@@ -8,6 +8,11 @@ from app.main.forms import SupportType, Feedback, Problem, Triage
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@main.route('/feedback', methods=['GET'])
|
||||
def old_feedback():
|
||||
return redirect(url_for('.support'))
|
||||
|
||||
|
||||
@main.route('/support', methods=['GET', 'POST'])
|
||||
def support():
|
||||
form = SupportType()
|
||||
|
||||
@@ -26,6 +26,7 @@ from app import (
|
||||
current_service,
|
||||
format_datetime_short)
|
||||
from app.main import main
|
||||
from app.main.forms import SearchNotificationsForm
|
||||
from app.utils import (
|
||||
get_page_from_request,
|
||||
generate_next_dict,
|
||||
@@ -197,8 +198,10 @@ def view_notifications(service_id, message_type):
|
||||
'views/notifications.html',
|
||||
partials=get_notifications(service_id, message_type),
|
||||
message_type=message_type,
|
||||
status=request.args.get('status'),
|
||||
page=request.args.get('page', 1)
|
||||
status=request.args.get('status') or 'sending,delivered,failed',
|
||||
page=request.args.get('page', 1),
|
||||
to=request.args.get('to'),
|
||||
search_form=SearchNotificationsForm(to=request.args.get('to')),
|
||||
)
|
||||
|
||||
|
||||
@@ -241,7 +244,9 @@ def get_notifications(service_id, message_type, status_override=None):
|
||||
page=page,
|
||||
template_type=[message_type],
|
||||
status=filter_args.get('status'),
|
||||
limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'])
|
||||
limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'],
|
||||
to=request.args.get('to'),
|
||||
)
|
||||
|
||||
url_args = {
|
||||
'message_type': message_type,
|
||||
@@ -249,11 +254,11 @@ def get_notifications(service_id, message_type, status_override=None):
|
||||
}
|
||||
prev_page = None
|
||||
|
||||
if notifications['links'].get('prev', None):
|
||||
if 'links' in notifications and notifications['links'].get('prev', None):
|
||||
prev_page = generate_previous_dict('main.view_notifications', service_id, page, url_args=url_args)
|
||||
next_page = None
|
||||
|
||||
if notifications['links'].get('next', None):
|
||||
if 'links' in notifications and notifications['links'].get('next', None):
|
||||
next_page = generate_next_dict('main.view_notifications', service_id, page, url_args)
|
||||
|
||||
return {
|
||||
|
||||
@@ -157,14 +157,18 @@ def get_example_csv(service_id, template_id):
|
||||
}
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/send/<template_id>/test")
|
||||
@main.route("/services/<service_id>/send/<template_id>/test", endpoint='send_test')
|
||||
@main.route("/services/<service_id>/send/<template_id>/one-off", endpoint='send_one_off')
|
||||
@login_required
|
||||
@user_has_permissions('send_texts', 'send_emails', 'send_letters')
|
||||
def send_test(service_id, template_id):
|
||||
session['send_test_values'] = dict()
|
||||
session['send_test_letter_page_count'] = None
|
||||
return redirect(url_for(
|
||||
'.send_test_step',
|
||||
{
|
||||
'main.send_test': '.send_test_step',
|
||||
'main.send_one_off': '.send_one_off_step',
|
||||
}[request.endpoint],
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=0,
|
||||
@@ -172,14 +176,28 @@ def send_test(service_id, template_id):
|
||||
))
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/send/<template_id>/test/step-<int:step_index>", methods=['GET', 'POST'])
|
||||
@main.route(
|
||||
"/services/<service_id>/send/<template_id>/test/step-<int:step_index>",
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='send_test_step',
|
||||
)
|
||||
@main.route(
|
||||
"/services/<service_id>/send/<template_id>/one-off/step-<int:step_index>",
|
||||
methods=['GET', 'POST'],
|
||||
endpoint='send_one_off_step',
|
||||
)
|
||||
@login_required
|
||||
@user_has_permissions('send_texts', 'send_emails', 'send_letters')
|
||||
def send_test_step(service_id, template_id, step_index):
|
||||
|
||||
if 'send_test_values' not in session:
|
||||
return redirect(url_for(
|
||||
'.send_test', service_id=service_id, template_id=template_id
|
||||
{
|
||||
'main.send_test_step': '.send_test',
|
||||
'main.send_one_off_step': '.send_one_off',
|
||||
}[request.endpoint],
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
))
|
||||
|
||||
template = service_api_client.get_service_template(service_id, template_id)['data']
|
||||
@@ -201,7 +219,10 @@ def send_test_step(service_id, template_id, step_index):
|
||||
page_count=session['send_test_letter_page_count']
|
||||
)
|
||||
|
||||
placeholders = fields_to_fill_in(template)
|
||||
placeholders = fields_to_fill_in(
|
||||
template,
|
||||
prefill_current_user=(request.endpoint == 'main.send_test_step'),
|
||||
)
|
||||
|
||||
if len(placeholders) == 0:
|
||||
return make_and_upload_csv_file(service_id, template)
|
||||
@@ -209,28 +230,33 @@ def send_test_step(service_id, template_id, step_index):
|
||||
try:
|
||||
current_placeholder = placeholders[step_index]
|
||||
except IndexError:
|
||||
if all_placeholders_in_session(placeholders):
|
||||
return make_and_upload_csv_file(service_id, template)
|
||||
return redirect(url_for(
|
||||
'.send_test', service_id=service_id, template_id=template_id
|
||||
{
|
||||
'main.send_test_step': '.send_test',
|
||||
'main.send_one_off_step': '.send_one_off',
|
||||
}[request.endpoint],
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
))
|
||||
optional_placeholder = (current_placeholder in optional_address_columns)
|
||||
form = get_placeholder_form_instance(
|
||||
current_placeholder,
|
||||
dict_to_populate_from=get_normalised_send_test_values_from_session(),
|
||||
optional_placeholder=optional_placeholder,
|
||||
allow_international_phone_numbers=current_service['can_send_international_sms'],
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
session['send_test_values'][current_placeholder] = form.placeholder_value.data
|
||||
|
||||
if all(
|
||||
get_normalised_send_test_values_from_session().get(placeholder, False) not in (False, None)
|
||||
for placeholder in placeholders
|
||||
):
|
||||
if all_placeholders_in_session(placeholders):
|
||||
return make_and_upload_csv_file(service_id, template)
|
||||
|
||||
return redirect(url_for(
|
||||
'.send_test_step',
|
||||
request.endpoint,
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index + 1,
|
||||
@@ -247,7 +273,7 @@ def send_test_step(service_id, template_id, step_index):
|
||||
)
|
||||
else:
|
||||
back_link = url_for(
|
||||
'.send_test_step',
|
||||
request.endpoint,
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
@@ -256,13 +282,27 @@ def send_test_step(service_id, template_id, step_index):
|
||||
template.values = get_normalised_send_test_values_from_session()
|
||||
template.values[current_placeholder] = None
|
||||
|
||||
if (
|
||||
request.endpoint == 'main.send_one_off_step' and
|
||||
step_index == 0 and
|
||||
template.template_type != 'letter'
|
||||
):
|
||||
skip_link = (
|
||||
'Use my {}'.format(first_column_headings[template.template_type][0]),
|
||||
url_for('.send_test', service_id=service_id, template_id=template.id),
|
||||
)
|
||||
else:
|
||||
skip_link = None
|
||||
|
||||
return render_template(
|
||||
'views/send-test.html',
|
||||
page_title=get_send_test_page_title(template.template_type, get_help_argument()),
|
||||
template=template,
|
||||
form=form,
|
||||
skip_link=skip_link,
|
||||
optional_placeholder=optional_placeholder,
|
||||
help=get_help_argument(),
|
||||
back_link=back_link,
|
||||
help=get_help_argument(),
|
||||
)
|
||||
|
||||
|
||||
@@ -470,11 +510,11 @@ def get_check_messages_back_url(service_id, template_type):
|
||||
return url_for('main.choose_template', service_id=service_id)
|
||||
|
||||
|
||||
def fields_to_fill_in(template):
|
||||
def fields_to_fill_in(template, prefill_current_user=False):
|
||||
|
||||
recipient_columns = first_column_headings[template.template_type]
|
||||
|
||||
if 'letter' == template.template_type:
|
||||
if 'letter' == template.template_type or not prefill_current_user:
|
||||
return recipient_columns + list(template.placeholders)
|
||||
|
||||
session['send_test_values'][recipient_columns[0]] = {
|
||||
@@ -513,3 +553,18 @@ def make_and_upload_csv_file(service_id, template):
|
||||
from_test=True,
|
||||
help=2 if get_help_argument() else 0
|
||||
))
|
||||
|
||||
|
||||
def all_placeholders_in_session(placeholders):
|
||||
return all(
|
||||
get_normalised_send_test_values_from_session().get(placeholder, False) not in (False, None)
|
||||
for placeholder in placeholders
|
||||
)
|
||||
|
||||
|
||||
def get_send_test_page_title(template_type, help_argument):
|
||||
if help_argument:
|
||||
return 'Example text message'
|
||||
if template_type == 'letter':
|
||||
return 'Print a test letter'
|
||||
return 'Send to one recipient'
|
||||
|
||||
@@ -14,6 +14,8 @@ from flask_login import (
|
||||
login_required,
|
||||
current_user
|
||||
)
|
||||
|
||||
from notifications_utils.field import Field
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import service_api_client
|
||||
@@ -46,7 +48,9 @@ def service_settings(service_id):
|
||||
organisation=organisation,
|
||||
letter_branding=letter_branding_organisations.get(
|
||||
current_service.get('dvla_organisation', '001')
|
||||
)
|
||||
),
|
||||
can_receive_inbound=('inbound_sms' in current_service['permissions']),
|
||||
letter_contact_block=Field(current_service['letter_contact_block'], html='escape')
|
||||
)
|
||||
|
||||
|
||||
@@ -264,14 +268,27 @@ def service_set_reply_to_email(service_id):
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_set_sms_sender(service_id):
|
||||
form = ServiceSmsSender()
|
||||
if form.validate_on_submit():
|
||||
set_inbound_sms = request.args.get('set_inbound_sms', False)
|
||||
if set_inbound_sms == 'True':
|
||||
permissions = current_service['permissions']
|
||||
if 'inbound_sms' in permissions:
|
||||
permissions.remove('inbound_sms')
|
||||
else:
|
||||
permissions.append('inbound_sms')
|
||||
service_api_client.update_service_with_properties(
|
||||
current_service['id'],
|
||||
{'permissions': permissions,
|
||||
'sms_sender': form.sms_sender.data or None}
|
||||
)
|
||||
else:
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
sms_sender=form.sms_sender.data or None
|
||||
)
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
if request.method == 'GET':
|
||||
form.sms_sender.data = current_service.get('sms_sender')
|
||||
if form.validate_on_submit():
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
sms_sender=form.sms_sender.data or None
|
||||
)
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
return render_template(
|
||||
'views/service-settings/set-sms-sender.html',
|
||||
form=form)
|
||||
|
||||
@@ -21,7 +21,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
limit_days=None,
|
||||
include_jobs=None,
|
||||
include_from_test_key=None,
|
||||
format_for_csv=None
|
||||
format_for_csv=None,
|
||||
to=None,
|
||||
):
|
||||
params = {}
|
||||
if page is not None:
|
||||
@@ -38,6 +39,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
params['include_from_test_key'] = include_from_test_key
|
||||
if format_for_csv is not None:
|
||||
params['format_for_csv'] = format_for_csv
|
||||
if to is not None:
|
||||
params['to'] = to
|
||||
if job_id:
|
||||
return self.get(
|
||||
url='/service/{}/job/{}/notifications'.format(service_id, job_id),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from flask import url_for
|
||||
from app.utils import BrowsableItem
|
||||
from app.notify_client import _attach_current_user, NotifyAdminAPIClient
|
||||
@@ -94,6 +95,7 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
'organisation',
|
||||
'letter_contact_block',
|
||||
'dvla_organisation',
|
||||
'permissions'
|
||||
}
|
||||
if disallowed_attributes:
|
||||
raise TypeError('Not allowed to update service attributes: {}'.format(
|
||||
@@ -242,6 +244,16 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
params=dict(year=year)
|
||||
)
|
||||
|
||||
def get_inbound_sms(self, service_id):
|
||||
return self.get(
|
||||
'/service/{}/inbound-sms'.format(service_id)
|
||||
)['data']
|
||||
|
||||
def get_inbound_sms_summary(self, service_id):
|
||||
return self.get(
|
||||
'/service/{}/inbound-sms/summary'.format(service_id)
|
||||
)
|
||||
|
||||
|
||||
class ServicesBrowsableItem(BrowsableItem):
|
||||
@property
|
||||
|
||||
@@ -31,22 +31,25 @@
|
||||
failure_percentage,
|
||||
danger_zone=False,
|
||||
failure_link=None,
|
||||
link=None
|
||||
link=None,
|
||||
show_failures=True
|
||||
) %}
|
||||
<div class="big-number-with-status">
|
||||
{{ big_number(number, label, link=link) }}
|
||||
<div class="big-number-status{% if danger_zone %}-failing{% endif %}">
|
||||
{% if failures %}
|
||||
{% if failure_link %}
|
||||
<a href="{{ failure_link }}">
|
||||
{% if show_failures %}
|
||||
<div class="big-number-status{% if danger_zone %}-failing{% endif %}">
|
||||
{% if failures %}
|
||||
{% if failure_link %}
|
||||
<a href="{{ failure_link }}">
|
||||
{{ "{:,}".format(failures) }} failed – {{ failure_percentage }}%
|
||||
</a>
|
||||
{% else %}
|
||||
{{ "{:,}".format(failures) }} failed – {{ failure_percentage }}%
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ "{:,}".format(failures) }} failed – {{ failure_percentage }}%
|
||||
No failures
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No failures
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
</th>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro index_field(text) -%}
|
||||
{% macro index_field(text=None) -%}
|
||||
<td class="table-field-index">
|
||||
<span>{{ text }}</span>
|
||||
{{ text if text != None else caller() }}
|
||||
</td>
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
{{ item.to }}
|
||||
</p>
|
||||
<p class="hint">
|
||||
{% if item.job %}
|
||||
{% if item.job and item.job.original_file_name == 'Report' %}
|
||||
<a href="{{ url_for('.view_template_version', service_id=current_service.id, template_id=item.template.id, version=item.template_version) }}">{{ item.template.name }}</a>
|
||||
sent to one recipient
|
||||
{% elif item.job %}
|
||||
From <a href="{{ url_for(".view_job", service_id=current_service.id, job_id=item.job.id) }}">{{ item.job.original_file_name }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('.view_template_version', service_id=current_service.id, template_id=item.template.id, version=item.template_version) }}">{{ item.template.name }}</a>
|
||||
|
||||
@@ -150,7 +150,9 @@
|
||||
|
||||
{% endif %}
|
||||
|
||||
{{ template|string }}
|
||||
{% if not errors %}
|
||||
{{ template|string }}
|
||||
{% endif %}
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
{% if errors %}
|
||||
@@ -188,14 +190,18 @@
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="visually-hidden">Row in file</span><span aria-hidden="true">1</span>'|safe
|
||||
'<span class="visually-hidden">Row in file</span><span aria-hidden="true" class="{}">1</span>'.format("table-field-invisible-error" if errors else "")|safe
|
||||
] + recipients.column_headers
|
||||
) %}
|
||||
{{ index_field(item.index + 2) }}
|
||||
{% call index_field() %}
|
||||
<span class="{% if item.index in recipients.rows_with_errors %}table-field-error{% endif %}">
|
||||
{{ item.index + 2 }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% for column in recipients.column_headers %}
|
||||
{% if item['columns'][column].error and not recipients.missing_column_headers %}
|
||||
{% call field() %}
|
||||
<span class="table-field-error">
|
||||
<span>
|
||||
<span class="table-field-error-label">{{ item['columns'][column].error }}</span>
|
||||
{{ item['columns'][column].data if item['columns'][column].data != None }}
|
||||
</span>
|
||||
@@ -234,4 +240,9 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if errors %}
|
||||
<h2 class="heading-medium">Preview of {{ template.name }}</h2>
|
||||
{{ template|string }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
21
app/templates/views/dashboard/_inbox.html
Normal file
21
app/templates/views/dashboard/_inbox.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% from "components/big-number.html" import big_number, big_number_with_status %}
|
||||
|
||||
<div class="ajax-block">
|
||||
{% if inbound_sms_summary != None %}
|
||||
<div class="big-number-meta-wrapper">
|
||||
{{
|
||||
big_number_with_status(
|
||||
inbound_sms_summary.count,
|
||||
'text messages received',
|
||||
link=url_for('.inbox', service_id=current_service.id),
|
||||
show_failures=False
|
||||
)
|
||||
}}
|
||||
<div class="big-number-meta">
|
||||
{% if inbound_sms_summary.most_recent %}
|
||||
latest message {{ inbound_sms_summary.most_recent | format_delta }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@
|
||||
<div id="total-email" class="column-half">
|
||||
{{ big_number_with_status(
|
||||
statistics['email']['requested'],
|
||||
message_count_label(statistics['email']['requested'], 'email', suffix=''),
|
||||
message_count_label(statistics['email']['requested'], 'email', suffix='sent'),
|
||||
statistics['email']['failed'],
|
||||
statistics['email']['failed_percentage'],
|
||||
statistics['email']['show_warning'],
|
||||
@@ -17,7 +17,7 @@
|
||||
<div id="total-sms" class="column-half">
|
||||
{{ big_number_with_status(
|
||||
statistics['sms']['requested'],
|
||||
message_count_label(statistics['sms']['requested'], 'sms', suffix=''),
|
||||
message_count_label(statistics['sms']['requested'], 'sms', suffix='sent'),
|
||||
statistics['sms']['failed'],
|
||||
statistics['sms']['failed_percentage'],
|
||||
statistics['sms']['show_warning'],
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
In the last 7 days
|
||||
</h2>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'totals') }}
|
||||
{{ show_more(
|
||||
url_for('.monthly', service_id=current_service.id),
|
||||
|
||||
43
app/templates/views/dashboard/inbox.html
Normal file
43
app/templates/views/dashboard/inbox.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
|
||||
{% extends "withnav_template.html" %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Inbox
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="heading-large">
|
||||
Received text messages
|
||||
</h1>
|
||||
<div>
|
||||
{% call(item, row_number) list_table(
|
||||
messages,
|
||||
caption="Inbox",
|
||||
caption_visible=False,
|
||||
empty_message='When users text your service’s phone number ({}) you’ll see the messages here'.format(current_service.sms_sender),
|
||||
field_headings=[
|
||||
'From',
|
||||
'First two lines of message'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call field() %}
|
||||
<span class="file-list-filename" href="#">{{ item.user_number }}</span>
|
||||
<span class="wide-left-hand-column">{{ item.content }}</span>
|
||||
{% endcall %}
|
||||
{% call field(align='right') %}
|
||||
<span class="file-list-hint align-with-message-body">
|
||||
{{ item.created_at | format_delta }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% if messages %}
|
||||
<p class="table-show-more-link">
|
||||
Showing all {{ messages | length }} messages
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,8 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/ajax-block.html" import ajax_block %}
|
||||
{% from "components/message-count-label.html" import message_count_label, recipient_count_label %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ message_count_label(99, message_type, suffix='') | capitalize }}
|
||||
@@ -18,9 +20,27 @@
|
||||
'counts'
|
||||
) }}
|
||||
|
||||
<form
|
||||
method="get"
|
||||
action="{{ url_for('.view_notifications', service_id=current_service.id, message_type=message_type) }}"
|
||||
class="grid-row"
|
||||
>
|
||||
<div class="column-three-quarters">
|
||||
<input type="hidden" name="status" value="{{ status }}">
|
||||
{{ textbox(
|
||||
search_form.to,
|
||||
width='1-1',
|
||||
label='Search by {}'.format('email address' if message_type == 'email' else 'phone number')
|
||||
) }}
|
||||
</div>
|
||||
<div class="column-one-quarter align-button-with-textbox">
|
||||
<input type="submit" class="button" value="Search">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{ ajax_block(
|
||||
partials,
|
||||
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page),
|
||||
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page, to=to),
|
||||
'notifications'
|
||||
) }}
|
||||
|
||||
|
||||
@@ -5,32 +5,30 @@
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, index_field_heading %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{% if request.args['help'] %}
|
||||
Example text message
|
||||
{% else %}
|
||||
Send yourself a test
|
||||
{% endif %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="heading-large">
|
||||
{% if request.args['help'] %}
|
||||
Example text message
|
||||
{% else %}
|
||||
{% if template.template_type == 'letter' %}
|
||||
Print a test letter
|
||||
{% else %}
|
||||
Send yourself a test
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ page_title }}
|
||||
</h1>
|
||||
|
||||
<form method="post" class="js-stick-at-top-when-scrolling" data-module="autofocus">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
hint='Optional' if optional_placeholder else None
|
||||
) }}
|
||||
<div class="grid-row">
|
||||
<div class="column-two-thirds">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
hint='Optional' if optional_placeholder else None,
|
||||
width='1-1',
|
||||
) }}
|
||||
</div>
|
||||
{% if skip_link %}
|
||||
<div class="column-one-third">
|
||||
<a href="{{ skip_link[1] }}" class="top-gutter-4-3">{{ skip_link[0] }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ page_footer('Next', back_link=back_link) }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -36,8 +36,13 @@
|
||||
|
||||
{% call row() %}
|
||||
{{ text_field('Text message sender') }}
|
||||
{{ text_field(current_service.sms_sender or 'GOVUK') }}
|
||||
{{ edit_field('Change', url_for('.service_set_sms_sender', service_id=current_service.id)) }}
|
||||
{{ text_field(current_service.sms_sender) }}
|
||||
{% if current_user.has_permissions([], admin_override=True) or not can_receive_inbound %}
|
||||
{{ edit_field('Change', url_for('.service_set_sms_sender', service_id=current_service.id, set_inbound_sms=False)) }}
|
||||
{% else %}
|
||||
{{ text_field('') }}
|
||||
{% endif %}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
{% call row() %}
|
||||
@@ -56,7 +61,7 @@
|
||||
{% call row() %}
|
||||
{{ text_field('Letter contact details') }}
|
||||
{% call field(status='' if current_service.letter_contact_block else 'default') %}
|
||||
{{ current_service.letter_contact_block | escape | nl2br | safe }}
|
||||
{{ letter_contact_block | string | nl2br | safe if current_service.letter_contact_block else 'None'}}
|
||||
{% endcall %}
|
||||
{{ edit_field('Change', url_for('.service_set_letter_contact_block', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
@@ -162,6 +167,11 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="bottom-gutter">
|
||||
<a href="{{ url_for('.service_set_sms_sender', service_id=current_service.id, set_inbound_sms=True) }}" class="button">
|
||||
{{ 'Stop inbound sms' if can_receive_inbound else 'Allow inbound sms' }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
label='How should users contact your service?<br>This applies to all the letters you send.'|safe,
|
||||
hint='10 lines maximum',
|
||||
width='1-1',
|
||||
rows=10
|
||||
rows=10,
|
||||
highlight_tags=True
|
||||
) }}
|
||||
{{ page_footer(
|
||||
'Save',
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<div class="grid-row bottom-gutter">
|
||||
<div class="column-half">
|
||||
<h3 class="visually-hidden">Services</h3>
|
||||
<div class="product-page-big-number">51</div>
|
||||
<div class="product-page-big-number">54</div>
|
||||
services
|
||||
</div>
|
||||
<div class="column-half">
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="{{ 'column-half' if template.template_type == 'letter' else 'column-third' }}">
|
||||
<a href="{{ url_for(".send_test", service_id=current_service.id, template_id=template.id) }}" class="pill-separate-item">
|
||||
{{ 'Print a test letter' if template.template_type == 'letter' else 'Send yourself a test' }}
|
||||
<a href="{{ url_for(".send_one_off", service_id=current_service.id, template_id=template.id) }}" class="pill-separate-item">
|
||||
{{ 'Print a test letter' if template.template_type == 'letter' else 'Send to one recipient' }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user