3
0
Fork 0

fix: add support for date, datetime form fields

using buefy-based picker widgets etc.
This commit is contained in:
Lance Edgar 2024-12-11 22:38:51 -06:00
parent fce1bf9de4
commit bf8397ba23
6 changed files with 201 additions and 0 deletions

View file

@ -33,6 +33,7 @@ intersphinx_mapping = {
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'python': ('https://docs.python.org/3/', None),
'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', None),
'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None),

View file

@ -24,15 +24,40 @@
Form schema types
"""
import datetime
import uuid as _uuid
import colander
import sqlalchemy as sa
from wuttaweb.db import Session
from wuttaweb.forms import widgets
from wuttjamaican.db.model import Person
class WuttaDateTime(colander.DateTime):
"""
Custom schema type for ``datetime`` fields.
This should be used automatically for
:class:`sqlalchemy:sqlalchemy.types.DateTime` columns unless you
register another default.
This schema type exists for sake of convenience, when working with
the Buefy datepicker + timepicker widgets.
"""
def deserialize(self, node, cstruct):
""" """
if not cstruct:
return colander.null
try:
return datetime.datetime.strptime(cstruct, '%Y-%m-%dT%I:%M %p')
except:
node.raise_invalid("Invalid date and/or time")
class ObjectNode(colander.SchemaNode):
"""
Custom schema node class which adds methods for compatibility with
@ -502,3 +527,7 @@ class FileDownload(colander.String):
""" """
kwargs.setdefault('url', self.url)
return widgets.FileDownloadWidget(self.request, **kwargs)
# nb. colanderalchemy schema overrides
sa.DateTime.__colanderalchemy_config__ = {'typ': WuttaDateTime}

View file

@ -0,0 +1,6 @@
<div>
${field.start_mapping()}
<wutta-datepicker name="date"
value="${cstruct}" />
${field.end_mapping()}
</div>

View file

@ -0,0 +1,10 @@
<div style="display: flex; gap: 0.5rem;">
${field.start_mapping()}
<wutta-datepicker name="date"
style="flex-grow: 1;"
value="${date}" />
<wutta-timepicker name="time"
style="flex-grow: 1;"
value="${time}" />
${field.end_mapping()}
</div>

View file

@ -2,6 +2,8 @@
<%def name="make_wutta_components()">
${self.make_wutta_request_mixin()}
${self.make_wutta_button_component()}
${self.make_wutta_datepicker_component()}
${self.make_wutta_timepicker_component()}
${self.make_wutta_filter_component()}
${self.make_wutta_filter_value_component()}
</%def>
@ -149,6 +151,141 @@
</script>
</%def>
<%def name="make_wutta_datepicker_component()">
<script type="text/x-template" id="wutta-datepicker-template">
<b-datepicker :name="name"
:editable="editable"
icon-pack="fas"
icon="calendar-alt"
:date-formatter="formatDate"
:value="buefyValue" />
</script>
<script>
const WuttaDatepicker = {
template: '#wutta-datepicker-template',
props: {
name: String,
value: String,
editable: {
type: Boolean,
default: true,
},
},
data() {
return {
buefyValue: this.parseDate(this.value),
}
},
methods: {
formatDate(date) {
if (date === null) {
return null
}
// just need to convert to simple ISO date format here, seems
// like there should be a more obvious way to do that?
var year = date.getFullYear()
var month = date.getMonth() + 1
var day = date.getDate()
month = month < 10 ? '0' + month : month
day = day < 10 ? '0' + day : day
return year + '-' + month + '-' + day
},
parseDate(date) {
if (!date) {
return
}
if (typeof(date) == 'string') {
// nb. this assumes classic YYYY-MM-DD (ISO) format
var parts = date.split('-')
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
}
return date
},
},
}
Vue.component('wutta-datepicker', WuttaDatepicker)
</script>
</%def>
<%def name="make_wutta_timepicker_component()">
<script type="text/x-template" id="wutta-timepicker-template">
<b-timepicker :name="name"
editable
:value="buefyValue" />
</script>
<script>
const WuttaTimepicker = {
template: '#wutta-timepicker-template',
props: {
name: String,
value: String,
editable: {
type: Boolean,
default: true,
},
},
data() {
return {
buefyValue: this.parseTime(this.value),
}
},
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, hours, minutes
found = time.match(/^(\d\d):(\d\d):\d\d$/)
if (found) {
hours = parseInt(found[1])
minutes = parseInt(found[2])
return new Date(null, null, null, hours, minutes)
}
found = time.match(/^\s*(\d\d?):(\d\d)\s*([AaPp][Mm])\s*$/)
if (found) {
hours = parseInt(found[1])
minutes = parseInt(found[2])
const ampm = found[3].toUpperCase()
if (ampm == 'AM') {
if (hours == 12) {
hours = 0
}
} else { // PM
if (hours < 12) {
hours += 12
}
}
return new Date(null, null, null, hours, minutes)
}
},
},
}
Vue.component('wutta-timepicker', WuttaTimepicker)
</script>
</%def>
<%def name="make_wutta_filter_component()">
<script type="text/x-template" id="wutta-filter-template">
<div v-show="filter.visible"

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8; -*-
import datetime
from unittest import TestCase
from unittest.mock import patch
import colander
@ -13,6 +15,22 @@ from wuttaweb.forms import widgets
from tests.util import DataTestCase, WebTestCase
class TestWutaDateTime(TestCase):
def test_deserialize(self):
typ = mod.WuttaDateTime()
node = colander.SchemaNode(typ)
result = typ.deserialize(node, colander.null)
self.assertIs(result, colander.null)
result = typ.deserialize(node, '2024-12-11T10:33 PM')
self.assertIsInstance(result, datetime.datetime)
self.assertEqual(result, datetime.datetime(2024, 12, 11, 22, 33))
self.assertRaises(colander.Invalid, typ.deserialize, node, 'bogus')
class TestObjectNode(DataTestCase):
def setUp(self):