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()} + + +