From 9c205d7da562cbc7ee16af0b89afe510b600c310 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 21 Nov 2017 11:11:18 -0600 Subject: [PATCH] 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... --- tailbone/forms2/core.py | 121 ++++++++++++++++++++++++++++------ tailbone/views/departments.py | 6 -- 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/tailbone/forms2/core.py b/tailbone/forms2/core.py index d167f908..0702b73e 100644 --- a/tailbone/forms2/core.py +++ b/tailbone/forms2/core.py @@ -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: - continue - value = getattr(obj, name) - except AttributeError: + if not self.association_proxy(name): continue + value = getattr(obj, name) 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': diff --git a/tailbone/views/departments.py b/tailbone/views/departments.py index db1d633c..200ebc00 100644 --- a/tailbone/views/departments.py +++ b/tailbone/views/departments.py @@ -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']