Refactor employees view to use master3
This commit is contained in:
parent
7a777964a7
commit
84ebf5d929
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue