mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-20 14:29:51 -04:00
Merge branch 'master' into remove-code-that-handled-old-new-job-api
Conflicts: app/main/views/jobs.py app/notify_client/job_api_client.py
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import pytz
|
||||
from flask_wtf import Form
|
||||
from datetime import datetime, timedelta
|
||||
from notifications_utils.recipients import (
|
||||
validate_phone_number,
|
||||
InvalidPhoneError
|
||||
@@ -22,6 +24,30 @@ from app.main.validators import (Blacklist, CsvFileValidator, ValidEmailDomainRe
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_NORMAL, KEY_TYPE_TEST, KEY_TYPE_TEAM
|
||||
|
||||
|
||||
def get_time_value_and_label(future_time):
|
||||
return (
|
||||
future_time.replace(tzinfo=None).isoformat(),
|
||||
get_human_time(future_time.astimezone(pytz.timezone('Europe/London')))
|
||||
)
|
||||
|
||||
|
||||
def get_human_time(time):
|
||||
return {
|
||||
'0': 'Midnight',
|
||||
'12': 'Midday'
|
||||
}.get(
|
||||
time.strftime('%-H'),
|
||||
time.strftime('%-I%p').lower()
|
||||
)
|
||||
|
||||
|
||||
def get_next_hours_from(now, hours=23):
|
||||
return [
|
||||
(now + timedelta(hours=i)).replace(minute=0, second=0).replace(tzinfo=pytz.utc)
|
||||
for i in range(1, hours + 1)
|
||||
]
|
||||
|
||||
|
||||
def email_address(label='Email address'):
|
||||
return EmailField(label, validators=[
|
||||
Length(min=5, max=255),
|
||||
@@ -288,6 +314,23 @@ class ConfirmMobileNumberForm(Form):
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
class ChooseTimeForm(Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChooseTimeForm, self).__init__(*args, **kwargs)
|
||||
self.scheduled_for.choices = [('', 'Now')] + [
|
||||
get_time_value_and_label(hour) for hour in get_next_hours_from(datetime.utcnow())
|
||||
]
|
||||
|
||||
scheduled_for = RadioField(
|
||||
'When should Notify send these messages?',
|
||||
default='',
|
||||
validators=[
|
||||
DataRequired()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class CreateKeyForm(Form):
|
||||
def __init__(self, existing_key_names=[], *args, **kwargs):
|
||||
self.existing_key_names = [x.lower() for x in existing_key_names]
|
||||
@@ -327,17 +370,21 @@ class ProviderForm(Form):
|
||||
|
||||
|
||||
class ServiceReplyToEmailFrom(Form):
|
||||
email_address = email_address()
|
||||
email_address = email_address(label='Email reply to address')
|
||||
|
||||
|
||||
class ServiceSmsSender(Form):
|
||||
sms_sender = StringField('', validators=[Length(max=11,
|
||||
message="Text message sender can't be longer than 11 characters")])
|
||||
sms_sender = StringField(
|
||||
'Text message sender',
|
||||
validators=[
|
||||
Length(max=11, message="Enter fewer than 11 characters")
|
||||
]
|
||||
)
|
||||
|
||||
def validate_sms_sender(form, field):
|
||||
import re
|
||||
if field.data and not re.match('^[a-zA-Z0-9\s]+$', field.data):
|
||||
raise ValidationError('Text message sender can only contain alpha-numeric characters')
|
||||
raise ValidationError('Use letters and numbers only')
|
||||
|
||||
|
||||
class ServiceBrandingOrg(Form):
|
||||
|
||||
@@ -112,13 +112,23 @@ def get_dashboard_partials(service_id):
|
||||
template_statistics_client.get_template_statistics_for_service(service_id, limit_days=7)
|
||||
)
|
||||
|
||||
jobs = add_rate_to_jobs(filter(
|
||||
lambda job: job['original_file_name'] != current_app.config['TEST_MESSAGE_FILENAME'],
|
||||
job_api_client.get_job(service_id, limit_days=7)['data']
|
||||
))
|
||||
jobs = add_rate_to_jobs([
|
||||
job for job in job_api_client.get_job(service_id, limit_days=7)['data']
|
||||
if job['original_file_name'] != current_app.config['TEST_MESSAGE_FILENAME']
|
||||
])
|
||||
scheduled_jobs = sorted([
|
||||
job for job in jobs if job['job_status'] == 'scheduled'
|
||||
], key=lambda job: job['scheduled_for'])
|
||||
immediate_jobs = [
|
||||
job for job in jobs if job['job_status'] != 'scheduled'
|
||||
]
|
||||
service = service_api_client.get_detailed_service(service_id)
|
||||
|
||||
return {
|
||||
'upcoming': render_template(
|
||||
'views/dashboard/_upcoming.html',
|
||||
scheduled_jobs=scheduled_jobs
|
||||
),
|
||||
'totals': render_template(
|
||||
'views/dashboard/_totals.html',
|
||||
service_id=service_id,
|
||||
@@ -134,7 +144,7 @@ def get_dashboard_partials(service_id):
|
||||
'has_template_statistics': bool(template_statistics),
|
||||
'jobs': render_template(
|
||||
'views/dashboard/_jobs.html',
|
||||
jobs=jobs
|
||||
jobs=immediate_jobs
|
||||
),
|
||||
'has_jobs': bool(jobs),
|
||||
'usage': render_template(
|
||||
|
||||
@@ -31,7 +31,7 @@ def cookies():
|
||||
|
||||
@main.route('/trial-mode')
|
||||
def trial_mode():
|
||||
return render_template('views/trial-mode.html')
|
||||
return render_template('views/trial-mode.html', hours=hours)
|
||||
|
||||
|
||||
@main.route('/pricing')
|
||||
|
||||
@@ -65,7 +65,10 @@ def _set_status_filters(filter_args):
|
||||
def view_jobs(service_id):
|
||||
return render_template(
|
||||
'views/jobs/jobs.html',
|
||||
jobs=add_rate_to_jobs(job_api_client.get_job(service_id)['data'])
|
||||
jobs=add_rate_to_jobs([
|
||||
job for job in job_api_client.get_job(service_id)['data']
|
||||
if job['job_status'] != 'scheduled'
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@@ -90,7 +93,8 @@ def view_job(service_id, job_id):
|
||||
template_id=job['template'],
|
||||
version=job['template_version']
|
||||
)['data'],
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
),
|
||||
status=request.args.get('status', ''),
|
||||
updates_url=url_for(
|
||||
@@ -271,6 +275,11 @@ def get_status_filters(service, message_type, statistics):
|
||||
|
||||
|
||||
def _get_job_counts(job, help_argument):
|
||||
sending = 0 if job['job_status'] == 'scheduled' else (
|
||||
job.get('notification_count', 0) -
|
||||
job.get('notifications_delivered', 0) -
|
||||
job.get('notifications_failed', 0)
|
||||
)
|
||||
return [
|
||||
(
|
||||
label,
|
||||
@@ -285,25 +294,23 @@ def _get_job_counts(job, help_argument):
|
||||
count
|
||||
) for label, query_param, count in [
|
||||
[
|
||||
'total', '',
|
||||
job.get('notification_count', 0)
|
||||
'total', '',
|
||||
job.get('notification_count', 0)
|
||||
],
|
||||
[
|
||||
'sending', 'sending',
|
||||
job.get('notification_count', 0) -
|
||||
job.get('notifications_delivered', 0) -
|
||||
job.get('notifications_failed', 0)
|
||||
'sending', 'sending',
|
||||
sending
|
||||
],
|
||||
[
|
||||
'delivered', 'delivered',
|
||||
job.get('notifications_delivered', 0)
|
||||
'delivered', 'delivered',
|
||||
job.get('notifications_delivered', 0)
|
||||
],
|
||||
[
|
||||
'failed', 'failed',
|
||||
job.get('notifications_failed', 0)
|
||||
'failed', 'failed',
|
||||
job.get('notifications_failed', 0)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def get_job_partials(job):
|
||||
@@ -315,7 +322,6 @@ def get_job_partials(job):
|
||||
return {
|
||||
'counts': render_template(
|
||||
'partials/jobs/count.html',
|
||||
job=job,
|
||||
counts=_get_job_counts(job, request.args.get('help', 0)),
|
||||
status=filter_args['status']
|
||||
),
|
||||
@@ -323,7 +329,7 @@ def get_job_partials(job):
|
||||
'partials/jobs/notifications.html',
|
||||
notifications=notifications['notifications'],
|
||||
more_than_one_page=bool(notifications.get('links', {}).get('next')),
|
||||
percentage_complete=(job['notifications_sent'] / job['notification_count'] * 100),
|
||||
percentage_complete=(job['notifications_requested'] / job['notification_count'] * 100),
|
||||
download_link=url_for(
|
||||
'.view_job_csv',
|
||||
service_id=current_service['id'],
|
||||
@@ -331,7 +337,8 @@ def get_job_partials(job):
|
||||
status=request.args.get('status')
|
||||
),
|
||||
help=get_help_argument(),
|
||||
time_left=get_time_left(job['created_at'])
|
||||
time_left=get_time_left(job['created_at']),
|
||||
job=job
|
||||
),
|
||||
'status': render_template(
|
||||
'partials/jobs/status.html',
|
||||
|
||||
@@ -1,50 +1,66 @@
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from flask import render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from app import statistics_api_client, service_api_client
|
||||
from app import service_api_client
|
||||
from app.main import main
|
||||
from app.utils import user_has_permissions
|
||||
from app.statistics_utils import sum_of_statistics, add_rates_to
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
|
||||
|
||||
@main.route("/platform-admin")
|
||||
@login_required
|
||||
@user_has_permissions(admin_override=True)
|
||||
def platform_admin():
|
||||
services = service_api_client.get_services({'detailed': True})['data']
|
||||
return render_template(
|
||||
'views/platform-admin.html',
|
||||
**get_statistics()
|
||||
**get_statistics(services)
|
||||
)
|
||||
|
||||
|
||||
def get_statistics():
|
||||
day = datetime.now(tz=pytz.timezone('Europe/London')).date()
|
||||
all_stats = statistics_api_client.get_statistics_for_all_services_for_day(day)['data']
|
||||
services = service_api_client.get_services()['data']
|
||||
service_stats = format_stats_by_service(all_stats, services)
|
||||
def get_statistics(services):
|
||||
return {
|
||||
'global_stats': add_rates_to(sum_of_statistics(all_stats)),
|
||||
'service_stats': service_stats
|
||||
'global_stats': create_global_stats(services),
|
||||
'service_stats': format_stats_by_service(services)
|
||||
}
|
||||
|
||||
|
||||
def format_stats_by_service(all_stats, services):
|
||||
services = {service['id']: service for service in services}
|
||||
return [
|
||||
{
|
||||
'id': stats['service'],
|
||||
'name': services[stats['service']]['name'],
|
||||
'sending': (
|
||||
(stats['sms_requested'] - stats['sms_delivered'] - stats['sms_failed']) +
|
||||
(stats['emails_requested'] - stats['emails_delivered'] - stats['emails_failed'])
|
||||
),
|
||||
'delivered': stats['sms_delivered'] + stats['emails_delivered'],
|
||||
'failed': stats['sms_failed'] + stats['emails_failed'],
|
||||
'restricted': services[stats['service']]['restricted'],
|
||||
'research_mode': services[stats['service']]['research_mode']
|
||||
def create_global_stats(services):
|
||||
stats = {
|
||||
'email': {
|
||||
'delivered': 0,
|
||||
'failed': 0,
|
||||
'requested': 0
|
||||
},
|
||||
'sms': {
|
||||
'delivered': 0,
|
||||
'failed': 0,
|
||||
'requested': 0
|
||||
}
|
||||
}
|
||||
for service in services:
|
||||
for msg_type, status in itertools.product(('sms', 'email'), ('delivered', 'failed', 'requested')):
|
||||
stats[msg_type][status] += service['statistics'][msg_type][status]
|
||||
|
||||
for stat in stats.values():
|
||||
stat['failure_rate'] = get_formatted_percentage(stat['failed'], stat['requested'])
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def format_stats_by_service(services):
|
||||
for service in services:
|
||||
stats = service['statistics'].values()
|
||||
yield {
|
||||
'id': service['id'],
|
||||
'name': service['name'],
|
||||
'sending': sum((stat['requested'] - stat['delivered'] - stat['failed']) for stat in stats),
|
||||
'delivered': sum(stat['delivered'] for stat in stats),
|
||||
'failed': sum(stat['failed'] for stat in stats),
|
||||
'restricted': service['restricted'],
|
||||
'research_mode': service['research_mode']
|
||||
}
|
||||
for stats in all_stats
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import itertools
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from string import ascii_uppercase
|
||||
|
||||
from contextlib import suppress
|
||||
@@ -23,7 +23,7 @@ from notifications_utils.template import Template
|
||||
from notifications_utils.recipients import RecipientCSV, first_column_heading, validate_and_format_phone_number
|
||||
|
||||
from app.main import main
|
||||
from app.main.forms import CsvUploadForm
|
||||
from app.main.forms import CsvUploadForm, ChooseTimeForm
|
||||
from app.main.uploader import (
|
||||
s3upload,
|
||||
s3download
|
||||
@@ -75,7 +75,8 @@ def choose_template(service_id, template_type):
|
||||
templates=[
|
||||
Template(
|
||||
template,
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
) for template in service_api_client.get_service_templates(service_id)['data']
|
||||
if template['template_type'] == template_type
|
||||
],
|
||||
@@ -90,7 +91,8 @@ def choose_template(service_id, template_type):
|
||||
def send_messages(service_id, template_id):
|
||||
template = Template(
|
||||
service_api_client.get_service_template(service_id, template_id)['data'],
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
|
||||
form = CsvUploadForm()
|
||||
@@ -149,7 +151,8 @@ def send_test(service_id, template_id):
|
||||
|
||||
template = Template(
|
||||
service_api_client.get_service_template(service_id, template_id)['data'],
|
||||
prefix=current_service['name']
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
|
||||
if len(template.placeholders) == 0 or request.method == 'POST':
|
||||
@@ -189,13 +192,13 @@ def send_test(service_id, template_id):
|
||||
@main.route("/services/<service_id>/send/<template_id>/from-api", methods=['GET'])
|
||||
@login_required
|
||||
def send_from_api(service_id, template_id):
|
||||
template = Template(
|
||||
service_api_client.get_service_template(service_id, template_id)['data'],
|
||||
prefix=current_service['name']
|
||||
)
|
||||
return render_template(
|
||||
'views/send-from-api.html',
|
||||
template=template
|
||||
template=Template(
|
||||
service_api_client.get_service_template(service_id, template_id)['data'],
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -216,14 +219,13 @@ def check_messages(service_id, template_type, upload_id):
|
||||
if not contents:
|
||||
flash('There was a problem reading your upload file')
|
||||
|
||||
template = service_api_client.get_service_template(
|
||||
service_id,
|
||||
session['upload_data'].get('template_id')
|
||||
)['data']
|
||||
|
||||
template = Template(
|
||||
template,
|
||||
prefix=current_service['name']
|
||||
service_api_client.get_service_template(
|
||||
service_id,
|
||||
session['upload_data'].get('template_id')
|
||||
)['data'],
|
||||
prefix=current_service['name'],
|
||||
sms_sender=current_service['sms_sender']
|
||||
)
|
||||
|
||||
recipients = RecipientCSV(
|
||||
@@ -248,8 +250,10 @@ def check_messages(service_id, template_type, upload_id):
|
||||
back_link = url_for(
|
||||
'.choose_template', service_id=service_id, template_type=template.template_type, **extra_args
|
||||
)
|
||||
choose_time_form = None
|
||||
else:
|
||||
back_link = url_for('.send_messages', service_id=service_id, template_id=template.id)
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
with suppress(StopIteration):
|
||||
template.values = next(recipients.rows)
|
||||
@@ -274,6 +278,7 @@ def check_messages(service_id, template_type, upload_id):
|
||||
upload_id=upload_id,
|
||||
form=CsvUploadForm(),
|
||||
remaining_messages=remaining_messages,
|
||||
choose_time_form=choose_time_form,
|
||||
back_link=back_link,
|
||||
help=get_help_argument()
|
||||
)
|
||||
@@ -308,7 +313,8 @@ def start_job(service_id, upload_id):
|
||||
service_id,
|
||||
upload_data.get('template_id'),
|
||||
upload_data.get('original_file_name'),
|
||||
upload_data.get('notification_count')
|
||||
upload_data.get('notification_count'),
|
||||
scheduled_for=request.form.get('scheduled_for', '')
|
||||
)
|
||||
|
||||
return redirect(
|
||||
|
||||
@@ -34,7 +34,14 @@ from app import user_api_client, current_service, organisations_client
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_settings(service_id):
|
||||
return render_template('views/service-settings.html')
|
||||
if current_service['organisation']:
|
||||
organisation = organisations_client.get_organisation(current_service['organisation'])['organisation']
|
||||
else:
|
||||
organisation = None
|
||||
return render_template(
|
||||
'views/service-settings.html',
|
||||
organisation=organisation
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/name", methods=['GET', 'POST'])
|
||||
@@ -157,41 +164,6 @@ def service_switch_research_mode(service_id):
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/status", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_status_change(service_id):
|
||||
if request.method == 'GET':
|
||||
return render_template(
|
||||
'views/service-settings/status.html'
|
||||
)
|
||||
elif request.method == 'POST':
|
||||
return redirect(url_for('.service_status_change_confirm', service_id=service_id))
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/status/confirm", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
def service_status_change_confirm(service_id):
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ConfirmPasswordForm(_check_password)
|
||||
|
||||
if form.validate_on_submit():
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
active=True
|
||||
)
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
return render_template(
|
||||
'views/service-settings/confirm.html',
|
||||
heading='Turn off all outgoing notifications',
|
||||
destructive=True,
|
||||
form=form)
|
||||
|
||||
|
||||
@main.route("/services/<service_id>/service-settings/delete", methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@user_has_permissions('manage_settings', admin_override=True)
|
||||
@@ -233,12 +205,10 @@ def service_set_reply_to_email(service_id):
|
||||
if request.method == 'GET':
|
||||
form.email_address.data = current_service.get('reply_to_email_address')
|
||||
if form.validate_on_submit():
|
||||
message = 'Reply to email set to {}'.format(form.email_address.data)
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
reply_to_email_address=form.email_address.data
|
||||
)
|
||||
flash(message, 'default_with_tick')
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
return render_template(
|
||||
'views/service-settings/set-reply-to-email.html',
|
||||
@@ -253,15 +223,10 @@ def service_set_sms_sender(service_id):
|
||||
if request.method == 'GET':
|
||||
form.sms_sender.data = current_service.get('sms_sender')
|
||||
if form.validate_on_submit():
|
||||
if form.sms_sender.data:
|
||||
message = 'Text message sender set to {}'.format(form.sms_sender.data)
|
||||
else:
|
||||
message = 'Text message sender removed'
|
||||
service_api_client.update_service(
|
||||
current_service['id'],
|
||||
sms_sender=form.sms_sender.data or None
|
||||
)
|
||||
flash(message, 'default_with_tick')
|
||||
return redirect(url_for('.service_settings', service_id=service_id))
|
||||
return render_template(
|
||||
'views/service-settings/set-sms-sender.html',
|
||||
|
||||
Reference in New Issue
Block a user