Merge pull request #234 from DanielGrams/issue/233

Optimize date time input #233
This commit is contained in:
Daniel Grams 2021-07-24 19:14:48 +02:00 committed by GitHub
commit da0cfad9fa
9 changed files with 204 additions and 103 deletions

View File

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

View File

@ -19,7 +19,7 @@ def create_option_string(count, value):
result = ""
for i in range(count):
selected = " selected" if i == value else ""
result = result + '<option value="%02d"%s>%02d</option>' % (i, selected, i)
result = result + '<option value="%d"%s>%02d</option>' % (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(
'<div class="input-group-prepend mt-1"><input type="text" {}/><button class="btn btn-outline-secondary" type="button" id="{}"><i class="fa fa-times"></i></button></div><div class="mx-2"></div><div class="input-group-append mt-1"><select {}>{}</select><span class="input-group-text">:</span><select {}>{}</select></div>'.format(
'<div class="input-group-prepend mt-1"><input type="text" {}/></div><div class="mx-2"></div><div class="input-group-append mt-1"><select {}>{}</select><span class="input-group-text">:</span><select {}>{}</select></div>'.format(
date_params,
clear_button_id,
time_hour_params,
create_option_string(24, hour),
time_minute_params,

View File

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

View File

@ -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 %}
<div class="form-group {% if field.errors %} has-error{% endif -%}">
{% if 'ri' in kwargs and kwargs['ri'] == 'checkbox' %}
{% else %}
{{ field.label(text=label_text, class="mb-0") }}
{% endif %}
{% if field.description %}
<div class="form-text mt-0 text-muted w-100">
{{ field.description }}
</div>
{% endif %}
<div class="input-group">
{% 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' %}
<fieldset class="form-group">
{% for choice in field %}
<div class="form-check">
{{ choice(class="form-check-input") }}
{{ choice.label(class="form-check-label") }}
</div>
{% endfor %}
</fieldset>
{% elif 'ri' in kwargs and kwargs['ri'] == 'multicheckbox-inline' %}
<fieldset class="form-group my-auto">
{% for choice in field %}
<div class="form-check form-check-inline">
{{ choice(class="form-check-input") }}
{{ choice.label(class="form-check-label") }}
</div>
{% endfor %}
</fieldset>
{% elif 'ri' in kwargs and kwargs['ri'] == 'checkbox' %}
<div class="form-check">
{{ field(class="form-check-input") }}
{{ field.label(class="form-check-label") }}
</div>
{% elif 'ri' in kwargs and kwargs['ri'] == 'switch' %}
<div class="custom-control custom-switch">
{{ field(class="custom-control-input") }}
<label class="custom-control-label" for="{{ field.id }}"></label>
</div>
{% 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' %}
<script type="text/javascript">
$( function() {
$("textarea[name=recurrence_rule]").recurrenceinput({lang:'de', startField: "start", ajaxURL: "{{ url_for('event_rrule') }}", firstDay: 1});
});
</script>
{% endif %}
{% endif %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
{% set is_collapsible = kwargs['is_collapsible'] if 'is_collapsible' in kwargs else False %}
{% if is_collapsible %}
<div class="mb-4" id="{{ field.id }}-show-container"{% if field.data %} style="display: none;"{% endif %}>
<a href="#" class="show-link" data-container="{{ field.id }}-container" data-show-container="{{ field.id }}-show-container"><i class="fa fa-plus"></i> {{ field.label.text }}</a>
</div>
<div id="{{ field.id }}-container"{% if not field.data %} style="display: none;"{% endif %}>
{% endif %}
{% 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 %}
<div class="form-group {% if field.errors %} has-error{% endif -%}">
{% if 'ri' in kwargs and kwargs['ri'] == 'checkbox' %}
{% else %}
{{ field.label(text=label_text, class="mb-0") }}
{% endif %}
{% if field.description %}
<div class="form-text mt-0 text-muted w-100">
{{ field.description }}
</div>
{% endif %}
<div class="input-group">
{% 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' %}
<fieldset class="form-group">
{% for choice in field %}
<div class="form-check">
{{ choice(class="form-check-input") }}
{{ choice.label(class="form-check-label") }}
</div>
{% endfor %}
</fieldset>
{% elif 'ri' in kwargs and kwargs['ri'] == 'multicheckbox-inline' %}
<fieldset class="form-group my-auto">
{% for choice in field %}
<div class="form-check form-check-inline">
{{ choice(class="form-check-input") }}
{{ choice.label(class="form-check-label") }}
</div>
{% endfor %}
</fieldset>
{% elif 'ri' in kwargs and kwargs['ri'] == 'checkbox' %}
<div class="form-check">
{{ field(class="form-check-input") }}
{{ field.label(class="form-check-label") }}
</div>
{% elif 'ri' in kwargs and kwargs['ri'] == 'switch' %}
<div class="custom-control custom-switch">
{{ field(class="custom-control-input") }}
<label class="custom-control-label" for="{{ field.id }}"></label>
</div>
{% 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' %}
<script type="text/javascript">
$( function() {
$("textarea[name=recurrence_rule]").recurrenceinput({lang:'de', startField: "start", ajaxURL: "{{ url_for('event_rrule') }}", firstDay: 1});
});
</script>
{% endif %}
{% endif %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
<div>{{ error }}</div>
{% endfor %}
</div>
{% endif %}
</div>
{% if is_collapsible %}
<div class="mb-4" id="{{ field.id }}-hide-container">
<a href="#" class="hide-link" data-container="{{ field.id }}-container" data-show-container="{{ field.id }}-show-container"><i class="fa fa-minus"></i> {{ field.label.text }}</a>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro render_field(field) %}
@ -1151,10 +1166,6 @@ if (URL) {
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.2/additional-methods.min.js" integrity="sha512-6Uv+497AWTmj/6V14BsQioPrm3kgwmK9HYIyWP+vClykX52b0zrDGP7lajZoIY1nNlX4oQuh7zsGjmF7D0VZYA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.2/localization/messages_de.min.js" integrity="sha512-CZoLR7uTljYchtaY9SbWetTDxZ7bW3h6YALF4orf6k+WTvZhr4zu+a9XqhHkK+zsKbQL5HNXTNrd21TE3M6eUg==" crossorigin="anonymous"></script>
<script>
function get_moment_with_time(field_id) {
return moment($(field_id).val()).add($(field_id + "-hour").val(), "hour").add($(field_id + "-minute").val(), "minute");
}
jQuery.validator.addMethod("dateRange", function(value, element, params) {
var start_id = params[0];
var end_id = params[1];
@ -1195,16 +1206,30 @@ if (URL) {
element.closest('.input-group').after(error);
},
highlight: function (element, errorClass, validClass) {
$(element).removeClass('is-valid').addClass('is-invalid');
$(element).closest('.input-group').find(':input').removeClass('is-valid').addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid').addClass('is-valid');;
$(element).closest('.input-group').find(':input').removeClass('is-invalid').addClass('is-valid');
},
onclick: function (element, event) {
return true;
}
});
</script>
{% 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() %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.9/cropper.js" integrity="sha512-oqBsjjSHWqkDx4UKoU+5IUZN2nW2qDp2GFSKw9+mcFm+ZywqfBKp79nfWmGPco2wzTWuE46XpjtCjZ9tFmI12g==" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.9/cropper.css" integrity="sha512-949FvIQOibfhLTgmNws4F3DVlYz3FmCRRhJznR22hx76SKkcpZiVV5Kwo0iwK9L6BFuY+6mpdqB2+vDIGVuyHg==" crossorigin="anonymous" />

View File

@ -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() }}
});
</script>
{% endblock %}
@ -85,8 +87,8 @@ $( function() {
{{ _('Event date') }}
</div>
<div class="card-body">
{{ 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") }}
</div>
</div>

View File

@ -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() }}
});
</script>
{% endblock %}
@ -75,8 +77,8 @@
{{ _('Event date') }}
</div>
<div class="card-body">
{{ 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") }}
</div>
</div>

View File

@ -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() }}
});
</script>
{% 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") }}
<div class="d-flex justify-content-between">

View File

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

View File

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