mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-25 08:44:23 -04:00
Merge branch 'master' into reinstate-new-rate-api
Conflicts: app/main/views/dashboard.py
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user