From 812cfabfde68397b0a9b3821a11c8cb9eab42cf2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Mon, 11 Jan 2021 13:57:32 +0100
Subject: [PATCH 1/4] fields: add date/times visualisation to item fields
 (#50035)

---
 wcs/data_sources.py                           |  11 +-
 wcs/fields.py                                 |   4 +
 wcs/qommon/form.py                            |   2 +
 wcs/qommon/static/js/qommon.admin.js          |  12 +-
 .../forms/widgets/select-datetimes.html       | 189 ++++++++++++++++++
 5 files changed, 214 insertions(+), 4 deletions(-)
 create mode 100644 wcs/qommon/templates/qommon/forms/widgets/select-datetimes.html

diff --git a/wcs/data_sources.py b/wcs/data_sources.py
index 15018e923..f9ae3469d 100644
--- a/wcs/data_sources.py
+++ b/wcs/data_sources.py
@@ -68,7 +68,11 @@ class DataSourceSelectionWidget(CompositeWidget):
                 options.append(OptGroup(_('Cards')))
                 options.extend(cards_options)
 
-            nds_options = [(x.slug, x.name, x.slug, {'data-type': x.type}) for x in NamedDataSource.select()]
+            nds_options = [(
+                x.slug, x.name, x.slug, {
+                    'data-type': x.type,
+                    'data-maybe-datetimes': 'true' if x.maybe_datetimes() else 'false'})
+                for x in NamedDataSource.select()]
             nds_options.sort(key=lambda x: misc.simplify(x[1]))
             if nds_options:
                 options.append(OptGroup(_('Manually Configured Data Sources')))
@@ -76,7 +80,7 @@ class DataSourceSelectionWidget(CompositeWidget):
 
         if len(options) > 1:
             options.append(OptGroup(_('Generic Data Sources')))
-        options.append(('json', _('JSON URL'), 'json'))
+        options.append(('json', _('JSON URL'), 'json', {'data-maybe-datetimes': 'true'}))
         if allow_jsonp:
             options.append(('jsonp', _('JSONP URL'), 'jsonp'))
         if allow_geojson:
@@ -391,6 +395,9 @@ class NamedDataSource(XmlStorableObject):
             return True
         return False
 
+    def maybe_datetimes(self):
+        return self.type == 'json' and 'datetimes' in (self.data_source.get('value') or '')
+
     def migrate(self):
         changed = False
 
diff --git a/wcs/fields.py b/wcs/fields.py
index fe4b088c4..b5bf8854c 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -1572,6 +1572,9 @@ class ItemField(WidgetField, MapOptionsMixin):
             kwargs['select2'] = True
         elif display_mode == 'map':
             self.widget_class = MapMarkerSelectionWidget
+        elif display_mode == 'datetimes':
+            # SingleSelectHintWidget with custom template
+            kwargs['template-name'] = 'qommon/forms/widgets/select-datetimes.html'
 
     def get_display_value(self, value):
         data_source = data_sources.get_object(self.data_source)
@@ -1689,6 +1692,7 @@ class ItemField(WidgetField, MapOptionsMixin):
                    ('radio', _('Radio buttons'), 'radio'),
                    ('autocomplete', _('Autocomplete'), 'autocomplete'),
                    ('map', _('Map (requires geographical data)'), 'map'),
+                   ('datetimes', _('Date/times'), 'datetimes'),
                   ]
         form.add(RadiobuttonsWidget, 'display_mode',
                 title=_('Display Mode'),
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index d3820eca0..95d15ee6d 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -1858,6 +1858,8 @@ class SingleSelectHintWidget(SingleSelectWidget):
     def __init__(self, name, value=None, **kwargs):
         self.options_with_attributes = kwargs.pop('options_with_attributes', None)
         self.select2 = kwargs.pop('select2', None)
+        if 'template-name' in kwargs:
+            self.template_name = kwargs.pop('template-name')
         super(SingleSelectHintWidget, self).__init__(name, value=value, **kwargs)
 
     def add_media(self):
diff --git a/wcs/qommon/static/js/qommon.admin.js b/wcs/qommon/static/js/qommon.admin.js
index 88b05054b..ca2eaf783 100644
--- a/wcs/qommon/static/js/qommon.admin.js
+++ b/wcs/qommon/static/js/qommon.admin.js
@@ -256,14 +256,22 @@ $(function() {
     }
 
     $('[type=radio][name=display_mode]').on('change', function() {
+      // show everything
+      $('select[name="data_source$type"] option').show();
+      // then restrict
       if ($(this).val() == 'map') {
         $('input[name="data_mode"][value="data-source"]').click()
         $('select[name="data_source$type"] option:not([data-type="geojson"])').hide();
         if ($('select[name="data_source$type"] option:selected:visible').length == 0) {
           $('select[name="data_source$type"] option:visible').first().prop('selected', true);
         }
-      } else {
-        $('select[name="data_source$type"] option').show();
+      }
+      if ($(this).val() == 'datetimes') {
+        $('input[name="data_mode"][value="data-source"]').click()
+        $('select[name="data_source$type"] option:not([data-maybe-datetimes="true"])').hide();
+        if ($('select[name="data_source$type"] option:selected:visible').length == 0) {
+          $('select[name="data_source$type"] option:visible').first().prop('selected', true);
+        }
       }
     });
     $('[type=radio][name=display_mode]:checked').trigger('change');
diff --git a/wcs/qommon/templates/qommon/forms/widgets/select-datetimes.html b/wcs/qommon/templates/qommon/forms/widgets/select-datetimes.html
new file mode 100644
index 000000000..1c6cb22d3
--- /dev/null
+++ b/wcs/qommon/templates/qommon/forms/widgets/select-datetimes.html
@@ -0,0 +1,189 @@
+{% extends "qommon/forms/widget.html" %}
+{% load qommon i18n %}
+{% block widget-control %}
+<select style="display: none" id="form_{{widget.name}}" name="{{widget.name}}"
+    {% for attr in widget.attrs.items %}{{attr.0}}="{{attr.1}}"{% endfor %}>
+  <option value="">---</option>
+  {% for option in widget.get_options %}
+    {% with datetime=option.options.datetime|parse_datetime %}
+    <option{% for attr in option.attrs.items %} {{attr.0}}="{{attr.1}}"{% endfor %}
+          data-weekday="{{ datetime|date:"l" }}"
+          data-date="{{ datetime|date:"Y-m-d" }}"
+          data-time="{{ datetime|date:"H:i" }}"
+      >{{ option.description }}</option>
+    {% endwith %}
+  {% endfor %}
+</select>
+<div id="form_{{widget.name}}_table" class="meetings_table">
+</div>
+<script>
+$(function() {
+  var WEEKDAYS = ["{% trans "Sunday" %}", "{% trans "Monday" %}",
+                  "{% trans "Tuesday" %}", "{% trans "Wednesday" %}",
+                  "{% trans "Thursday" %}", "{% trans "Friday" %}",
+                  "{% trans "Saturday" %}"];
+  var $select = $('#form_{{widget.name}}');
+  var $table = $('#form_{{widget.name}}_table');
+  var column_count = 5;
+
+  function fill_with_items(items) {
+    $select.empty();
+    $('<option value=""></option>').appendTo($select);
+    for (var i=0; i<items.length; i++) {
+      var $option = $('<option></option>', {value: items[i].id, text: items[i].text});
+      if (items[i].disabled) {
+          $option.prop('disabled', true);
+      }
+      var date = new Date(items[i].datetime);
+      $option.attr('data-weekday', WEEKDAYS[date.getDay()]);
+      $option.attr('data-date', items[i].datetime.slice(0, 10));
+      $option.attr('data-time', items[i].datetime.slice(11, 16));
+      $option.appendTo($select);
+    }
+  }
+
+  $select.on('wcs:options-change', function(ev, data) {
+    if (data !== undefined) {
+      fill_with_items(data.items);
+    }
+  $table.empty();
+  var options = $select.find('option');
+  var current_date = null;
+  var current_day_div = null;
+  var current_offset = 0;
+  var nb_days = 0;
+
+  for (var i=0; i<options.length; i++) {
+    var $option = $(options[i]);
+    if ($option.attr('value') == '') continue;
+    var option_date = $option.data('date');
+    if (option_date !== current_date) {
+      var day_label = $option.text().split(' ', 3).join(' ');
+      if (Intl && Intl.DateTimeFormat) {
+        // create day label from actual date, this works for event agendas.
+        var date = new Date(option_date);
+        var month = new Intl.DateTimeFormat('fr', {month: 'long'}).format(date);
+        day_label = date.getDate() + ' ' + month + ' ' + date.getFullYear();
+      }
+      var weekday = $option.data('weekday');
+      nb_days += 1;
+      current_day_div = $('<div><div class="head">' + weekday + '<br>' + day_label + '</div></div>');
+      current_day_div.appendTo($('#form_{{widget.name}}_table'));
+      current_date = option_date;
+    }
+    var day_time = $option.data('time');
+    var option_span = $('<span class="selectable" data-idx="' + i + '">' + day_time + '</span>').appendTo(current_day_div);
+    if ($option.attr('disabled')) {
+      $(option_span).addClass('disabled').removeClass('selectable');
+    }
+    if ($option.attr('selected')) {
+      current_offset = nb_days - 1;
+      $(option_span).addClass('on');
+    }
+  }
+  var go_prev = $('<button class="prev">←</button>');
+  var go_next = $('<button class="next">→</button>');
+  go_prev.prependTo($('#form_{{widget.name}}_table'));
+  go_next.appendTo($('#form_{{widget.name}}_table'));
+  go_prev.on('click', function() {
+    current_offset = Math.max(0, current_offset - 1);
+    display(current_offset);
+    return false;
+  });
+  go_next.on('click', function() {
+    current_offset = Math.min(current_offset + 1, Math.max(0, nb_days-column_count));
+    display(current_offset);
+    return false;
+  });
+
+  function display(offset) {
+    $('#form_{{widget.name}}_table > div').each(function(idx, elem) {
+      if (idx >= offset && idx < offset+column_count) {
+        $(elem).show();
+      } else {
+        $(elem).hide();
+      }
+    });
+    if (go_prev.next().is(':visible')) {
+      go_prev.prop('disabled', true);
+    } else {
+      go_prev.prop('disabled', null);
+    }
+    if (go_next.prev().is(':visible')) {
+      go_next.prop('disabled', true);
+    } else {
+      go_next.prop('disabled', null);
+    }
+    current_offset = offset;
+  }
+
+  function set_layout() {
+    if ($select.parents('form').width() > 600) {
+      // desktop layout
+      column_count = 5;
+      $table.removeClass('mobile');
+    } else {
+      // mobile layout
+      column_count = 1;
+      $table.addClass('mobile');
+    }
+    display(current_offset);
+    t1 = new Date();
+  }
+  set_layout();
+  var layout_change_timeout_id = null;
+  $(window).on('resize', function() {
+    clearTimeout(layout_change_timeout_id);
+    layout_change_timeout_id = setTimeout(set_layout, 200);
+  });
+
+  $('#form_{{widget.name}}_table span.selectable').on('click', function() {
+   $('#form_{{widget.name}}_table span').removeClass('on');
+   $(this).addClass('on');
+   $('#form_{{widget.name}}').val($(options[$(this).data('idx')]).attr('value'));
+  });
+});
+  $select.trigger('wcs:options-change');
+});
+</script>
+{% if request.quixote_request.is_in_backoffice %}
+<style>
+div.meetings_table {
+  margin-top: 1ex;
+  display: flex;
+  width: 100%;
+}
+
+div.meetings_table > div {
+  flex: 0 1 auto;
+  width: 20%;
+  text-align: center;
+  display: none;
+}
+
+div.meetings_table div span {
+  display: block;
+  cursor: pointer;
+  padding: 0.3ex 0;
+}
+
+div.meetings_table div span.disabled {
+  opacity: 0.3;
+  cursor: not-allowed;
+}
+
+div.meetings_table div span.selectable.on {
+  background: #215D9C;
+  color: white;
+}
+
+div.meetings_table div.head {
+  padding-bottom: 1ex;
+}
+</style>
+{% endif %}
+{% endblock %}
+
+{% block widget-hint %}
+{% if widget.hint %}<div class="hint">{{widget.hint}}</div>{% endif %}
+{% endblock %}
-- 
2.30.0

