From d6aeb1d10f5c9e28ec2f8655421d2dfe3cfd86d9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 8 Aug 2022 23:34:40 -0500 Subject: [PATCH] Add convenience wrapper to make customer field widget, etc. customer widget is either autocomplete or dropdown, per config also added a way to pass arbitrary kwargs to the chameleon template rendering for a field also moved the logic for rendering a out of the template and into the Form class also start to prefer `input_handler` over `input_callback` when specifying client-side JS hook --- tailbone/forms/core.py | 71 ++++++++++++++- tailbone/forms/widgets.py | 89 +++++++++++++++++++ tailbone/templates/customers/configure.mako | 14 +++ .../templates/deform/autocomplete_jquery.pt | 2 +- tailbone/templates/forms/deform_buefy.mako | 3 +- tailbone/templates/forms/util.mako | 26 +----- tailbone/views/customers.py | 5 ++ 7 files changed, 183 insertions(+), 27 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 7278cd2b..14703eb7 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -332,7 +332,7 @@ class Form(object): def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[], model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={}, - assume_local_times=False, renderers=None, + assume_local_times=False, renderers=None, renderer_kwargs={}, hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None, action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form', vuejs_field_converters={}, @@ -361,6 +361,7 @@ class Form(object): self.renderers = self.make_renderers() else: self.renderers = renderers or {} + self.renderer_kwargs = renderer_kwargs or {} self.hidden = hidden or {} self.widgets = widgets or {} self.defaults = defaults or {} @@ -660,6 +661,22 @@ class Form(object): else: self.renderers[key] = renderer + def add_renderer_kwargs(self, key, kwargs): + self.renderer_kwargs.setdefault(key, {}).update(kwargs) + + def get_renderer_kwargs(self, key): + return self.renderer_kwargs.get(key, {}) + + def set_renderer_kwargs(self, key, kwargs): + self.renderer_kwargs[key] = kwargs + + def set_input_handler(self, key, value): + """ + Convenience method to assign "input handler" callback code for + the given field. + """ + self.add_renderer_kwargs(key, {'input_handler': value}) + def set_hidden(self, key, hidden=True): self.hidden[key] = hidden @@ -858,6 +875,58 @@ class Form(object): return False return True + def render_buefy_field(self, fieldname, bfield_attrs={}): + """ + Render the given field in a Buefy-compatible way. Note that + this is meant to render *editable* fields, i.e. showing a + widget, unless the field input is hidden. In other words it's + not for "readonly" fields. + """ + dform = self.make_deform_form() + field = dform[fieldname] + + if self.field_visible(fieldname): + + # these attrs will be for the (*not* the widget) + attrs = { + ':horizontal': 'true', + 'label': self.get_label(fieldname), + } + + # add some magic for file input fields + if isinstance(field.schema.typ, deform.FileData): + attrs['class_'] = 'file' + + # show helptext if present + if self.has_helptext(fieldname): + attrs['message'] = self.render_helptext(fieldname) + + # show errors if present + error_messages = self.get_error_messages(field) + if error_messages: + attrs.update({ + 'type': 'is-danger', + # ':message': self.messages_json(error_messages), + ':message': error_messages, + }) + + # merge anything caller provided + attrs.update(bfield_attrs) + + # render the field widget or whatever + html = field.serialize(use_buefy=True, + **self.get_renderer_kwargs(fieldname)) + # TODO: why do we not get HTML literal from serialize() ? + html = HTML.literal(html) + + # and finally wrap it all in a + return HTML.tag('b-field', c=[html], **attrs) + + else: # hidden field + + # can just do normal thing for these + return field.serialize() + def render_field_readonly(self, field_name, **kwargs): """ Render the given field completely, but in read-only fashion. diff --git a/tailbone/forms/widgets.py b/tailbone/forms/widgets.py index 91b6cb32..e72ab6b9 100644 --- a/tailbone/forms/widgets.py +++ b/tailbone/forms/widgets.py @@ -289,6 +289,95 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget): return field.renderer(template, **tmpl_values) +def make_customer_widget(request, **kwargs): + """ + Make a customer widget; will be either autocomplete or dropdown + depending on config. + """ + # use autocomplete widget by default + factory = CustomerAutocompleteWidget + + # caller may request dropdown widget + if kwargs.pop('dropdown', False): + factory = CustomerDropdownWidget + + else: # or, config may say to use dropdown + if request.rattail_config.getbool( + 'rattail', 'customers.choice_uses_dropdown', + default=False): + factory = CustomerDropdownWidget + + # instantiate whichever + return factory(request, **kwargs) + + +class CustomerAutocompleteWidget(JQueryAutocompleteWidget): + """ + Autocomplete widget for a Customer reference field. + """ + + def __init__(self, request, *args, **kwargs): + super(CustomerAutocompleteWidget, self).__init__(*args, **kwargs) + self.request = request + model = self.request.rattail_config.get_model() + + # must figure out URL providing autocomplete service + if 'service_url' not in kwargs: + + # caller can just pass 'url' instead of 'service_url' + if 'url' in kwargs: + self.service_url = kwargs['url'] + + else: # use default url + self.service_url = self.request.route_url('customers.autocomplete') + + # TODO + if 'input_callback' not in kwargs: + if 'input_handler' in kwargs: + self.input_callback = input_handler + + def serialize(self, field, cstruct, **kw): + + # fetch customer to provide button label, if we have a value + if cstruct: + model = self.request.rattail_config.get_model() + customer = Session.query(model.Customer).get(cstruct) + if customer: + self.field_display = six.text_type(customer) + + return super(CustomerAutocompleteWidget, self).serialize( + field, cstruct, **kw) + + +class CustomerDropdownWidget(dfwidget.SelectWidget): + """ + Dropdown widget for a Customer reference field. + """ + + def __init__(self, request, *args, **kwargs): + super(CustomerDropdownWidget, self).__init__(*args, **kwargs) + self.request = request + + # must figure out dropdown values, if they weren't given + if 'values' not in kwargs: + + # use what caller gave us, if they did + if 'customers' in kwargs: + customers = kwargs['customers'] + if callable(customers): + customers = customers() + + else: # default customer list + model = self.request.rattail_config.get_model() + customers = Session.query(model.Customer)\ + .order_by(model.Customer.name)\ + .all() + + # convert customer list to option values + self.values = [(c.uuid, c.name) + for c in customers] + + class DepartmentWidget(dfwidget.SelectWidget): """ Custom select widget for a Department reference field. diff --git a/tailbone/templates/customers/configure.mako b/tailbone/templates/customers/configure.mako index 13093a7b..f465fdf5 100644 --- a/tailbone/templates/customers/configure.mako +++ b/tailbone/templates/customers/configure.mako @@ -3,6 +3,20 @@ <%def name="form_content()"> +

General

+
+ + + + Show customer chooser as dropdown (select) element + + + +
+

POS

diff --git a/tailbone/templates/deform/autocomplete_jquery.pt b/tailbone/templates/deform/autocomplete_jquery.pt index 4ebc17b2..dd9a6084 100644 --- a/tailbone/templates/deform/autocomplete_jquery.pt +++ b/tailbone/templates/deform/autocomplete_jquery.pt @@ -113,7 +113,7 @@ v-model="${vmodel}" initial-label="${field_display}" tal:attributes=":assigned-label assigned_label or 'null'; - @input input_callback|''; + @input input_handler|input_callback|''; @new-label new_label_callback|'';"> diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index a26c946a..860449fb 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -1,5 +1,4 @@ ## -*- coding: utf-8; -*- -<%namespace file="/forms/util.mako" import="render_buefy_field" />