diff --git a/tailbone/forms2/core.py b/tailbone/forms2/core.py index 7c2a8918..65d63221 100644 --- a/tailbone/forms2/core.py +++ b/tailbone/forms2/core.py @@ -75,6 +75,38 @@ class CustomSchemaNode(SQLAlchemySchemaNode): """ return get_association_proxy(self.inspector, field) + def association_proxy_target(self, field): + """ + Returns the property on the main class, which represents the "target" + for the given association proxy field name. Typically this will refer + to the "extension" model class. + """ + proxy = self.association_proxy(field) + if proxy: + proxy_target = self.inspector.get_property(proxy.target_collection) + if isinstance(proxy_target, orm.RelationshipProperty) and not proxy_target.uselist: + return proxy_target + + def association_proxy_column(self, field): + """ + Returns the property on the proxy target class, for the column which is + reflected by the proxy. + """ + proxy_target = self.association_proxy_target(field) + if proxy_target: + prop = proxy_target.mapper.get_property(field) + if isinstance(prop, orm.ColumnProperty) and isinstance(prop.columns[0], sa.Column): + return prop + + def supported_association_proxy(self, field): + """ + Returns boolean indicating whether the association proxy corresponding + to the given field name, is "supported" with typical logic. + """ + if not self.association_proxy_column(field): + return False + return True + def add_nodes(self, includes, excludes, overrides): """ Add all automatic nodes to the schema. @@ -117,13 +149,9 @@ class CustomSchemaNode(SQLAlchemySchemaNode): 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) + column = self.association_proxy_column(name) + if column: + node = self.get_schema_from_column(column, name_overrides_copy) else: log.debug( @@ -167,7 +195,8 @@ class CustomSchemaNode(SQLAlchemySchemaNode): name = node.name if name not in dict_: - if not self.association_proxy(name): + # we're only processing association proxy fields here + if not self.supported_association_proxy(name): continue value = getattr(obj, name) @@ -229,7 +258,7 @@ class CustomSchemaNode(SQLAlchemySchemaNode): else: # try to process association proxy field - if self.association_proxy(attr): + if self.supported_association_proxy(attr): value = dict_[attr] if value is colander.null: # `colander.null` is never an appropriate @@ -306,6 +335,10 @@ class Form(object): if key in self.fields: self.fields.remove(key) + def remove_fields(self, *args): + for arg in args: + self.remove_field(arg) + def make_schema(self): if not self.model_class: # TODO @@ -566,10 +599,11 @@ class Form(object): return HTML.tag('pre', value) def obtain_value(self, record, field_name): - try: - return record[field_name] - except TypeError: - return getattr(record, field_name) + if record: + try: + return record[field_name] + except TypeError: + return getattr(record, field_name) def validate(self, *args, **kwargs): form = self.make_deform_form() diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py index f02154c4..50c7c030 100644 --- a/tailbone/views/employees.py +++ b/tailbone/views/employees.py @@ -26,15 +26,18 @@ Employee Views from __future__ import unicode_literals, absolute_import +import six import sqlalchemy as sa from rattail.db import model -import formalchemy as fa +import colander +from deform import widget as dfwidget +from webhelpers2.html import tags, HTML -from tailbone import forms, grids +from tailbone import grids from tailbone.db import Session -from tailbone.views import MasterView2 as MasterView, AutocompleteView +from tailbone.views import MasterView3 as MasterView, AutocompleteView class EmployeesView(MasterView): @@ -53,6 +56,21 @@ class EmployeesView(MasterView): 'status', ] + form_fields = [ + 'person', + 'first_name', + 'last_name', + 'display_name', + 'phone', + 'email', + 'status', + 'full_time', + 'full_time_start', + 'id', + 'stores', + 'departments', + ] + def configure_grid(self, g): super(EmployeesView, self).configure_grid(g) @@ -124,38 +142,116 @@ class EmployeesView(MasterView): return not bool(employee.user and employee.user.username == 'chuck') return True - def _preconfigure_fieldset(self, fs): - fs.append(forms.AssociationProxyField('first_name')) - fs.append(forms.AssociationProxyField('last_name')) - fs.append(StoresField('stores')) - fs.append(DepartmentsField('departments')) + def configure_form(self, f): + super(EmployeesView, self).configure_form(f) + employee = f.model_instance - fs.person.set(renderer=forms.renderers.PersonFieldRenderer) - fs.display_name.set(label="Short Name") - fs.phone.set(label="Phone Number", readonly=True) - fs.email.set(label="Email Address", readonly=True) - fs.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.EMPLOYEE_STATUS)) - fs.id.set(label="ID") + f.set_renderer('person', self.render_person) + + f.set_renderer('stores', self.render_stores) + f.set_label('stores', "Stores") # TODO: should not be necessary + if self.creating or self.editing: + stores = self.get_possible_stores().all() + store_values = [(s.uuid, six.text_type(s)) for s in stores] + f.set_node('stores', colander.SchemaNode(colander.Set())) + f.set_widget('stores', dfwidget.SelectWidget(multiple=True, + size=len(stores), + values=store_values)) + if self.editing: + f.set_default('stores', [s.uuid for s in employee.stores]) + + f.set_renderer('departments', self.render_departments) + f.set_label('departments', "Departments") # TODO: should not be necessary + if self.creating or self.editing: + departments = self.get_possible_departments().all() + dept_values = [(d.uuid, six.text_type(d)) for d in departments] + f.set_node('departments', colander.SchemaNode(colander.Set())) + f.set_widget('departments', dfwidget.SelectWidget(multiple=True, + size=len(departments), + values=dept_values)) + if self.editing: + f.set_default('departments', [d.uuid for d in employee.departments]) + + f.set_enum('status', self.enum.EMPLOYEE_STATUS) + + f.set_type('full_time_start', 'date_jquery') + if self.editing: + # TODO: this should not be needed (association proxy) + f.set_default('full_time_start', employee.full_time_start) + + f.set_readonly('person') + f.set_readonly('phone') + f.set_readonly('email') + + f.set_label('display_name', "Short Name") + f.set_label('phone', "Phone Number") + f.set_label('email', "Email Address") + f.set_label('id', "ID") - def configure_fieldset(self, fs): - fs.configure( - include=[ - fs.person, - fs.first_name, - fs.last_name, - fs.display_name, - fs.phone, - fs.email, - fs.status, - fs.full_time, - fs.full_time_start, - fs.id, - fs.stores, - fs.departments, - ]) if not self.viewing: - del fs.first_name - del fs.last_name + f.remove_fields('first_name', 'last_name') + + def objectify(self, form, data): + employee = super(EmployeesView, self).objectify(form, data) + self.update_stores(employee, data) + self.update_departments(employee, data) + return employee + + def update_stores(self, employee, data): + old_stores = set([s.uuid for s in employee.stores]) + new_stores = data['stores'] + for uuid in new_stores: + if uuid not in old_stores: + employee._stores.append(model.EmployeeStore(store_uuid=uuid)) + for uuid in old_stores: + if uuid not in new_stores: + store = self.Session.query(model.Store).get(uuid) + employee.stores.remove(store) + + def update_departments(self, employee, data): + old_depts = set([d.uuid for d in employee.departments]) + new_depts = data['departments'] + for uuid in new_depts: + if uuid not in old_depts: + employee._departments.append(model.EmployeeDepartment(department_uuid=uuid)) + for uuid in old_depts: + if uuid not in new_depts: + dept = self.Session.query(model.Department).get(uuid) + employee.departments.remove(dept) + + def get_possible_stores(self): + return self.Session.query(model.Store)\ + .order_by(model.Store.name) + + def get_possible_departments(self): + return self.Session.query(model.Department)\ + .order_by(model.Department.name) + + def render_person(self, employee, field): + person = employee.person if employee else None + if not person: + return "" + text = six.text_type(person) + url = self.request.route_url('people.view', uuid=person.uuid) + return tags.link_to(text, url) + + def render_stores(self, employee, field): + stores = employee.stores if employee else None + if not stores: + return "" + items = HTML.literal('') + for store in sorted(stores, key=six.text_type): + items += HTML.tag('li', c=six.text_type(store)) + return HTML.tag('ul', c=items) + + def render_departments(self, employee, field): + departments = employee.departments if employee else None + if not departments: + return "" + items = HTML.literal('') + for department in sorted(departments, key=six.text_type): + items += HTML.tag('li', c=six.text_type(department)) + return HTML.tag('ul', c=items) def get_version_child_classes(self): return [ @@ -167,62 +263,6 @@ class EmployeesView(MasterView): ] -class StoresField(fa.Field): - - def __init__(self, name, **kwargs): - kwargs.setdefault('type', fa.types.Set) - kwargs.setdefault('options', Session.query(model.Store).order_by(model.Store.name)) - kwargs.setdefault('value', self.get_value) - kwargs.setdefault('multiple', True) - kwargs.setdefault('size', 3) - fa.Field.__init__(self, name=name, **kwargs) - - def get_value(self, employee): - return [s.uuid for s in employee.stores] - - def sync(self): - if not self.is_readonly(): - employee = self.parent.model - old_stores = set([s.uuid for s in employee.stores]) - new_stores = set(self._deserialize()) - for uuid in new_stores: - if uuid not in old_stores: - employee._stores.append(model.EmployeeStore(store_uuid=uuid)) - for uuid in old_stores: - if uuid not in new_stores: - store = Session.query(model.Store).get(uuid) - assert store - employee.stores.remove(store) - - -class DepartmentsField(fa.Field): - - def __init__(self, name, **kwargs): - kwargs.setdefault('type', fa.types.Set) - kwargs.setdefault('options', Session.query(model.Department).order_by(model.Department.name)) - kwargs.setdefault('value', self.get_value) - kwargs.setdefault('multiple', True) - kwargs.setdefault('size', 10) - fa.Field.__init__(self, name=name, **kwargs) - - def get_value(self, employee): - return [d.uuid for d in employee.departments] - - def sync(self): - if not self.is_readonly(): - employee = self.parent.model - old_depts = set([d.uuid for d in employee.departments]) - new_depts = set(self._deserialize()) - for uuid in new_depts: - if uuid not in old_depts: - employee._departments.append(model.EmployeeDepartment(department_uuid=uuid)) - for uuid in old_depts: - if uuid not in new_depts: - dept = Session.query(model.Department).get(uuid) - assert dept - employee.departments.remove(dept) - - class EmployeesAutocomplete(AutocompleteView): """ Autocomplete view for the Employee model, but restricted to return only diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 420203e5..4dad973a 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -250,8 +250,6 @@ class MasterView(View): # let save_create_form() return alternate object if necessary obj = self.save_create_form(form) or form.fieldset.model self.after_create(obj) - # TODO: ugh, avoiding refactor for now but it's needed - self.after_create_form(form, obj) self.flash_after_create(obj) return self.redirect_after_create(obj) context = {'form': form} @@ -1625,11 +1623,6 @@ class MasterView(View): Event hook, called just after a new instance is saved. """ - def after_create_form(self, form, obj): - """ - Event hook, called just after a new instance is saved. - """ - def editable_instance(self, instance): """ Returns boolean indicating whether or not the given instance can be @@ -1643,11 +1636,6 @@ class MasterView(View): Event hook, called just after an existing instance is saved. """ - def after_edit_form(self, form, obj): - """ - Event hook, called just after an instance is updated. - """ - def deletable_instance(self, instance): """ Returns boolean indicating whether or not the given instance can be diff --git a/tailbone/views/master3.py b/tailbone/views/master3.py index 53e2c0de..7e661adb 100644 --- a/tailbone/views/master3.py +++ b/tailbone/views/master3.py @@ -152,9 +152,13 @@ class MasterView3(MasterView2): return False return True + def objectify(self, form, data): + obj = form.schema.objectify(data, context=form.model_instance) + return obj + def save_create_form(self, form): self.before_create(form) - obj = form.schema.objectify(self.form_deserialized) + obj = self.objectify(form, self.form_deserialized) self.before_create_flush(obj, form) self.Session.add(obj) self.Session.flush() @@ -164,8 +168,6 @@ class MasterView3(MasterView2): pass def save_edit_form(self, form): - obj = form.schema.objectify(self.form_deserialized, context=form.model_instance) + obj = self.objectify(form, self.form_deserialized) self.after_edit(obj) - # TODO: ugh, this is to avoid refactor for the moment..but it's needed.. - self.after_edit_form(form, obj) self.Session.flush()