From 71c8ac941515c85d786cfebf15e7ed7c3deda31c Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sat, 24 Jul 2021 19:05:58 +0200 Subject: [PATCH] Optimize date time input #233 --- project/dateutils.py | 10 + project/forms/widgets.py | 6 +- project/static/site.js | 74 ++++++- project/templates/_macros.html | 185 ++++++++++-------- project/templates/event/create.html | 8 +- project/templates/event/update.html | 8 +- .../widget/event_suggestion/create.html | 8 +- project/views/event.py | 4 + project/views/widget.py | 4 + 9 files changed, 204 insertions(+), 103 deletions(-) diff --git a/project/dateutils.py b/project/dateutils.py index bdd7ef4..4df0afc 100644 --- a/project/dateutils.py +++ b/project/dateutils.py @@ -42,6 +42,16 @@ def date_set_end_of_day(date): return date_add_time(date, hour=23, minute=59, second=59) +def round_to_next_full_hour(date): + new_date = date + timedelta(hours=1) + return date_add_time(date, new_date.hour, tzinfo=date.tzinfo) + + +def get_next_full_hour(): + now = get_now() + return round_to_next_full_hour(now) + + def form_input_to_date(date_str, hour=0, minute=0, second=0): date = datetime.strptime(date_str, "%Y-%m-%d") date_time = date_add_time(date, hour=hour, minute=minute, second=second) diff --git a/project/forms/widgets.py b/project/forms/widgets.py index 707af62..8cf791d 100644 --- a/project/forms/widgets.py +++ b/project/forms/widgets.py @@ -19,7 +19,7 @@ def create_option_string(count, value): result = "" for i in range(count): selected = " selected" if i == value else "" - result = result + '' % (i, selected, i) + result = result + '' % (i, selected, i) return result @@ -51,12 +51,10 @@ class CustomDateTimeWidget: time_minute_params = html_params( name=field.name, id=id + "-minute", class_=kwargs_class, **kwargs ) - clear_button_id = id + "-clear-button" return Markup( - '
:
'.format( + '
:
'.format( date_params, - clear_button_id, time_hour_params, create_option_string(24, hour), time_minute_params, diff --git a/project/static/site.js b/project/static/site.js index aca58fd..405d5b1 100644 --- a/project/static/site.js +++ b/project/static/site.js @@ -98,13 +98,24 @@ jQuery.tools.recurrenceinput.localize('de', { } }); +function get_moment_with_time(field_id) { + return moment($(field_id).val()).add($(field_id + "-hour").val(), "hour").add($(field_id + "-minute").val(), "minute"); +} + function set_date_bounds(picker) { var data_range_to_attr = picker.attr('data-range-to'); - if (data_range_to_attr) { - from_date = picker.datepicker("getDate"); - from_moment = moment(from_date); - $(data_range_to_attr + '-user').datepicker("option", "minDate", from_date); + var hidden_field_id = picker.attr('id').replace('-user', ''); + var from_moment = get_moment_with_time('#'+hidden_field_id); + $(data_range_to_attr + '-user').datepicker("option", "minDate", from_moment.toDate()); + + var end_val = $(data_range_to_attr).val(); + if (end_val != '') { + var end_moment = get_moment_with_time(data_range_to_attr); + if (end_moment < from_moment) { + set_picker_date($(data_range_to_attr), from_moment.toDate()); + } + } var data_range_max_attr = picker.attr('data-range-max-days'); if (data_range_max_attr) { @@ -112,11 +123,31 @@ function set_date_bounds(picker) { $(data_range_to_attr + '-user').datepicker("option", "maxDate", from_moment.toDate()); } } + + var data_range_from_attr = picker.attr('data-range-from'); + if (data_range_from_attr) { + var hidden_field_id = picker.attr('id').replace('-user', ''); + var to_moment = get_moment_with_time('#'+hidden_field_id); + + var start_val = $(data_range_from_attr).val(); + if (start_val != '') { + var start_moment = get_moment_with_time(data_range_from_attr); + if (start_moment > to_moment) { + set_picker_date($(data_range_from_attr), to_moment.toDate()); + } + } + } } function set_picker_date(picker, date, timeout = -1) { picker.datepicker("setDate", date); + var hidden_field_id = picker.attr('id').replace('-user', ''); + var hour = date == null ? 0 : date.getHours(); + var minute = date == null ? 0 : date.getMinutes(); + $("#" + hidden_field_id + "-hour").val(hour.toString()); + $("#" + hidden_field_id + "-minute").val(minute.toString()); + if (timeout < 0) { set_date_bounds(picker); } else { @@ -150,16 +181,15 @@ function start_datepicker(input) { var hidden_value = hidden_field.val(); if (hidden_value) { - set_picker_date(picker, moment(hidden_value).toDate(), 100) + set_picker_date(picker, get_moment_with_time('#'+hidden_field_id).toDate(), 100) } hidden_field.after(user_field); - $("#" + hidden_field_id + "-clear-button").click(function() { - set_picker_date(picker, null) - $("#" + hidden_field_id + "-hour").val("00"); - $("#" + hidden_field_id + "-minute").val("00"); - }); + var data_range_to_attr = picker.attr('data-range-to'); + if (data_range_to_attr) { + $(data_range_to_attr).attr('data-range-from', '#'+hidden_field_id); + } hidden_field.change(function() { var hidden_value = hidden_field.val(); @@ -184,6 +214,14 @@ function start_datepicker(input) { } }); + $("#" + hidden_field_id + "-hour").change(function() { + set_date_bounds(picker); + }); + + $("#" + hidden_field_id + "-minute").change(function() { + set_date_bounds(picker); + }); + return picker; } @@ -274,6 +312,22 @@ $( function() { e.stopPropagation(); $(this).removeClass('dragover'); }); + + $('.show-link').click(function(e){ + e.preventDefault(); + e.stopPropagation(); + $('#'+$(this).attr('data-show-container')).hide(); + $('#'+$(this).attr('data-container')).show(); + $('#'+$(this).attr('data-container')).trigger('shown'); + }); + + $('.hide-link').click(function(e){ + e.preventDefault(); + e.stopPropagation(); + $('#'+$(this).attr('data-show-container')).show(); + $('#'+$(this).attr('data-container')).hide(); + $('#'+$(this).attr('data-container')).trigger('hidden'); + }); }); String.prototype.truncate = String.prototype.truncate || diff --git a/project/templates/_macros.html b/project/templates/_macros.html index 74fc68b..dbe64e4 100644 --- a/project/templates/_macros.html +++ b/project/templates/_macros.html @@ -1,79 +1,94 @@ {% macro render_field_with_errors(field) %} - {% set is_required = kwargs['is_required'] if 'is_required' in kwargs else field.flags.required %} - {% set label_text = field.label.text + ' *' if is_required else field.label.text %} -
- {% if 'ri' in kwargs and kwargs['ri'] == 'checkbox' %} - {% else %} - {{ field.label(text=label_text, class="mb-0") }} - {% endif %} - - {% if field.description %} -
- {{ field.description }} -
- {% endif %} - -
- {% set field_class = kwargs['class'] if 'class' in kwargs else '' %} - {% set field_class = field_class + ' form-control' %} - {% if field.errors %} - {% set field_class = field_class + ' is-invalid' %} - {% endif %} - - {% if 'ri' in kwargs and kwargs['ri'] == 'multicheckbox' %} -
- {% for choice in field %} -
- {{ choice(class="form-check-input") }} - {{ choice.label(class="form-check-label") }} -
- {% endfor %} -
- {% elif 'ri' in kwargs and kwargs['ri'] == 'multicheckbox-inline' %} -
- {% for choice in field %} -
- {{ choice(class="form-check-input") }} - {{ choice.label(class="form-check-label") }} -
- {% endfor %} -
- {% elif 'ri' in kwargs and kwargs['ri'] == 'checkbox' %} -
- {{ field(class="form-check-input") }} - {{ field.label(class="form-check-label") }} -
- {% elif 'ri' in kwargs and kwargs['ri'] == 'switch' %} -
- {{ field(class="custom-control-input") }} - -
- {% else %} - {% if 'class' in kwargs %} - {% set _dummy=kwargs.pop('class') %} - {% endif %} - {{ field(class=field_class, **kwargs)|safe }} - {% endif %} - - {% if 'ri' in kwargs %} - {% if kwargs['ri'] == 'rrule' %} - - {% endif %} - {% endif %} -
- - {% if field.errors %} -
- {% for error in field.errors %} -
{{ error }}
- {% endfor %} -
- {% endif %} + {% set is_collapsible = kwargs['is_collapsible'] if 'is_collapsible' in kwargs else False %} + {% if is_collapsible %} + + + {% endif %} {% endmacro %} {% macro render_field(field) %} @@ -1151,10 +1166,6 @@ if (URL) { {% endmacro %} +{% macro render_end_container_handling() %} +$('#end-container').on('shown', function() { + var end_moment = get_moment_with_time('#start').add(3, 'hours'); + set_picker_date($('#end-user'), end_moment.toDate()); +}); + +$('#end-container').on('hidden', function() { + set_picker_date($('#end-user'), null); +}); +{% endmacro %} + {% macro render_cropper_header() %} diff --git a/project/templates/event/create.html b/project/templates/event/create.html index 9813969..8d833cb 100644 --- a/project/templates/event/create.html +++ b/project/templates/event/create.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_jquery_steps_header, render_google_place_autocomplete_field, render_google_place_autocomplete_header, render_cropper_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %} +{% from "_macros.html" import render_end_container_handling, render_jquery_steps_header, render_google_place_autocomplete_field, render_google_place_autocomplete_header, render_cropper_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %} {%- block title -%} {{ _('Create event') }} {%- endblock -%} @@ -60,6 +60,8 @@ $( function() { }); update_organizer_container($('input[type=radio][name=organizer_choice]:checked').val()); + + {{ render_end_container_handling() }} }); {% endblock %} @@ -85,8 +87,8 @@ $( function() { {{ _('Event date') }}
- {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "1"}) }} - {{ render_field_with_errors(form.end) }} + {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "14"}) }} + {{ render_field_with_errors(form.end, is_collapsible=1) }} {{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
diff --git a/project/templates/event/update.html b/project/templates/event/update.html index c5eff0f..e970803 100644 --- a/project/templates/event/update.html +++ b/project/templates/event/update.html @@ -1,5 +1,5 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_jquery_steps_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %} +{% from "_macros.html" import render_end_container_handling, render_jquery_steps_header, render_cropper_code, render_crop_image_form_section, render_radio_buttons, render_field_with_errors, render_field %} {%- block title -%} {{ _('Update event') }} {%- endblock -%} @@ -50,6 +50,8 @@ window.open('{{ url_for("manage_admin_unit_places_create", id=event.admin_unit_id) }}'); return false; }); + + {{ render_end_container_handling() }} }); {% endblock %} @@ -75,8 +77,8 @@ {{ _('Event date') }}
- {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "1"}) }} - {{ render_field_with_errors(form.end) }} + {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "14"}) }} + {{ render_field_with_errors(form.end, is_collapsible=1) }} {{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
diff --git a/project/templates/widget/event_suggestion/create.html b/project/templates/widget/event_suggestion/create.html index af270ec..2515900 100644 --- a/project/templates/widget/event_suggestion/create.html +++ b/project/templates/widget/event_suggestion/create.html @@ -1,5 +1,5 @@ {% extends "layout_widget.html" %} -{% from "_macros.html" import render_logo, render_cropper_code, render_crop_image_form, render_jquery_steps_header, render_cropper_header, render_radio_buttons, render_field_with_errors, render_field %} +{% from "_macros.html" import render_end_container_handling, render_logo, render_cropper_code, render_crop_image_form, render_jquery_steps_header, render_cropper_header, render_radio_buttons, render_field_with_errors, render_field %} {%- block title -%} {{ _('Create event suggestion') }} {%- endblock -%} @@ -114,6 +114,8 @@ } }); + {{ render_end_container_handling() }} + }); {% endblock %} @@ -198,8 +200,8 @@ {{ render_field_with_errors(form.name) }} {{ render_field_with_errors(form.event_place_id, class="w-100") }} {{ render_field_with_errors(form.organizer_id, class="w-100") }} - {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "1"}) }} - {{ render_field_with_errors(form.end) }} + {{ render_field_with_errors(form.start, **{"data-range-to":"#end", "data-range-max-days": "14"}) }} + {{ render_field_with_errors(form.end, is_collapsible=1) }} {{ render_field_with_errors(form.recurrence_rule, ri="rrule") }}
diff --git a/project/views/event.py b/project/views/event.py index 60615b7..1dc267e 100644 --- a/project/views/event.py +++ b/project/views/event.py @@ -15,6 +15,7 @@ from project.access import ( has_access, has_admin_unit_member_permission, ) +from project.dateutils import get_next_full_hour from project.forms.event import CreateEventForm, DeleteEventForm, UpdateEventForm from project.jsonld import DateTimeEncoder, get_sd_for_event_date from project.models import ( @@ -279,6 +280,9 @@ def prepare_event_form(form, admin_unit): form.organizer_id.choices.insert(0, (0, "")) form.event_place_id.choices.insert(0, (0, "")) + if not form.start.data: + form.start.data = get_next_full_hour() + def prepare_event_form_for_suggestion(form, event_suggestion): form.name.data = event_suggestion.name diff --git a/project/views/widget.py b/project/views/widget.py index f035323..83fcee1 100644 --- a/project/views/widget.py +++ b/project/views/widget.py @@ -8,6 +8,7 @@ from sqlalchemy.sql import func from project import app, db from project.access import has_admin_unit_member_permission +from project.dateutils import get_next_full_hour from project.forms.event_date import FindEventDateForm from project.forms.event_suggestion import CreateEventSuggestionForm from project.jsonld import DateTimeEncoder, get_sd_for_event_date @@ -136,6 +137,9 @@ def event_suggestion_create_for_admin_unit(au_short_name): form.category_ids.choices = get_event_category_choices() + if not form.start.data: + form.start.data = get_next_full_hour() + if form.validate_on_submit(): event_suggestion = EventSuggestion() form.populate_obj(event_suggestion)