Add new v3 master with v2 forms, with colander/deform

goal here is to replace FormAlchemy dependency, slowly but surely..
so far only the Settings and Stores views use v3 master
This commit is contained in:
Lance Edgar 2017-07-18 12:29:05 -05:00
parent 2b5aaa0753
commit 4dcd89fba7
18 changed files with 718 additions and 59 deletions

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Forms Library
"""
from __future__ import unicode_literals, absolute_import
from .core import Form

381
tailbone/forms2/core.py Normal file
View file

@ -0,0 +1,381 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Forms Core
"""
from __future__ import unicode_literals, absolute_import
import logging
import six
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY
from rattail.util import prettify
import colander
from colanderalchemy import SQLAlchemySchemaNode
import deform
from deform import widget as dfwidget
from pyramid.renderers import render
from webhelpers2.html import tags, HTML
log = logging.getLogger(__name__)
class CustomSchemaNode(SQLAlchemySchemaNode):
def get_schema_from_relationship(self, prop, overrides):
""" Build and return a :class:`colander.SchemaNode` for a relationship.
"""
# for some reason ColanderAlchemy wants to crawl our entire ORM by
# default, by way of relationships. this 'excludes' hack is used to
# prevent that, by forcing skip of 2nd-level relationships
excludes = []
if isinstance(prop, orm.RelationshipProperty):
for next_prop in prop.mapper.iterate_properties:
if isinstance(next_prop, orm.RelationshipProperty):
excludes.append(next_prop.key)
if excludes:
overrides['excludes'] = excludes
return super(CustomSchemaNode, self).get_schema_from_relationship(prop, overrides)
def dictify(self, obj):
""" Return a dictified version of `obj` using schema information.
.. note::
This method was copied from upstream and modified to add automatic
handling of "association proxy" fields.
"""
dict_ = super(CustomSchemaNode, self).dictify(obj)
for node in self:
name = node.name
if name not in dict_:
try:
desc = getattr(self.inspector.all_orm_descriptors, name)
if desc.extension_type != ASSOCIATION_PROXY:
continue
value = getattr(obj, name)
except AttributeError:
continue
if value is None:
if isinstance(node.typ, colander.String):
# colander has an issue with `None` on a String type
# where it translates it into "None". Let's check
# for that specific case and turn it into a
# `colander.null`.
dict_[name] = colander.null
else:
# A specific case this helps is with Integer where
# `None` is an invalid value. We call serialize()
# to test if we have a value that will work later
# for serialization and then allow it if it doesn't
# raise an exception. Hopefully this also catches
# issues with user defined types and future issues.
try:
node.serialize(value)
except:
dict_[name] = colander.null
else:
dict_[name] = value
else:
dict_[name] = value
return dict_
def objectify(self, dict_, context=None):
""" Return an object representing ``dict_`` using schema information.
.. note::
This method was copied from upstream and modified to add automatic
handling of "association proxy" fields.
"""
mapper = self.inspector
context = mapper.class_() if context is None else context
for attr in dict_:
if mapper.has_property(attr):
prop = mapper.get_property(attr)
if hasattr(prop, 'mapper'):
cls = prop.mapper.class_
if prop.uselist:
# Sequence of objects
value = [self[attr].children[0].objectify(obj)
for obj in dict_[attr]]
else:
# Single object
value = self[attr].objectify(dict_[attr])
else:
value = dict_[attr]
if value is colander.null:
# `colander.null` is never an appropriate
# value to be placed on an SQLAlchemy object
# so we translate it into `None`.
value = None
setattr(context, attr, value)
else:
# try to process association proxy field
desc = mapper.all_orm_descriptors.get(attr)
if desc and desc.extension_type == ASSOCIATION_PROXY:
value = dict_[attr]
if value is colander.null:
# `colander.null` is never an appropriate
# value to be placed on an SQLAlchemy object
# so we translate it into `None`.
value = None
setattr(context, attr, value)
else:
# Ignore attributes if they are not mapped
log.debug(
'SQLAlchemySchemaNode.objectify: %s not found on '
'%s. This property has been ignored.',
attr, self
)
continue
return context
class Form(object):
"""
Base class for all forms.
"""
def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[],
model_instance=None, model_class=None, labels={}, renderers={}, widgets={},
action_url=None, cancel_url=None):
self.fields = fields
self.schema = schema
self.request = request
self.readonly = readonly
self.readonly_fields = set(readonly_fields or [])
self.model_instance = model_instance
self.model_class = model_class
if self.model_instance and not self.model_class:
self.model_class = type(self.model_instance)
if self.model_class and self.fields is None:
self.fields = self.make_fields()
self.labels = labels or {}
self.renderers = renderers or {}
self.widgets = widgets or {}
self.action_url = action_url
self.cancel_url = cancel_url
def make_fields(self):
"""
Return a default list of fields, based on :attr:`model_class`.
"""
if not self.model_class:
raise ValueError("Must define model_class to use make_fields()")
mapper = orm.class_mapper(self.model_class)
# first add primary column fields
fields = [prop.key for prop in mapper.iterate_properties
if not prop.key.startswith('_')
and prop.key != 'versions']
# then add association proxy fields
for key, desc in sa.inspect(self.model_class).all_orm_descriptors.items():
if desc.extension_type == ASSOCIATION_PROXY:
fields.append(key)
return fields
def remove_field(self, key):
if key in self.fields:
self.fields.remove(key)
def make_schema(self):
if not self.model_class:
# TODO
raise NotImplementedError
if not self.schema:
mapper = orm.class_mapper(self.model_class)
# first filter our "full" field list so we ignore certain ones. in
# particular we don't want readonly fields in the schema, or any
# which appear to be "private"
includes = [f for f in self.fields
if f not in self.readonly_fields
and not f.startswith('_')
and f != 'versions']
# make schema - only include *property* fields at this point
schema = CustomSchemaNode(self.model_class,
includes=[p.key for p in mapper.iterate_properties
if p.key in includes])
# for now, must manually add any "extra" fields? this includes all
# association proxy fields, not sure how other fields will behave
for field in includes:
if field not in schema:
schema.add(colander.SchemaNode(colander.String(), name=field))
# apply any label overrides
for key, label in self.labels.items():
if key in schema:
schema[key].title = label
# apply any widget overrides
for key, widget in self.widgets.items():
if key in schema:
schema[key].widget = widget
self.schema = schema
return self.schema
def set_label(self, key, label):
self.labels[key] = label
def get_label(self, key):
return self.labels.get(key, prettify(key))
def set_readonly(self, key, readonly=True):
if readonly:
self.readonly_fields.add(key)
else:
if key in self.readonly_fields:
self.readonly_fields.remove(key)
def set_type(self, key, type_):
if type_ == 'codeblock':
self.set_renderer(key, self.render_codeblock)
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
def set_renderer(self, key, renderer):
self.renderers[key] = renderer
def set_widget(self, key, widget):
self.widgets[key] = widget
def set_validator(self, key, validator):
schema = self.make_schema()
schema[key].validator = validator
def render(self, template=None, **kwargs):
if not template:
if self.readonly:
template = '/forms2/form_readonly.mako'
else:
template = '/forms2/form.mako'
context = kwargs
context['form'] = self
return render(template, context)
def make_deform_form(self):
if not hasattr(self, 'deform_form'):
schema = self.make_schema()
# get initial form values from model instance
kwargs = {}
if self.model_instance:
kwargs['appstruct'] = schema.dictify(self.model_instance)
# create form
form = deform.Form(schema, **kwargs)
# set readonly widget where applicable
for field in self.readonly_fields:
if field in form:
form[field].widget = ReadonlyWidget()
self.deform_form = form
return self.deform_form
def render_deform(self, dform=None, template='/forms2/deform.mako', **kwargs):
if dform is None:
dform = self.make_deform_form()
# TODO: would perhaps be nice to leverage deform's default rendering
# someday..? i.e. using Chameleon *.pt templates
# return form.render()
context = kwargs
context['form'] = self
context['dform'] = dform
context['request'] = self.request
context['readonly_fields'] = self.readonly_fields
context['render_field_readonly'] = self.render_field_readonly
return render('/forms2/deform.mako', context)
def render_field_readonly(self, field_name, **kwargs):
label = HTML.tag('label', self.get_label(field_name), for_=field_name)
field = self.render_field_value(field_name) or ''
field_div = HTML.tag('div', class_='field', c=field)
return HTML.tag('div', class_='field-wrapper {}'.format(field), c=label + field_div)
def render_field_value(self, field_name):
record = self.model_instance
if self.renderers and field_name in self.renderers:
return self.renderers[field_name](record, field_name)
return self.render_generic(record, field_name)
def render_generic(self, record, field_name):
value = self.obtain_value(record, field_name)
if value is None:
return ""
return six.text_type(value)
def render_codeblock(self, record, field_name):
value = self.obtain_value(record, field_name)
if value is None:
return ""
return HTML.tag('pre', value)
def obtain_value(self, record, field_name):
try:
return record[field_name]
except TypeError:
return getattr(record, field_name)
def validate(self, *args, **kwargs):
form = self.make_deform_form()
return form.validate(*args, **kwargs)
class ReadonlyWidget(dfwidget.HiddenWidget):
readonly = True
def serialize(self, field, cstruct, **kw):
if cstruct in (colander.null, None):
cstruct = ''
return HTML.tag('span', cstruct) + tags.hidden(field.name, value=cstruct, id=field.oid)

