diff --git a/app/__init__.py b/app/__init__.py
index 0fd40481c..88e2e8d25 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -117,6 +117,7 @@ def create_app():
application.add_template_filter(format_date)
application.add_template_filter(format_date_normal)
application.add_template_filter(format_date_short)
+ application.add_template_filter(format_datetime_relative)
application.add_template_filter(format_delta)
application.add_template_filter(format_notification_status)
application.add_template_filter(format_notification_status_as_time)
@@ -232,6 +233,23 @@ def format_datetime_short(date):
)
+def format_datetime_relative(date):
+ return '{} at {}'.format(
+ get_human_day(date),
+ format_time(date)
+ )
+
+
+def get_human_day(time):
+ # Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
+ time = (gmt_timezones(time) - timedelta(hours=1)).strftime('%A')
+ if time == datetime.utcnow().strftime('%A'):
+ return 'today'
+ if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
+ return 'tomorrow'
+ return time
+
+
def format_time(date):
return {
'12:00AM': 'Midnight',
diff --git a/app/assets/javascripts/radioSelect.js b/app/assets/javascripts/radioSelect.js
index 8923376ef..8d359cc7d 100644
--- a/app/assets/javascripts/radioSelect.js
+++ b/app/assets/javascripts/radioSelect.js
@@ -2,88 +2,144 @@
"use strict";
- var render = ($options, $button) => (
- filterOptionVisibility($options) && setButtonState($options, $button)
- );
+ let states = {
+ 'initial': Hogan.compile(`
+
+
+
+
+ {{#categories}}
+
+ {{/categories}}
+
+ `),
+ 'choose': Hogan.compile(`
+
+
+
+
+ {{#choices}}
+
+ {{/choices}}
+
+ `),
+ 'chosen': Hogan.compile(`
+
+
+
+
+ {{#choices}}
+
+ {{/choices}}
+
+
+
+
+ `)
+ };
- 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'
+ let focusSelected = function() {
+ setTimeout(
+ () => $('[type=radio]:checked').parent('label').blur().trigger('focus').addClass('selected'),
+ 10
);
-
- // Workaround because GOV.UK SelectionButtons doesn’t 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);
+ let render = (state, data) => $component.html(states[state].render(data));
+ let choices = $('label', $component).toArray().map(function(element) {
+ let $element = $(element);
+ return {
+ 'id': $element.attr('for'),
+ 'label': $.trim($element.text()),
+ 'value': $element.find('input').attr('value')
+ };
+ });
+ let categories = $component.data('categories').split(',');
+ let name = $component.find('input').eq(0).attr('name');
- $component.append(
- $button = $('')
- );
+ $component
+ .on('click', '.js-category-button', function(event) {
- $button.on('click', () =>
- $options.addClass('js-visible').has(':checked').focus() &&
- $button.removeClass('js-visible')
- );
+ event.preventDefault();
+ let wordsInDay = $(this).attr('value').split(' ');
+ let day = wordsInDay[wordsInDay.length - 1].toLowerCase();
+ render('choose', {
+ 'choices': choices.filter(
+ element => element.label.toLowerCase().indexOf(day) > -1
+ ),
+ 'name': name
+ });
+ $('.js-option').eq(0).parent('label').trigger('focus');
- $component.on('keydown', 'input[type=radio]', function() {
+ })
+ .on('click', '.js-option', function(event) {
- // intercept keypresses which aren’t enter or space
- if (event.which !== 13 && event.which !== 32) {
- setTimeout(
- renderIfComponentLosesFocus($options, $button, $(document.activeElement)),
- 200
- );
- return true;
- }
+ // stop click being triggered by keyboard events
+ if (!event.pageX) return true;
- event.preventDefault();
+ event.preventDefault();
+ let value = $(this).attr('value');
+ render('chosen', {
+ 'choices': choices.filter(
+ element => element.value == value
+ ),
+ 'name': name
+ });
+ focusSelected();
- render($options, $button);
- refocus($(this));
+ })
+ .on('keydown', 'input[type=radio]', function(event) {
+ // intercept keypresses which aren’t enter or space
+ if (event.which !== 13 && event.which !== 32) {
+ return true;
+ }
+
+ event.preventDefault();
+ let value = $(this).attr('value');
+ render('chosen', {
+ 'choices': choices.filter(
+ element => element.value == value
+ ),
+ 'name': name
+ });
+ focusSelected();
+
+ })
+ .on('click', '.js-reset-button', function(event) {
+
+ event.preventDefault();
+ render('initial', {
+ 'categories': categories,
+ 'name': name
+ });
+ focusSelected();
+
+ });
+
+ render('initial', {
+ 'categories': categories,
+ 'name': name
});
- $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);
+ $component.css({'height': 'auto'});
};
diff --git a/app/assets/stylesheets/components/radio-select.scss b/app/assets/stylesheets/components/radio-select.scss
index 7462eed1e..1440d0ad6 100644
--- a/app/assets/stylesheets/components/radio-select.scss
+++ b/app/assets/stylesheets/components/radio-select.scss
@@ -6,7 +6,9 @@
vertical-align: top;
.block-label {
- margin-right: 10px;
+ margin-right: 5px;
+ padding-right: $gutter - 10px;
+ padding-left: 54px - 10px;
}
}
@@ -15,31 +17,21 @@
display: inline-block;
vertical-align: top;
width: auto;
- padding: 20px 30px 15px 30px;
+ padding: 20px $gutter-half 15px $gutter-half;
+ margin-right: 5px;
}
.js-enabled & {
+ height: 60px;
+ overflow: visible;
.block-label {
- &:last-child {
- margin-bottom: 10px;
- }
- }
-
- .block-label,
- .tertiary-button {
display: none;
}
- .js-visible {
-
- display: block;
-
- &.tertiary-button {
- display: inline-block;
- }
-
+ .js-block-label {
+ display: inline-block;
}
}
diff --git a/app/main/forms.py b/app/main/forms.py
index 650f72891..1695ac682 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -27,27 +27,58 @@ from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoC
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')))
+ '{} at {}'.format(
+ get_human_day(future_time.astimezone(pytz.timezone('Europe/London'))),
+ get_human_time(future_time.astimezone(pytz.timezone('Europe/London')))
+ )
)
def get_human_time(time):
return {
- '0': 'Midnight',
- '12': 'Midday'
+ '0': 'midnight',
+ '12': 'midday'
}.get(
time.strftime('%-H'),
time.strftime('%-I%p').lower()
)
-def get_next_hours_from(now, hours=23):
+def get_human_day(time, prefix_today_with='T'):
+ # Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
+ time = (time - timedelta(hours=1)).strftime('%A')
+ if time == datetime.utcnow().strftime('%A'):
+ return '{}oday'.format(prefix_today_with)
+ if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
+ return 'Tomorrow'
+ return time
+
+
+def get_furthest_possible_scheduled_time():
+ return (datetime.utcnow() + timedelta(days=4)).replace(hour=0)
+
+
+def get_next_hours_until(until):
+ now = datetime.utcnow()
+ hours = int((until - now).total_seconds() / (60 * 60))
return [
(now + timedelta(hours=i)).replace(minute=0, second=0).replace(tzinfo=pytz.utc)
for i in range(1, hours + 1)
]
+def get_next_days_until(until):
+ now = datetime.utcnow()
+ days = int((until - now).total_seconds() / (60 * 60 * 24))
+ return [
+ get_human_day(
+ (now + timedelta(days=i)).replace(tzinfo=pytz.utc),
+ prefix_today_with='Later t'
+ )
+ for i in range(0, days + 1)
+ ]
+
+
def email_address(label='Email address', gov_user=True):
validators = [
Length(min=5, max=255),
@@ -310,8 +341,11 @@ 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())
+ get_time_value_and_label(hour) for hour in get_next_hours_until(
+ get_furthest_possible_scheduled_time()
+ )
]
+ self.scheduled_for.categories = get_next_days_until(get_furthest_possible_scheduled_time())
scheduled_for = RadioField(
'When should Notify send these messages?',
diff --git a/app/main/views/send.py b/app/main/views/send.py
index 9ed33784d..90f423d13 100644
--- a/app/main/views/send.py
+++ b/app/main/views/send.py
@@ -21,7 +21,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, ChooseTimeForm
+from app.main.forms import CsvUploadForm, ChooseTimeForm, get_next_days_until, get_furthest_possible_scheduled_time
from app.main.uploader import (
s3upload,
s3download
diff --git a/app/templates/components/radios.html b/app/templates/components/radios.html
index 3a0f19a7c..c361c443e 100644
--- a/app/templates/components/radios.html
+++ b/app/templates/components/radios.html
@@ -38,7 +38,7 @@
{% endif %}
-
+
{% for option in field %}