Refactor employees view to use master3

This commit is contained in:
Lance Edgar 2017-12-04 13:48:31 -06:00
parent 7a777964a7
commit 84ebf5d929
4 changed files with 181 additions and 117 deletions

View file

@ -75,6 +75,38 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
""" """
return get_association_proxy(self.inspector, field) 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): def add_nodes(self, includes, excludes, overrides):
""" """
Add all automatic nodes to the schema. Add all automatic nodes to the schema.
@ -117,13 +149,9 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
else: else:
# magic for association proxy fields # magic for association proxy fields
proxy = self.association_proxy(name) column = self.association_proxy_column(name)
if proxy: if column:
proxy_prop = self.inspector.get_property(proxy.target_collection) node = self.get_schema_from_column(column, name_overrides_copy)
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: else:
log.debug( log.debug(
@ -167,7 +195,8 @@ 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): # we're only processing association proxy fields here
if not self.supported_association_proxy(name):
continue continue
value = getattr(obj, name) value = getattr(obj, name)
@ -229,7 +258,7 @@ class CustomSchemaNode(SQLAlchemySchemaNode):
else: else:
# try to process association proxy field # try to process association proxy field
if self.association_proxy(attr): if self.supported_association_proxy(attr):
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
@ -306,6 +335,10 @@ class Form(object):
if key in self.fields: if key in self.fields:
self.fields.remove(key) self.fields.remove(key)
def remove_fields(self, *args):
for arg in args:
self.remove_field(arg)
def make_schema(self): def make_schema(self):
if not self.model_class: if not self.model_class:
# TODO # TODO
@ -566,6 +599,7 @@ class Form(object):
return HTML.tag('pre', value) return HTML.tag('pre', value)
def obtain_value(self, record, field_name): def obtain_value(self, record, field_name):
if record:
try: try:
return record[field_name] return record[field_name]
except TypeError: except TypeError:

View file

@ -26,15 +26,18 @@ Employee Views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import six
import sqlalchemy as sa import sqlalchemy as sa
from rattail.db import model 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.db import Session
from tailbone.views import MasterView2 as MasterView, AutocompleteView from tailbone.views import MasterView3 as MasterView, AutocompleteView
class EmployeesView(MasterView): class EmployeesView(MasterView):
@ -53,6 +56,21 @@ class EmployeesView(MasterView):
'status', '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): def configure_grid(self, g):
super(EmployeesView, self).configure_grid(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 not bool(employee.user and employee.user.username == 'chuck')
return True return True
def _preconfigure_fieldset(self, fs): def configure_form(self, f):
fs.append(forms.AssociationProxyField('first_name')) super(EmployeesView, self).configure_form(f)
fs.append(forms.AssociationProxyField('last_name')) employee = f.model_instance
fs.append(StoresField('stores'))
fs.append(DepartmentsField('departments'))
fs.person.set(renderer=forms.renderers.PersonFieldRenderer) f.set_renderer('person', self.render_person)
fs.display_name.set(label="Short Name")
fs.phone.set(label="Phone Number", readonly=True) f.set_renderer('stores', self.render_stores)
fs.email.set(label="Email Address", readonly=True) f.set_label('stores', "Stores") # TODO: should not be necessary
fs.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.EMPLOYEE_STATUS)) if self.creating or self.editing:
fs.id.set(label="ID") 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: if not self.viewing:
del fs.first_name f.remove_fields('first_name', 'last_name')
del fs.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): def get_version_child_classes(self):
return [ 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): class EmployeesAutocomplete(AutocompleteView):
""" """
Autocomplete view for the Employee model, but restricted to return only Autocomplete view for the Employee model, but restricted to return only

View file

@ -250,8 +250,6 @@ class MasterView(View):
# let save_create_form() return alternate object if necessary # let save_create_form() return alternate object if necessary
obj = self.save_create_form(form) or form.fieldset.model obj = self.save_create_form(form) or form.fieldset.model
self.after_create(obj) 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) self.flash_after_create(obj)
return self.redirect_after_create(obj) return self.redirect_after_create(obj)
context = {'form': form} context = {'form': form}
@ -1625,11 +1623,6 @@ class MasterView(View):
Event hook, called just after a new instance is saved. 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): def editable_instance(self, instance):
""" """
Returns boolean indicating whether or not the given instance can be 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. 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): def deletable_instance(self, instance):
""" """
Returns boolean indicating whether or not the given instance can be Returns boolean indicating whether or not the given instance can be

View file

@ -152,9 +152,13 @@ class MasterView3(MasterView2):
return False return False
return True return True
def objectify(self, form, data):
obj = form.schema.objectify(data, context=form.model_instance)
return obj
def save_create_form(self, form): def save_create_form(self, form):
self.before_create(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.before_create_flush(obj, form)
self.Session.add(obj) self.Session.add(obj)
self.Session.flush() self.Session.flush()
@ -164,8 +168,6 @@ class MasterView3(MasterView2):
pass pass
def save_edit_form(self, form): 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) 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() self.Session.flush()