From a807a0f50c1e332b2a066b10f2a01a4b8bb6a134 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 16 Sep 2023 20:01:32 -0500 Subject: [PATCH] Add "falafel" custom date/time field type and widget finally able to edit datetime fields, but feels like a lot of assumptions to make, just to determine time zone..so keeping naive UTC on the backend still, and naive local on the frontend in general this needs more polish, but is a start.. --- tailbone/forms/__init__.py | 7 ++- tailbone/forms/core.py | 13 ++++++ tailbone/forms/types.py | 46 +++++++++++++++++++ tailbone/forms/widgets.py | 10 +++- .../static/js/tailbone.buefy.timepicker.js | 44 +++++++++++++++++- tailbone/templates/deform/datetime_falafel.pt | 23 ++++++++++ 6 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 tailbone/templates/deform/datetime_falafel.pt diff --git a/tailbone/forms/__init__.py b/tailbone/forms/__init__.py index a368f2d1..34b34a6c 100644 --- a/tailbone/forms/__init__.py +++ b/tailbone/forms/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,8 +24,7 @@ Forms Library """ -from __future__ import unicode_literals, absolute_import - -from . import types +# nb. import widgets before types, b/c types may refer to widgets from . import widgets +from . import types from .core import Form, SimpleFileImport diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index c4a7b0ea..245ee1e4 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -590,8 +590,18 @@ class Form(object): self.schema[key] = node def set_type(self, key, type_, **kwargs): + if type_ == 'datetime': self.set_renderer(key, self.render_datetime) + + elif type_ == 'datetime_falafel': + self.set_renderer(key, self.render_datetime) + self.set_node(key, types.FalafelDateTime(request=self.request)) + if kwargs.get('helptext'): + app = self.request.rattail_config.get_app() + timezone = app.get_timezone() + self.set_helptext(key, f"NOTE: all times are local to {timezone}") + elif type_ == 'datetime_local': self.set_renderer(key, self.render_datetime_local) elif type_ == 'date_plain': @@ -871,6 +881,9 @@ class Form(object): if field.cstruct is colander.null: return '[]' + if isinstance(field.schema.typ, types.FalafelDateTime): + return field.cstruct + try: return self.jsonify_value(field.cstruct) except Exception as error: diff --git a/tailbone/forms/types.py b/tailbone/forms/types.py index 0d87ae3f..173a83a2 100644 --- a/tailbone/forms/types.py +++ b/tailbone/forms/types.py @@ -26,6 +26,7 @@ Form Schema Types import re import datetime +import json from rattail.db import model from rattail.gpc import GPC @@ -33,6 +34,7 @@ from rattail.gpc import GPC import colander from tailbone.db import Session +from tailbone.forms import widgets class JQueryTime(colander.Time): @@ -72,6 +74,50 @@ class DateTimeBoolean(colander.Boolean): return datetime.datetime.utcnow() +class FalafelDateTime(colander.DateTime): + """ + Custom schema node type for rattail UTC datetimes + """ + widget_maker = widgets.FalafelDateTimeWidget + + def __init__(self, *args, **kwargs): + request = kwargs.pop('request') + super().__init__(*args, **kwargs) + self.request = request + + def serialize(self, node, appstruct): + if not appstruct: + return colander.null + + # cant use isinstance; dt subs date + if type(appstruct) is datetime.date: + appstruct = datetime.datetime.combine(appstruct, datetime.time()) + + if not isinstance(appstruct, datetime.datetime): + raise colander.Invalid(node, f'"{appstruct}" is not a datetime object') + + if appstruct.tzinfo is None: + appstruct = appstruct.replace(tzinfo=self.default_tzinfo) + + app = self.request.rattail_config.get_app() + dt = app.localtime(appstruct, from_utc=True) + + return json.dumps({ + 'date': str(dt.date()), + 'time': str(dt.time()), + }) + + def deserialize(self, node, cstruct): + if not cstruct: + return colander.null + + app = self.request.rattail_config.get_app() + result = datetime.datetime.strptime(cstruct, '%Y-%m-%dT%H:%M:%S') + result = app.localtime(result) + result = app.make_utc(result) + return result + + class GPCType(colander.SchemaType): """ Schema type for product GPC data. diff --git a/tailbone/forms/widgets.py b/tailbone/forms/widgets.py index f672ab47..69f57520 100644 --- a/tailbone/forms/widgets.py +++ b/tailbone/forms/widgets.py @@ -33,7 +33,6 @@ from deform import widget as dfwidget from webhelpers2.html import tags, HTML from tailbone.db import Session -from tailbone.forms.types import ProductQuantity class ReadonlyWidget(dfwidget.HiddenWidget): @@ -119,6 +118,8 @@ class CasesUnitsWidget(dfwidget.Widget): return field.renderer(template, **values) def deserialize(self, field, pstruct): + from tailbone.forms.types import ProductQuantity + if pstruct is colander.null: return colander.null @@ -235,6 +236,13 @@ class JQueryTimeWidget(dfwidget.TimeInputWidget): ) +class FalafelDateTimeWidget(dfwidget.DateTimeInputWidget): + """ + Custom widget for rattail UTC datetimes + """ + template = 'datetime_falafel' + + class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget): """ Uses the jQuery autocomplete plugin, instead of whatever it is deform uses diff --git a/tailbone/static/js/tailbone.buefy.timepicker.js b/tailbone/static/js/tailbone.buefy.timepicker.js index 6cca75f3..207a7940 100644 --- a/tailbone/static/js/tailbone.buefy.timepicker.js +++ b/tailbone/static/js/tailbone.buefy.timepicker.js @@ -9,15 +9,55 @@ const TailboneTimepicker = { 'placeholder="Click to select ..."', 'icon-pack="fas"', 'icon="clock"', + ':value="value ? parseTime(value) : null"', 'hour-format="12"', + '@input="timeChanged"', + ':time-formatter="formatTime"', '>', '' ].join(' '), props: { name: String, - id: String - } + id: String, + value: String, + }, + + methods: { + + formatTime(time) { + if (time === null) { + return null + } + + let h = time.getHours() + let m = time.getMinutes() + let s = time.getSeconds() + + h = h < 10 ? '0' + h : h + m = m < 10 ? '0' + m : m + s = s < 10 ? '0' + s : s + + return h + ':' + m + ':' + s + }, + + parseTime(time) { + + if (time.getHours) { + return time + } + + let found = time.match(/^(\d\d):(\d\d):\d\d$/) + if (found) { + return new Date(null, null, null, + parseInt(found[1]), parseInt(found[2])) + } + }, + + timeChanged(time) { + this.$emit('input', time) + }, + }, } Vue.component('tailbone-timepicker', TailboneTimepicker) diff --git a/tailbone/templates/deform/datetime_falafel.pt b/tailbone/templates/deform/datetime_falafel.pt new file mode 100644 index 00000000..17cfe6c3 --- /dev/null +++ b/tailbone/templates/deform/datetime_falafel.pt @@ -0,0 +1,23 @@ +
+ + + ${field.start_mapping()} + + + + + + + + + + + + ${field.end_mapping()} + + +