Add a component for picking the time to send a job

Users need to pick a time in the next 24hrs, or send a file immediately.

Rationale for this is a bit lost in time-before-holiday, but generally:

‘Now’ and ‘later’ as the inital choices makes it really clear what
this feature is about conceptually.

The choice of times is absolute, eg ‘1pm’ not ‘in 3 hours’
This commit is contained in:
Chris Hill-Scott
2016-08-07 09:17:49 +01:00
parent ee447ba6a4
commit 225a61ddd3
17 changed files with 288 additions and 12 deletions

View File

@@ -0,0 +1,92 @@
(function(Modules) {
"use strict";
var render = ($options, $button) => (
filterOptionVisibility($options) && setButtonState($options, $button)
);
var filterOptionVisibility = $options => $options
.removeClass('js-visible')
.filter(
(index, element) => (index === 0 || $(element).has(':checked').length)
)
.addClass('js-visible');
var setButtonState = ($options, $button) => $button
.addClass('js-visible')
.prop(
'value',
$options.has(':checked').find('input').attr('id') === $options.eq(0).find('input').attr('id') ?
'Later' : 'Choose a different time'
);
// Workaround because GOV.UK SelectionButtons doesnt deselect in this case
var deselectUnchecked = $options => $options
.filter(
(index, element) => $(element).not(':has(:checked)')
).removeClass('selected');
var refocus = $element => setTimeout(
() => $element.blur().trigger('focus'),
10
);
var renderIfComponentLosesFocus = ($options, $button, $focused) => () =>
($focused.attr('type') !== 'radio') &&
render($options, $button) &&
refocus($focused); // Make sure that window scrolls to focused element
Modules.RadioSelect = function() {
this.start = function(component) {
let $component = $(component);
let $options = $('label', $component);
$component.append(
$button = $('<input type="button" value="Later" class="tertiary-button" />')
);
$button.on('click', () =>
$options.addClass('js-visible').has(':checked').focus() &&
$button.removeClass('js-visible')
);
$component.on('keydown', 'input[type=radio]', function() {
// intercept keypresses which arent enter or space
if (event.which !== 13 && event.which !== 32) {
setTimeout(
renderIfComponentLosesFocus($options, $button, $(document.activeElement)),
200
);
return true;
}
event.preventDefault();
render($options, $button);
refocus($(this));
});
$component.on('click', 'input[type=radio]', function(event) {
deselectUnchecked($options);
// stop click being triggered by keyboard events
if (!event.pageX) return true;
render($options, $button);
refocus($(this));
});
render($options, $button);
};
};
})(window.GOVUK.Modules);

View File

@@ -0,0 +1,47 @@
.radio-select {
&-column {
display: inline-block;
vertical-align: top;
.block-label {
margin-right: 10px;
}
}
.tertiary-button {
display: inline-block;
vertical-align: top;
width: auto;
padding: 20px 30px 15px 30px;
}
.js-enabled & {
.block-label {
&:last-child {
margin-bottom: 10px;
}
}
.block-label,
.tertiary-button {
display: none;
}
.js-visible {
display: block;
&.tertiary-button {
display: inline-block;
}
}
}
}

View File

@@ -32,7 +32,7 @@
.sms-message-recipient {
@include copy-19;
color: $secondary-text-colour;
margin: 10px 0 5px 0;
margin: 10px 0 0 0;
}
.sms-message-name {

View File

@@ -49,6 +49,7 @@ $path: '/static/images/';
@import 'components/email-message';
@import 'components/api-key';
@import 'components/vendor/previous-next-navigation';
@import 'components/radio-select';
@import 'components/pill';
@import 'components/secondary-button';
@import 'components/show-more';

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]

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

@@ -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
@@ -276,6 +276,7 @@ def check_messages(service_id, template_type, upload_id):
upload_id=upload_id,
form=CsvUploadForm(),
remaining_messages=remaining_messages,
choose_time_form=ChooseTimeForm(),
back_link=back_link,
help=get_help_argument()
)
@@ -310,7 +311,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

@@ -58,13 +58,15 @@ class JobApiClient(BaseAPIClient):
return jobs
def create_job(self, job_id, service_id, template_id, original_file_name, notification_count):
def create_job(self, job_id, service_id, template_id, original_file_name, notification_count, scheduled_for=None):
data = {
"id": job_id,
"template": template_id,
"original_file_name": original_file_name,
"notification_count": notification_count
}
if scheduled_for:
data.update({'scheduled_for': scheduled_for})
data = _attach_current_user(data)
job = self.post(url='/service/{}/job'.format(service_id), data=data)

View File

@@ -23,6 +23,40 @@
{% endmacro %}
{% macro radio_select(
field,
hint=None,
wrapping_class='form-group'
) %}
<div class="{{ wrapping_class }} {% if field.errors %} error{% endif %}">
<fieldset>
<legend class="form-label">
{{ field.label }}
{% if field.errors %}
<span class="error-message">
{{ field.errors[0] }}
</span>
{% endif %}
</legend>
<div class="radio-select" data-module="radio-select">
<div class="radio-select-column">
{% for option in field %}
<label class="block-label" for="{{ option.id }}">
{{ option }}
{{ option.label.text }}
</label>
{% if loop.first %}
</div>
<div class="radio-select-column">
{% endif %}
{% endfor %}
</div>
</div>
</fieldset>
</div>
{% endmacro %}
{% macro branding_radios(
field,
hint=None,

View File

@@ -1,6 +1,7 @@
{% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/email-message.html" import email_message %}
{% from "components/radios.html" import radio_select %}
{% from "components/sms-message.html" import sms_message %}
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
{% from "components/file-upload.html" import file_upload %}
@@ -144,6 +145,10 @@
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="help" value="{{ '3' if help else 0 }}" />
{{ radio_select(
choose_time_form.scheduled_for,
wrapping_class='bottom-gutter-2-3'
) }}
<input type="submit" class="button" value="Send {{ count_of_recipients }} {{ message_count_label(count_of_recipients, template.template_type, suffix='') }}" />
<a href="{{ back_link }}" class="page-footer-back-link">Back</a>
</form>

View File

@@ -9,7 +9,6 @@
<div class="grid-row">
<div class="column-two-thirds">
<h1 class="heading-large">Trial mode</h1>
<p>
All new accounts on Notify start off in trial mode.
</p>