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), 'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'python': ('https://docs.python.org/3/', None), 'python': ('https://docs.python.org/3/', None),
'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', 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), 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None), 'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None),

View file

@ -24,15 +24,40 @@
Form schema types Form schema types
""" """
import datetime
import uuid as _uuid import uuid as _uuid
import colander import colander
import sqlalchemy as sa
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttaweb.forms import widgets from wuttaweb.forms import widgets
from wuttjamaican.db.model import Person 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): class ObjectNode(colander.SchemaNode):
""" """
Custom schema node class which adds methods for compatibility with Custom schema node class which adds methods for compatibility with
@ -502,3 +527,7 @@ class FileDownload(colander.String):
""" """ """ """
kwargs.setdefault('url', self.url) kwargs.setdefault('url', self.url)
return widgets.FileDownloadWidget(self.request, **kwargs) 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()"> <%def name="make_wutta_components()">
${self.make_wutta_request_mixin()} ${self.make_wutta_request_mixin()}
${self.make_wutta_button_component()} ${self.make_wutta_button_component()}
${self.make_wutta_datepicker_component()}
${self.make_wutta_timepicker_component()}
${self.make_wutta_filter_component()} ${self.make_wutta_filter_component()}
${self.make_wutta_filter_value_component()} ${self.make_wutta_filter_value_component()}
</%def> </%def>
@ -149,6 +151,141 @@
</script> </script>
</%def> </%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()"> <%def name="make_wutta_filter_component()">
<script type="text/x-template" id="wutta-filter-template"> <script type="text/x-template" id="wutta-filter-template">
<div v-show="filter.visible" <div v-show="filter.visible"

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
import datetime
from unittest import TestCase
from unittest.mock import patch from unittest.mock import patch
import colander import colander
@ -13,6 +15,22 @@ from wuttaweb.forms import widgets
from tests.util import DataTestCase, WebTestCase 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): class TestObjectNode(DataTestCase):
def setUp(self): def setUp(self):