Add colander magic for association proxy fields
hopefully now any association proxy fields which are included, will be given the appropriate type and widget. however this still doesn't work for the readonly rendering of fields...
This commit is contained in:
parent
6ea88808b2
commit
9c205d7da5
|
@ -38,8 +38,9 @@ from rattail.time import localtime
|
|||
from rattail.util import prettify, pretty_boolean, pretty_hours
|
||||
|
||||
import colander
|
||||
from colanderalchemy import SQLAlchemySchemaNode
|
||||
import deform
|
||||
from colanderalchemy import SQLAlchemySchemaNode
|
||||
from colanderalchemy.schema import _creation_order
|
||||
from deform import widget as dfwidget
|
||||
from pyramid.renderers import render
|
||||
from webhelpers2.html import tags, HTML
|
||||
|
@ -51,8 +52,90 @@ from .widgets import ReadonlyWidget, JQueryDateWidget, JQueryTimeWidget
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_association_proxy(mapper, field):
|
||||
"""
|
||||
Returns the association proxy corresponding to the given field name if one
|
||||
exists, or ``None``.
|
||||
"""
|
||||
try:
|
||||
desc = getattr(mapper.all_orm_descriptors, field)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if desc.extension_type == ASSOCIATION_PROXY:
|
||||
return desc
|
||||
|
||||
|
||||
class CustomSchemaNode(SQLAlchemySchemaNode):
|
||||
|
||||
def association_proxy(self, field):
|
||||
"""
|
||||
Returns the association proxy corresponding to the given field name if
|
||||
one exists, or ``None``.
|
||||
"""
|
||||
return get_association_proxy(self.inspector, field)
|
||||
|
||||
def add_nodes(self, includes, excludes, overrides):
|
||||
"""
|
||||
Add all automatic nodes to the schema.
|
||||
|
||||
.. note::
|
||||
This method was copied from upstream and modified to add automatic
|
||||
handling of "association proxy" fields.
|
||||
"""
|
||||
if set(excludes) & set(includes):
|
||||
msg = 'excludes and includes are mutually exclusive.'
|
||||
raise ValueError(msg)
|
||||
|
||||
properties = sorted(self.inspector.attrs, key=_creation_order)
|
||||
# sorted to maintain the order in which the attributes
|
||||
# are defined
|
||||
for name in includes or [item.key for item in properties]:
|
||||
prop = self.inspector.attrs.get(name, name)
|
||||
|
||||
if name in excludes or (includes and name not in includes):
|
||||
log.debug('Attribute %s skipped imperatively', name)
|
||||
continue
|
||||
|
||||
name_overrides_copy = overrides.get(name, {}).copy()
|
||||
|
||||
if (isinstance(prop, orm.ColumnProperty)
|
||||
and isinstance(prop.columns[0], sa.Column)):
|
||||
node = self.get_schema_from_column(
|
||||
prop,
|
||||
name_overrides_copy
|
||||
)
|
||||
elif isinstance(prop, orm.RelationshipProperty):
|
||||
if prop.mapper.class_ in self.parents_ and name not in includes:
|
||||
continue
|
||||
node = self.get_schema_from_relationship(
|
||||
prop,
|
||||
name_overrides_copy
|
||||
)
|
||||
elif isinstance(prop, colander.SchemaNode):
|
||||
node = prop
|
||||
else:
|
||||
|
||||
# magic for association proxy fields
|
||||
proxy = self.association_proxy(name)
|
||||
if proxy:
|
||||
proxy_prop = self.inspector.get_property(proxy.target_collection)
|
||||
if isinstance(proxy_prop, orm.RelationshipProperty):
|
||||
prop = proxy_prop.mapper.get_property(name)
|
||||
if isinstance(prop, orm.ColumnProperty) and isinstance(prop.columns[0], sa.Column):
|
||||
node = self.get_schema_from_column(prop, name_overrides_copy)
|
||||
|
||||
else:
|
||||
log.debug(
|
||||
'Attribute %s skipped due to not being '
|
||||
'a ColumnProperty or RelationshipProperty',
|
||||
name
|
||||
)
|
||||
continue
|
||||
|
||||
if node is not None:
|
||||
self.add(node)
|
||||
|
||||
def get_schema_from_relationship(self, prop, overrides):
|
||||
""" Build and return a :class:`colander.SchemaNode` for a relationship.
|
||||
"""
|
||||
|
@ -84,15 +167,10 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
|
|||
|
||||
name = node.name
|
||||
if name not in dict_:
|
||||
|
||||
try:
|
||||
desc = getattr(self.inspector.all_orm_descriptors, name)
|
||||
if desc.extension_type != ASSOCIATION_PROXY:
|
||||
if not self.association_proxy(name):
|
||||
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
|
||||
|
@ -149,9 +227,9 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
|
|||
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:
|
||||
if self.association_proxy(attr):
|
||||
value = dict_[attr]
|
||||
if value is colander.null:
|
||||
# `colander.null` is never an appropriate
|
||||
|
@ -245,16 +323,21 @@ class Form(object):
|
|||
and not f.startswith('_')
|
||||
and f != 'versions']
|
||||
|
||||
# filter list further, to avoid magic for nodes we already have
|
||||
auto_includes = list(includes)
|
||||
for field in self.nodes:
|
||||
if field in auto_includes:
|
||||
auto_includes.remove(field)
|
||||
# derive list of "auto included" fields. this is all "included"
|
||||
# fields which are part of the SQLAlchemy ORM for the object
|
||||
auto_includes = []
|
||||
property_keys = [p.key for p in mapper.iterate_properties]
|
||||
inspector = sa.inspect(self.model_class)
|
||||
for field in includes:
|
||||
if field in self.nodes:
|
||||
continue # these are explicitly set; no magic wanted
|
||||
if field in property_keys:
|
||||
auto_includes.append(field)
|
||||
elif get_association_proxy(inspector, field):
|
||||
auto_includes.append(field)
|
||||
|
||||
# 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 auto_includes])
|
||||
schema = CustomSchemaNode(self.model_class, includes=auto_includes)
|
||||
|
||||
# for now, must manually add any "extra" fields? this includes all
|
||||
# association proxy fields, not sure how other fields will behave
|
||||
|
@ -328,7 +411,7 @@ class Form(object):
|
|||
self.set_renderer(key, self.render_duration)
|
||||
elif type_ == 'boolean':
|
||||
self.set_renderer(key, self.render_boolean)
|
||||
self.set_widget(key, dfwidget.CheckboxWidget(true_val='True', false_val='False'))
|
||||
self.set_widget(key, dfwidget.CheckboxWidget())
|
||||
elif type_ == 'currency':
|
||||
self.set_renderer(key, self.render_currency)
|
||||
elif type_ == 'enum':
|
||||
|
|
|
@ -64,14 +64,8 @@ class DepartmentsView(MasterView):
|
|||
super(DepartmentsView, self).configure_form(f)
|
||||
f.remove_field('subdepartments')
|
||||
f.remove_field('employees')
|
||||
|
||||
# TODO: widget should not be necessary, per type
|
||||
f.set_type('product', 'boolean')
|
||||
f.set_widget('product', dfwidget.CheckboxWidget())
|
||||
|
||||
# TODO: widget should not be necessary, per type
|
||||
f.set_type('personnel', 'boolean')
|
||||
f.set_widget('personnel', dfwidget.CheckboxWidget())
|
||||
|
||||
def template_kwargs_view(self, **kwargs):
|
||||
department = kwargs['instance']
|
||||
|
|
Loading…
Reference in a new issue