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:
parent
5334cf1871
commit
d6aeb1d10f
|
@ -332,7 +332,7 @@ class Form(object):
|
||||||
|
|
||||||
def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[],
|
def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[],
|
||||||
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
|
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,
|
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
|
||||||
action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form',
|
action_url=None, cancel_url=None, use_buefy=None, component='tailbone-form',
|
||||||
vuejs_field_converters={},
|
vuejs_field_converters={},
|
||||||
|
@ -361,6 +361,7 @@ class Form(object):
|
||||||
self.renderers = self.make_renderers()
|
self.renderers = self.make_renderers()
|
||||||
else:
|
else:
|
||||||
self.renderers = renderers or {}
|
self.renderers = renderers or {}
|
||||||
|
self.renderer_kwargs = renderer_kwargs or {}
|
||||||
self.hidden = hidden or {}
|
self.hidden = hidden or {}
|
||||||
self.widgets = widgets or {}
|
self.widgets = widgets or {}
|
||||||
self.defaults = defaults or {}
|
self.defaults = defaults or {}
|
||||||
|
@ -660,6 +661,22 @@ class Form(object):
|
||||||
else:
|
else:
|
||||||
self.renderers[key] = renderer
|
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):
|
def set_hidden(self, key, hidden=True):
|
||||||
self.hidden[key] = hidden
|
self.hidden[key] = hidden
|
||||||
|
|
||||||
|
@ -858,6 +875,58 @@ class Form(object):
|
||||||
return False
|
return False
|
||||||
return True
|
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):
|
def render_field_readonly(self, field_name, **kwargs):
|
||||||
"""
|
"""
|
||||||
Render the given field completely, but in read-only fashion.
|
Render the given field completely, but in read-only fashion.
|
||||||
|
|
|
@ -289,6 +289,95 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
|
||||||
return field.renderer(template, **tmpl_values)
|
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):
|
class DepartmentWidget(dfwidget.SelectWidget):
|
||||||
"""
|
"""
|
||||||
Custom select widget for a Department reference field.
|
Custom select widget for a Department reference field.
|
||||||
|
|
|
@ -3,6 +3,20 @@
|
||||||
|
|
||||||
<%def name="form_content()">
|
<%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>
|
<h3 class="block is-size-3">POS</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@
|
||||||
v-model="${vmodel}"
|
v-model="${vmodel}"
|
||||||
initial-label="${field_display}"
|
initial-label="${field_display}"
|
||||||
tal:attributes=":assigned-label assigned_label or 'null';
|
tal:attributes=":assigned-label assigned_label or 'null';
|
||||||
@input input_callback|'';
|
@input input_handler|input_callback|'';
|
||||||
@new-label new_label_callback|'';">
|
@new-label new_label_callback|'';">
|
||||||
|
|
||||||
</tailbone-autocomplete>
|
</tailbone-autocomplete>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%namespace file="/forms/util.mako" import="render_buefy_field" />
|
|
||||||
|
|
||||||
<script type="text/x-template" id="${form.component}-template">
|
<script type="text/x-template" id="${form.component}-template">
|
||||||
|
|
||||||
|
@ -21,7 +20,7 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
% elif field in dform:
|
% elif field in dform:
|
||||||
${render_buefy_field(dform[field])}
|
${form.render_buefy_field(field)}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
|
|
|
@ -1,27 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- 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={})">
|
<%def name="render_buefy_field(field, bfield_kwargs={})">
|
||||||
% if form.field_visible(field.name):
|
${form.render_buefy_field(field.name, bfield_attrs=bfield_kwargs)}
|
||||||
<% 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
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -490,6 +490,11 @@ class CustomerView(MasterView):
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_simple_settings(self):
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
# General
|
||||||
|
{'section': 'rattail',
|
||||||
|
'option': 'customers.choice_uses_dropdown',
|
||||||
|
'type': bool},
|
||||||
|
|
||||||
# POS
|
# POS
|
||||||
{'section': 'rattail',
|
{'section': 'rattail',
|
||||||
'option': 'customers.active_in_pos',
|
'option': 'customers.active_in_pos',
|
||||||
|
|
Loading…
Reference in a new issue