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

View file

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

View file

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

View file

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

View file

@ -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"',
'>',
'</b-timepicker>'
].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)

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>