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 <b-field> out of the template and
into the Form class

also start to prefer `input_handler` over `input_callback` when
specifying client-side JS hook
This commit is contained in:
Lance Edgar 2022-08-08 23:34:40 -05:00
parent 5334cf1871
commit d6aeb1d10f
7 changed files with 183 additions and 27 deletions

View file

@ -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 <b-field> (*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 <b-field>
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.

View file

@ -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.

View file

@ -3,6 +3,20 @@
<%def name="form_content()">
<h3 class="block is-size-3">General</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If not set, customer chooser is an autocomplete field.">
<b-checkbox name="rattail.customers.choice_uses_dropdown"
v-model="simpleSettings['rattail.customers.choice_uses_dropdown']"
native-value="true"
@input="settingsNeedSaved = true">
Show customer chooser as dropdown (select) element
</b-checkbox>
</b-field>
</div>
<h3 class="block is-size-3">POS</h3>
<div class="block" style="padding-left: 2rem;">

View file

@ -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|'';">
</tailbone-autocomplete>

View file

@ -1,5 +1,4 @@
## -*- coding: utf-8; -*-
<%namespace file="/forms/util.mako" import="render_buefy_field" />
<script type="text/x-template" id="${form.component}-template">
@ -21,7 +20,7 @@
</b-field>
% elif field in dform:
${render_buefy_field(dform[field])}
${form.render_buefy_field(field)}
% endif
% endfor

View file

@ -1,27 +1,7 @@
## -*- coding: utf-8; -*-
## TODO: deprecate / remove this
## (tried to add deprecation warning here but it didn't seem to work)
<%def name="render_buefy_field(field, bfield_kwargs={})">
% if form.field_visible(field.name):
<% error_messages = form.get_error_messages(field) %>
<b-field horizontal
label="${form.get_label(field.name)}"
## TODO: is this class="file" really needed?
% if isinstance(field.schema.typ, deform.FileData):
class="file"
% endif
% if form.has_helptext(field.name):
message="${form.render_helptext(field.name)}"
% endif
% if error_messages:
type="is-danger"
:message='${form.messages_json(error_messages)|n}'
% endif
${h.HTML.render_attrs(bfield_kwargs)}
>
${field.serialize(use_buefy=True)|n}
</b-field>
% else:
## hidden field
${field.serialize()|n}
% endif
${form.render_buefy_field(field.name, bfield_attrs=bfield_kwargs)}
</%def>

View file

@ -490,6 +490,11 @@ class CustomerView(MasterView):
def configure_get_simple_settings(self):
return [
# General
{'section': 'rattail',
'option': 'customers.choice_uses_dropdown',
'type': bool},
# POS
{'section': 'rattail',
'option': 'customers.active_in_pos',