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
|
from rattail.util import prettify, pretty_boolean, pretty_hours
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from colanderalchemy import SQLAlchemySchemaNode
|
|
||||||
import deform
|
import deform
|
||||||
|
from colanderalchemy import SQLAlchemySchemaNode
|
||||||
|
from colanderalchemy.schema import _creation_order
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
@ -51,8 +52,90 @@ from .widgets import ReadonlyWidget, JQueryDateWidget, JQueryTimeWidget
|
||||||
log = logging.getLogger(__name__)
|
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):
|
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):
|
def get_schema_from_relationship(self, prop, overrides):
|
||||||
""" Build and return a :class:`colander.SchemaNode` for a relationship.
|
""" Build and return a :class:`colander.SchemaNode` for a relationship.
|
||||||
"""
|
"""
|
||||||
|
@ -84,15 +167,10 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
|
||||||
|
|
||||||
name = node.name
|
name = node.name
|
||||||
if name not in dict_:
|
if name not in dict_:
|
||||||
|
if not self.association_proxy(name):
|
||||||
try:
|
|
||||||
desc = getattr(self.inspector.all_orm_descriptors, name)
|
|
||||||
if desc.extension_type != ASSOCIATION_PROXY:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
value = getattr(obj, name)
|
value = getattr(obj, name)
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
if isinstance(node.typ, colander.String):
|
if isinstance(node.typ, colander.String):
|
||||||
# colander has an issue with `None` on a String type
|
# colander has an issue with `None` on a String type
|
||||||
|
@ -149,9 +227,9 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
|
||||||
setattr(context, attr, value)
|
setattr(context, attr, value)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# try to process association proxy field
|
# try to process association proxy field
|
||||||
desc = mapper.all_orm_descriptors.get(attr)
|
if self.association_proxy(attr):
|
||||||
if desc and desc.extension_type == ASSOCIATION_PROXY:
|
|
||||||
value = dict_[attr]
|
value = dict_[attr]
|
||||||
if value is colander.null:
|
if value is colander.null:
|
||||||
# `colander.null` is never an appropriate
|
# `colander.null` is never an appropriate
|
||||||
|
@ -245,16 +323,21 @@ class Form(object):
|
||||||
and not f.startswith('_')
|
and not f.startswith('_')
|
||||||
and f != 'versions']
|
and f != 'versions']
|
||||||
|
|
||||||
# filter list further, to avoid magic for nodes we already have
|
# derive list of "auto included" fields. this is all "included"
|
||||||
auto_includes = list(includes)
|
# fields which are part of the SQLAlchemy ORM for the object
|
||||||
for field in self.nodes:
|
auto_includes = []
|
||||||
if field in auto_includes:
|
property_keys = [p.key for p in mapper.iterate_properties]
|
||||||
auto_includes.remove(field)
|
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
|
# make schema - only include *property* fields at this point
|
||||||
schema = CustomSchemaNode(self.model_class,
|
schema = CustomSchemaNode(self.model_class, includes=auto_includes)
|
||||||
includes=[p.key for p in mapper.iterate_properties
|
|
||||||
if p.key in auto_includes])
|
|
||||||
|
|
||||||
# for now, must manually add any "extra" fields? this includes all
|
# for now, must manually add any "extra" fields? this includes all
|
||||||
# association proxy fields, not sure how other fields will behave
|
# association proxy fields, not sure how other fields will behave
|
||||||
|
@ -328,7 +411,7 @@ class Form(object):
|
||||||
self.set_renderer(key, self.render_duration)
|
self.set_renderer(key, self.render_duration)
|
||||||
elif type_ == 'boolean':
|
elif type_ == 'boolean':
|
||||||
self.set_renderer(key, self.render_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':
|
elif type_ == 'currency':
|
||||||
self.set_renderer(key, self.render_currency)
|
self.set_renderer(key, self.render_currency)
|
||||||
elif type_ == 'enum':
|
elif type_ == 'enum':
|
||||||
|
|
|
@ -64,14 +64,8 @@ class DepartmentsView(MasterView):
|
||||||
super(DepartmentsView, self).configure_form(f)
|
super(DepartmentsView, self).configure_form(f)
|
||||||
f.remove_field('subdepartments')
|
f.remove_field('subdepartments')
|
||||||
f.remove_field('employees')
|
f.remove_field('employees')
|
||||||
|
|
||||||
# TODO: widget should not be necessary, per type
|
|
||||||
f.set_type('product', 'boolean')
|
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_type('personnel', 'boolean')
|
||||||
f.set_widget('personnel', dfwidget.CheckboxWidget())
|
|
||||||
|
|
||||||
def template_kwargs_view(self, **kwargs):
|
def template_kwargs_view(self, **kwargs):
|
||||||
department = kwargs['instance']
|
department = kwargs['instance']
|
||||||
|
|
Loading…
Reference in a new issue