diff --git a/docs/conf.py b/docs/conf.py index cf15be8..8bf7c8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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), diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py index 8f5cafc..9caed35 100644 --- a/src/wuttaweb/forms/base.py +++ b/src/wuttaweb/forms/base.py @@ -457,23 +457,62 @@ class Form: if self.schema: self.schema[key] = node - def set_widget(self, key, widget): + def set_widget(self, key, widget, **kwargs): """ Set/override the widget for a field. + You can specify a widget instance or else a named "type" of + widget, in which case that is passed along to + :meth:`make_widget()`. + :param key: Name of field. - :param widget: Instance of - :class:`deform:deform.widget.Widget`. + :param widget: Either a :class:`deform:deform.widget.Widget` + instance, or else a widget "type" name. + + :param \**kwargs: Any remaining kwargs are passed along to + :meth:`make_widget()` - if applicable. Widget overrides are tracked via :attr:`widgets`. """ + if not isinstance(widget, deform.widget.Widget): + widget_obj = self.make_widget(widget, **kwargs) + if not widget_obj: + raise ValueError(f"widget type not supported: {widget}") + widget = widget_obj + self.widgets[key] = widget # update schema if necessary if self.schema and key in self.schema: self.schema[key].widget = widget + def make_widget(self, widget_type, **kwargs): + """ + Make and return a new field widget of the given type. + + This has built-in support for the following types (although + subclass can override as needed): + + * ``'notes'`` => :class:`~wuttaweb.forms.widgets.NotesWidget` + + See also :meth:`set_widget()` which may call this method + automatically. + + :param widget_type: Which of the above (or custom) widget + type to create. + + :param \**kwargs: Remaining kwargs are passed as-is to the + widget factory. + + :returns: New widget instance, or ``None`` if e.g. it could + not determine how to create the widget. + """ + from wuttaweb.forms import widgets + + if widget_type == 'notes': + return widgets.NotesWidget(**kwargs) + def set_grid(self, key, grid): """ Establish a :term:`grid` to be displayed for a field. This @@ -1111,6 +1150,32 @@ class Form: return self.validated + def has_global_errors(self): + """ + Convenience function to check if the form has any "global" + (not field-level) errors. + + See also :meth:`get_global_errors()`. + + :returns: ``True`` if global errors present, else ``False``. + """ + dform = self.get_deform() + return bool(dform.error) + + def get_global_errors(self): + """ + Returns a list of "global" (not field-level) error messages + for the form. + + See also :meth:`has_global_errors()`. + + :returns: List of error messages (possibly empty). + """ + dform = self.get_deform() + if dform.error is None: + return [] + return dform.error.messages() + def get_field_errors(self, field): """ Return a list of error messages for the given field. diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py index 6347b61..275d42c 100644 --- a/src/wuttaweb/forms/schema.py +++ b/src/wuttaweb/forms/schema.py @@ -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 @@ -207,6 +232,11 @@ class ObjectRef(colander.SchemaType): def serialize(self, node, appstruct): """ """ + # nb. normalize to empty option if no object ref, so that + # works as expected + if self.empty_option and not appstruct: + return self.empty_option[0] + if appstruct is colander.null: return colander.null @@ -497,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} diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py index 2d91eb9..b4ed6a3 100644 --- a/src/wuttaweb/forms/widgets.py +++ b/src/wuttaweb/forms/widgets.py @@ -102,7 +102,7 @@ class ObjectRefWidget(SelectWidget): # add url, only if rendering readonly readonly = kw.get('readonly', self.readonly) if readonly: - if 'url' not in values and self.url and field.schema.model_instance: + if 'url' not in values and self.url and hasattr(field.schema, 'model_instance'): values['url'] = self.url(field.schema.model_instance) return values diff --git a/src/wuttaweb/handler.py b/src/wuttaweb/handler.py index f5b4d71..1ac0b78 100644 --- a/src/wuttaweb/handler.py +++ b/src/wuttaweb/handler.py @@ -26,7 +26,7 @@ Web Handler from wuttjamaican.app import GenericHandler -from wuttaweb import static +from wuttaweb import static, forms, grids class WebHandler(GenericHandler): @@ -122,3 +122,23 @@ class WebHandler(GenericHandler): default='wuttaweb.menus:MenuHandler') self.menu_handler = self.app.load_object(spec)(self.config) return self.menu_handler + + def make_form(self, request, **kwargs): + """ + Make and return a new :class:`~wuttaweb.forms.base.Form` + instance, per the given ``kwargs``. + + This is the "base" factory which merely invokes the + constructor. + """ + return forms.Form(request, **kwargs) + + def make_grid(self, request, **kwargs): + """ + Make and return a new :class:`~wuttaweb.grids.base.Grid` + instance, per the given ``kwargs``. + + This is the "base" factory which merely invokes the + constructor. + """ + return grids.Grid(request, **kwargs) diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako index 8da535a..695bfb8 100644 --- a/src/wuttaweb/templates/base.mako +++ b/src/wuttaweb/templates/base.mako @@ -196,6 +196,11 @@ width: 50%; } + .wutta-form-wrapper .field.is-horizontal .field-body .select, + .wutta-form-wrapper .field.is-horizontal .field-body .select select { + width: 100%; + } + diff --git a/src/wuttaweb/templates/deform/dateinput.pt b/src/wuttaweb/templates/deform/dateinput.pt new file mode 100644 index 0000000..b0e1285 --- /dev/null +++ b/src/wuttaweb/templates/deform/dateinput.pt @@ -0,0 +1,6 @@ +
+ ${field.start_mapping()} + + ${field.end_mapping()} +
diff --git a/src/wuttaweb/templates/deform/datetimeinput.pt b/src/wuttaweb/templates/deform/datetimeinput.pt new file mode 100644 index 0000000..b617929 --- /dev/null +++ b/src/wuttaweb/templates/deform/datetimeinput.pt @@ -0,0 +1,10 @@ +
+ ${field.start_mapping()} + + + ${field.end_mapping()} +
diff --git a/src/wuttaweb/templates/deform/readonly/objectref.pt b/src/wuttaweb/templates/deform/readonly/objectref.pt index 3ab9e0e..70c1040 100644 --- a/src/wuttaweb/templates/deform/readonly/objectref.pt +++ b/src/wuttaweb/templates/deform/readonly/objectref.pt @@ -4,6 +4,6 @@ ${str(field.schema.model_instance or '')} - ${str(field.schema.model_instance or '')} + ${str(getattr(field.schema, 'model_instance', None) or '')} diff --git a/src/wuttaweb/templates/forms/vue_template.mako b/src/wuttaweb/templates/forms/vue_template.mako index 5a4af70..d913054 100644 --- a/src/wuttaweb/templates/forms/vue_template.mako +++ b/src/wuttaweb/templates/forms/vue_template.mako @@ -4,6 +4,14 @@ ${h.form(form.action_url, method='post', enctype='multipart/form-data', **form_attrs)} ${h.csrf_token(request)} + % if form.has_global_errors(): + % for msg in form.get_global_errors(): + + ${msg} + + % endfor + % endif +
% for fieldname in form: ${form.render_vue_field(fieldname)} diff --git a/src/wuttaweb/templates/wutta-components.mako b/src/wuttaweb/templates/wutta-components.mako index b52992e..6030840 100644 --- a/src/wuttaweb/templates/wutta-components.mako +++ b/src/wuttaweb/templates/wutta-components.mako @@ -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()} @@ -149,6 +151,141 @@ +<%def name="make_wutta_datepicker_component()"> + + + + +<%def name="make_wutta_timepicker_component()"> + + + + <%def name="make_wutta_filter_component()">