fix: add support for date, datetime form fields
using buefy-based picker widgets etc.
This commit is contained in:
parent
fce1bf9de4
commit
bf8397ba23
|
@ -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),
|
||||
|
|
|
@ -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}
|
||||
|
|
6
src/wuttaweb/templates/deform/dateinput.pt
Normal file
6
src/wuttaweb/templates/deform/dateinput.pt
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
${field.start_mapping()}
|
||||
<wutta-datepicker name="date"
|
||||
value="${cstruct}" />
|
||||
${field.end_mapping()}
|
||||
</div>
|
10
src/wuttaweb/templates/deform/datetimeinput.pt
Normal file
10
src/wuttaweb/templates/deform/datetimeinput.pt
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue