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..
This commit is contained in:
Lance Edgar 2023-09-16 20:01:32 -05:00
parent 99065548ff
commit a807a0f50c
6 changed files with 136 additions and 7 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,8 +24,7 @@
Forms Library Forms Library
""" """
from __future__ import unicode_literals, absolute_import # nb. import widgets before types, b/c types may refer to widgets
from . import types
from . import widgets from . import widgets
from . import types
from .core import Form, SimpleFileImport from .core import Form, SimpleFileImport

View file

@ -590,8 +590,18 @@ class Form(object):
self.schema[key] = node self.schema[key] = node
def set_type(self, key, type_, **kwargs): def set_type(self, key, type_, **kwargs):
if type_ == 'datetime': if type_ == 'datetime':
self.set_renderer(key, self.render_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': elif type_ == 'datetime_local':
self.set_renderer(key, self.render_datetime_local) self.set_renderer(key, self.render_datetime_local)
elif type_ == 'date_plain': elif type_ == 'date_plain':
@ -871,6 +881,9 @@ class Form(object):
if field.cstruct is colander.null: if field.cstruct is colander.null:
return '[]' return '[]'
if isinstance(field.schema.typ, types.FalafelDateTime):
return field.cstruct
try: try:
return self.jsonify_value(field.cstruct) return self.jsonify_value(field.cstruct)
except Exception as error: except Exception as error:

View file

@ -26,6 +26,7 @@ Form Schema Types
import re import re
import datetime import datetime
import json
from rattail.db import model from rattail.db import model
from rattail.gpc import GPC from rattail.gpc import GPC
@ -33,6 +34,7 @@ from rattail.gpc import GPC
import colander import colander
from tailbone.db import Session from tailbone.db import Session
from tailbone.forms import widgets
class JQueryTime(colander.Time): class JQueryTime(colander.Time):
@ -72,6 +74,50 @@ class DateTimeBoolean(colander.Boolean):
return datetime.datetime.utcnow() 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): class GPCType(colander.SchemaType):
""" """
Schema type for product GPC data. Schema type for product GPC data.

View file

@ -33,7 +33,6 @@ from deform import widget as dfwidget
from webhelpers2.html import tags, HTML from webhelpers2.html import tags, HTML
from tailbone.db import Session from tailbone.db import Session
from tailbone.forms.types import ProductQuantity
class ReadonlyWidget(dfwidget.HiddenWidget): class ReadonlyWidget(dfwidget.HiddenWidget):
@ -119,6 +118,8 @@ class CasesUnitsWidget(dfwidget.Widget):
return field.renderer(template, **values) return field.renderer(template, **values)
def deserialize(self, field, pstruct): def deserialize(self, field, pstruct):
from tailbone.forms.types import ProductQuantity
if pstruct is colander.null: if pstruct is colander.null:
return 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): class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
""" """
Uses the jQuery autocomplete plugin, instead of whatever it is deform uses Uses the jQuery autocomplete plugin, instead of whatever it is deform uses

View file

@ -9,15 +9,55 @@ const TailboneTimepicker = {
'placeholder="Click to select ..."', 'placeholder="Click to select ..."',
'icon-pack="fas"', 'icon-pack="fas"',
'icon="clock"', 'icon="clock"',
':value="value ? parseTime(value) : null"',
'hour-format="12"', 'hour-format="12"',
'@input="timeChanged"',
':time-formatter="formatTime"',
'>', '>',
'</b-timepicker>' '</b-timepicker>'
].join(' '), ].join(' '),
props: { props: {
name: String, 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) Vue.component('tailbone-timepicker', TailboneTimepicker)

View file

@ -0,0 +1,23 @@
<div tal:omit-tag=""
tal:define="name name|field.name;
vmodel vmodel|'field_model_' + name;">
<b-field grouped>
${field.start_mapping()}
<b-field label="Date">
<tailbone-datepicker name="date"
v-model="${vmodel}.date">
</tailbone-datepicker>
</b-field>
<b-field label="Time">
<tailbone-timepicker name="time"
v-model="${vmodel}.time">
</tailbone-timepicker>
</b-field>
${field.end_mapping()}
</b-field>
</div>