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.