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:
Martyn Inglis
2016-09-05 14:29:58 +01:00
64 changed files with 1455 additions and 814 deletions

View File

@@ -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):

View File

@@ -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(

View File

@@ -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')

View File

@@ -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',

View File

@@ -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
]

View File

@@ -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(

View File

@@ -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',