View file

@ -54,6 +54,7 @@ div.field-wrapper.error {
}
div.field-wrapper label {
color: Black;
display: block;
float: left;
width: 15em;

View file

@ -0,0 +1,81 @@
## -*- coding: utf-8; -*-
% if not readonly:
<% _focus_rendered = False %>
${h.form(form.action_url, id=dform.formid, method='post', enctype='multipart/form-data')}
${h.csrf_token(request)}
% endif
## % for error in fieldset.errors.get(None, []):
## <div class="fieldset-error">${error}</div>
## % endfor
% for field in form.fields:
## % if readonly or field.name in readonly_fields:
% if readonly:
${render_field_readonly(field)|n}
% elif field not in dform and field in form.readonly_fields:
${render_field_readonly(field)|n}
% elif field in dform:
<% field = dform[field] %>
## % if field.requires_label:
<div class="field-wrapper ${field.name} ${'error' if field.error else ''}">
## % for error in field.errors:
## <div class="field-error">${error}</div>
## % endfor
% if field.error:
<div class="field-error">${field.error.msg}</div>
% endif
<label for="${field.name}">${field.title}</label>
<div class="field">
${field.serialize()|n}
</div>
## % if 'instructions' in field.metadata:
## <span class="instructions">${field.metadata['instructions']}</span>
## % endif
</div>
## % if not _focus_rendered and (fieldset.focus is True or fieldset.focus is field):
% if not readonly and not _focus_rendered:
## % if not field.is_readonly() and getattr(field.renderer, 'needs_focus', True):
% if not field.widget.readonly:
<script type="text/javascript">
$(function() {
## % if hasattr(field.renderer, 'focus_name'):
## $('#${field.renderer.focus_name}').focus();
## % else:
## $('#${field.renderer.name}').focus();
## % endif
$('#${field.oid}').focus();
});
</script>
<% _focus_rendered = True %>
% endif
% endif
## % else:
## ${field.render()|n}
## % endif
% endif
% endfor
% if buttons:
${buttons|n}
% elif not readonly:
<div class="buttons">
## ${h.submit('create', form.create_label if form.creating else form.update_label)}
${h.submit('save', "Save")}
## % if form.creating and form.allow_successive_creates:
## ${h.submit('create_and_continue', form.successive_create_label)}
## % endif
${h.link_to("Cancel", form.cancel_url, class_='button')}
</div>
% endif
% if not readonly:
${h.end_form()}
% endif

View file

@ -0,0 +1,5 @@
## -*- coding: utf-8; -*-
<div class="form">
${form.render_deform()|n}
</div>

View file

@ -0,0 +1,8 @@
## -*- coding: utf-8; -*-
<div class="form">
${form.render_deform(readonly=True)|n}
## % if buttons:
## ${buttons|n}
## % endif
</div><!-- form -->

View file

@ -1,10 +1,10 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
<%inherit file="/base.mako" />
<%def name="title()">${model_title}: ${instance_title}</%def>
<%def name="title()">${model_title_plural} &raquo; ${instance_title}</%def>
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
% if master.has_rows:
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))}
<script type="text/javascript">
@ -12,6 +12,12 @@
$('.grid-wrapper').gridwrapper();
});
</script>
% endif
</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
% if master.has_rows:
<style type="text/css">
.grid-wrapper {
margin-top: 10px;

View file

@ -7,6 +7,8 @@
## ##############################################################################
<%inherit file="/mobile/base.mako" />
<%def name="title()">${instance_title}</%def>
<%def name="title()">${index_title} &raquo; ${instance_title}</%def>
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${instance_title}</%def>
${form.render()|n}

View file

@ -1,8 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()">Receiving</%def>
${h.link_to("New Receiving Batch", url('purchases.batch.mobile_create'), class_='ui-btn')}
${h.link_to("View Receiving Batches", url('mobile.purchases.batch'), class_='ui-btn')}

View file

@ -1,7 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; New Batch</%def>
<%def name="title()">Receiving &raquo; New Batch</%def>
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; New Batch</%def>
${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')}
${h.csrf_token(request)}

View file

@ -1,7 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/view.mako" />
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${instance.id_str}</%def>
<%def name="title()">Receiving &raquo; ${instance.id_str}</%def>
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${instance.id_str}</%def>
${form.render()|n}
<br />

View file

@ -2,8 +2,9 @@
<%inherit file="/mobile/master/view_row.mako" />
<%namespace file="/mobile/keypad.mako" import="keypad" />
## TODO: this is broken for actual page (header) title
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${h.link_to(instance.batch.id_str, url('mobile.receiving.view', uuid=instance.batch_uuid))} &raquo; ${row.upc.pretty()}</%def>
<%def name="title()">Receiving &raquo; ${instance.batch.id_str} &raquo; ${row.upc.pretty()}</%def>
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${h.link_to(instance.batch.id_str, url('mobile.receiving.view', uuid=instance.batch_uuid))} &raquo; ${row.upc.pretty()}</%def>
<%
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'

View file

@ -29,6 +29,7 @@ from __future__ import unicode_literals, absolute_import
from .core import View
from .master import MasterView
from .master2 import MasterView2
from .master3 import MasterView3
# TODO: deprecate / remove some of this
from .autocomplete import AutocompleteView

View file

@ -225,7 +225,7 @@ class MasterView(View):
self.creating = True
form = self.make_form(self.get_model_class())
if self.request.method == 'POST':
if form.validate():
if self.validate_form(form):
# let save_create_form() return alternate object if necessary
obj = self.save_create_form(form) or form.fieldset.model
self.after_create(obj)
@ -615,6 +615,7 @@ class MasterView(View):
"""
self.editing = True
instance = self.get_instance()
instance_title = self.get_instance_title(instance)
if not self.editable_instance(instance):
self.request.session.flash("Edit is not permitted for {}: {}".format(
@ -624,18 +625,22 @@ class MasterView(View):
form = self.make_form(instance)
if self.request.method == 'POST':
if form.validate():
if self.validate_form(form):
self.save_edit_form(form)
# note we must fetch new instance title, in case it changed
self.request.session.flash("{} has been updated: {}".format(
self.get_model_title(), self.get_instance_title(instance)))
return self.redirect_after_edit(instance)
return self.render_to_response('edit', {
'instance': instance,
'instance_title': self.get_instance_title(instance),
'instance_title': instance_title,
'instance_deletable': self.deletable_instance(instance),
'form': form})
def validate_form(self, form):
return form.validate()
def save_edit_form(self, form):
self.save_form(form)
self.after_edit(form.fieldset.model)

View file

@ -39,6 +39,7 @@ class MasterView2(MasterView):
sortable = True
rows_pageable = True
mobile_pageable = True
labels = {'uuid': "UUID"}
@classmethod
def get_grid_factory(cls):
@ -391,8 +392,12 @@ class MasterView2(MasterView):
the given row object, or ``None``.
"""
def set_labels(self, obj):
for key, label in self.labels.items():
obj.set_label(key, label)
def configure_grid(self, grid):
pass
self.set_labels(grid)
def configure_row_grid(self, grid):
pass

134
tailbone/views/master3.py Normal file
View file

@ -0,0 +1,134 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Master View
"""
from __future__ import unicode_literals, absolute_import
from sqlalchemy import orm
import deform
from tailbone import forms2 as forms
from tailbone.views import MasterView2
class MasterView3(MasterView2):
"""
Base "master" view class. All model master views should derive from this.
"""
@classmethod
def get_form_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
grid instances.
"""
return getattr(cls, 'form_factory', forms.Form)
def make_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
"""
Creates a new form for the given model class/instance
"""
if factory is None:
factory = self.get_form_factory()
if fields is None:
fields = self.get_form_fields()
if schema is None:
schema = self.make_form_schema()
if not self.creating:
kwargs['model_instance'] = instance
kwargs = self.make_form_kwargs(**kwargs)
form = factory(fields, schema, **kwargs)
self.configure_form(form)
return form
def make_form_schema(self):
if not self.model_class:
# TODO
raise NotImplementedError
def get_form_fields(self):
if hasattr(self, 'form_fields'):
return self.form_fields
# TODO
# raise NotImplementedError
def make_form_kwargs(self, **kwargs):
"""
Return a dictionary of kwargs to be passed to the factory when creating
new form instances.
"""
defaults = {
'request': self.request,
'readonly': self.viewing,
'model_class': getattr(self, 'model_class', None),
'action_url': self.request.current_route_url(_query=None),
}
if self.creating:
kwargs.setdefault('cancel_url', self.get_index_url())
else:
instance = kwargs['model_instance']
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
defaults.update(kwargs)
return defaults
def configure_form(self, form):
"""
Configure the primary form. By default this just sets any primary key
fields to be readonly (if we have a :attr:`model_class`).
"""
if self.editing and self.model_class:
mapper = orm.class_mapper(self.model_class)
for key in mapper.primary_key:
for field in form.fields:
if field == key.name:
form.set_readonly(field)
break
form.remove_field('uuid')
self.set_labels(form)
def validate_form(self, form):
controls = self.request.POST.items()
try:
self.form_deserialized = form.validate(controls)
except deform.ValidationFailure:
return False
return True
def save_create_form(self, form):
self.before_create(form)
obj = form.schema.objectify(self.form_deserialized)
self.Session.add(obj)
self.Session.flush()
return obj
def save_edit_form(self, form):
obj = form.schema.objectify(self.form_deserialized, context=form.model_instance)
self.after_edit(obj)
self.Session.flush()

View file

@ -30,16 +30,9 @@ import re
from rattail.db import model
import formalchemy as fa
import colander
from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView
def unique_name(value, field):
setting = Session.query(model.Setting).get(value)
if setting:
raise fa.ValidationError("Setting name must be unique")
from tailbone.views import MasterView3 as MasterView
class SettingsView(MasterView):
@ -48,27 +41,28 @@ class SettingsView(MasterView):
"""
model_class = model.Setting
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
grid_columns = [
'name',
'value',
]
def configure_grid(self, g):
super(SettingsView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.set_link('name')
def _preconfigure_fieldset(self, fs):
fs.name.set(validate=unique_name)
if self.editing:
fs.name.set(readonly=True)
def configure_form(self, f):
super(SettingsView, self).configure_form(f)
if self.creating:
f.set_validator('name', self.unique_name)
def configure_fieldset(self, fs):
fs.configure(include=[
fs.name,
fs.value,
])
def unique_name(self, node, value):
setting = self.Session.query(model.Setting).get(value)
if setting:
raise colander.Invalid(node, "Setting name must be unique")
def editable_instance(self, setting):
if self.rattail_config.demo():

View file

@ -30,7 +30,9 @@ import sqlalchemy as sa
from rattail.db import model
from tailbone.views import MasterView2 as MasterView
import colander
from tailbone.views import MasterView3 as MasterView
class StoresView(MasterView):
@ -39,6 +41,7 @@ class StoresView(MasterView):
"""
model_class = model.Store
has_versions = True
grid_columns = [
'id',
'name',
@ -46,40 +49,47 @@ class StoresView(MasterView):
'email',
]
def configure_grid(self, g):
labels = {
'id': "ID",
'phone': "Phone Number",
'email': "Email Address",
}
g.joiners['email'] = lambda q: q.outerjoin(model.StoreEmailAddress, sa.and_(
def configure_grid(self, g):
super(StoresView, self).configure_grid(g)
g.set_joiner('email', lambda q: q.outerjoin(model.StoreEmailAddress, sa.and_(
model.StoreEmailAddress.parent_uuid == model.Store.uuid,
model.StoreEmailAddress.preference == 1))
g.joiners['phone'] = lambda q: q.outerjoin(model.StorePhoneNumber, sa.and_(
model.StoreEmailAddress.preference == 1)))
g.set_joiner('phone', lambda q: q.outerjoin(model.StorePhoneNumber, sa.and_(
model.StorePhoneNumber.parent_uuid == model.Store.uuid,
model.StorePhoneNumber.preference == 1))
model.StorePhoneNumber.preference == 1)))
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number)
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.sorters['phone'] = g.make_sorter(model.StorePhoneNumber.number)
g.sorters['email'] = g.make_sorter(model.StoreEmailAddress.address)
g.set_sorter('phone', model.StorePhoneNumber.number)
g.set_sorter('email', model.StoreEmailAddress.address)
g.default_sortkey = 'id'
g.set_link('id')
g.set_link('name')
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
def configure_form(self, f):
super(StoresView, self).configure_form(f)
def configure_fieldset(self, fs):
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
fs.database_key,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
])
f.remove_field('employees')
f.remove_field('phones')
f.remove_field('emails')
if self.creating:
f.remove_field('phone')
f.remove_field('email')
else:
f.set_readonly('phone')
f.set_readonly('email')
def get_version_child_classes(self):
return [