From c422b900c6c615a7c9773c11c1656b7e609e7037 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 21 May 2013 21:51:41 -0700 Subject: [PATCH] Extensive commit; see notes. * Replaced `forms` module with subpackage; added some initial goodies (many of which are currently just imports from `edbob`). * Added/edited various CRUD templates for consistency. * Renamed `customer_groups` module and template folder to `customergroups`. * Modified several view modules so their Pyramid configuration is more "extensible." This just means routes and views are defined as two separate steps, so that derived applications may inherit the route definitions if they so choose. * Added Employee CRUD views; added Email Address field to index view. * Updated `people` view module so it no longer derives from that of `edbob`. * Added support for, and some implementations of, extra key lookup abilities to CRUD views. This allows URLs to use a "natural" key (e.g. Customer ID instead of UUID), for cases where that is more helpful. * Product CRUD now uses autocomplete for Brand field. Also, price fields no longer appear within an editable fieldset. * Within Store index view, default sort is now ID instead of Name. * Added Contact and Phone Number fields to Vendor CRUD views; added Contact and Email Address fields to index view. --- rattail/pyramid/forms/__init__.py | 30 ++++ rattail/pyramid/forms/fields.py | 54 ++++++++ .../pyramid/{forms.py => forms/renderers.py} | 27 +++- .../templates/customer_groups/crud.mako | 7 - .../templates/customer_groups/index.mako | 5 - .../templates/customergroups/crud.mako | 12 ++ .../templates/customergroups/index.mako | 11 ++ rattail/pyramid/templates/customers/crud.mako | 7 +- rattail/pyramid/templates/employees/crud.mako | 12 ++ rattail/pyramid/templates/people/crud.mako | 12 ++ rattail/pyramid/templates/stores/crud.mako | 5 + rattail/pyramid/templates/users/crud.mako | 12 ++ rattail/pyramid/views/__init__.py | 2 +- rattail/pyramid/views/brands.py | 23 ++-- .../{customer_groups.py => customergroups.py} | 33 ++++- rattail/pyramid/views/customers.py | 31 ++++- rattail/pyramid/views/employees.py | 106 +++++++++++--- rattail/pyramid/views/people.py | 129 ++++++++++++++++-- rattail/pyramid/views/products.py | 11 +- rattail/pyramid/views/stores.py | 2 +- rattail/pyramid/views/users.py | 21 +-- rattail/pyramid/views/vendors.py | 24 ++-- 22 files changed, 471 insertions(+), 105 deletions(-) create mode 100644 rattail/pyramid/forms/__init__.py create mode 100644 rattail/pyramid/forms/fields.py rename rattail/pyramid/{forms.py => forms/renderers.py} (77%) delete mode 100644 rattail/pyramid/templates/customer_groups/crud.mako delete mode 100644 rattail/pyramid/templates/customer_groups/index.mako create mode 100644 rattail/pyramid/templates/customergroups/crud.mako create mode 100644 rattail/pyramid/templates/customergroups/index.mako create mode 100644 rattail/pyramid/templates/employees/crud.mako create mode 100644 rattail/pyramid/templates/people/crud.mako create mode 100644 rattail/pyramid/templates/users/crud.mako rename rattail/pyramid/views/{customer_groups.py => customergroups.py} (61%) diff --git a/rattail/pyramid/forms/__init__.py b/rattail/pyramid/forms/__init__.py new file mode 100644 index 00000000..6f77ba06 --- /dev/null +++ b/rattail/pyramid/forms/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.pyramid.forms`` -- Forms +""" + +from rattail.pyramid.forms.fields import * +from rattail.pyramid.forms.renderers import * diff --git a/rattail/pyramid/forms/fields.py b/rattail/pyramid/forms/fields.py new file mode 100644 index 00000000..0ea31b1c --- /dev/null +++ b/rattail/pyramid/forms/fields.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ + +""" +``rattail.pyramid.forms.fields`` -- FormAlchemy Fields +""" + +from formalchemy import Field + + +__all__ = ['AssociationProxyField'] + + +def AssociationProxyField(name, **kwargs): + """ + Returns a ``Field`` class which is aware of SQLAlchemy association proxies. + """ + + class ProxyField(Field): + + def sync(self): + if not self.is_readonly(): + setattr(self.parent.model, self.name, + self.renderer.deserialize()) + + def value(model): + try: + return getattr(model, name) + except AttributeError: + return None + + kwargs.setdefault('value', value) + return ProxyField(name, **kwargs) diff --git a/rattail/pyramid/forms.py b/rattail/pyramid/forms/renderers.py similarity index 77% rename from rattail/pyramid/forms.py rename to rattail/pyramid/forms/renderers.py index 0005f0ce..c444e0a9 100644 --- a/rattail/pyramid/forms.py +++ b/rattail/pyramid/forms/renderers.py @@ -23,19 +23,27 @@ ################################################################################ """ -``rattail.pyramid.forms`` -- Rattail Forms +``rattail.pyramid.forms.renderers`` -- FormAlchemy Field Renderers """ from webhelpers.html import literal +from webhelpers.html import tags import formalchemy from edbob.pyramid.forms import pretty_datetime +from edbob.pyramid.forms.formalchemy.renderers import ( + AutocompleteFieldRenderer, EnumFieldRenderer, YesNoFieldRenderer) import rattail from rattail.gpc import GPC +__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', 'YesNoFieldRenderer', + 'GPCFieldRenderer', 'PersonFieldRenderer', 'PriceFieldRenderer', + 'PriceWithExpirationFieldRenderer'] + + class GPCFieldRenderer(formalchemy.TextFieldRenderer): """ Renderer for :class:`rattail.gpc.GPC` fields. @@ -47,6 +55,23 @@ class GPCFieldRenderer(formalchemy.TextFieldRenderer): return len(str(GPC(0))) +def PersonFieldRenderer(url): + + BaseRenderer = AutocompleteFieldRenderer(url) + + class PersonFieldRenderer(BaseRenderer): + + def render_readonly(self, **kwargs): + person = self.raw_value + if not person: + return '' + return tags.link_to( + str(person), + self.request.route_url('person.read', uuid=person.uuid)) + + return PersonFieldRenderer + + class PriceFieldRenderer(formalchemy.TextFieldRenderer): """ Renderer for fields which reference a :class:`ProductPrice` instance. diff --git a/rattail/pyramid/templates/customer_groups/crud.mako b/rattail/pyramid/templates/customer_groups/crud.mako deleted file mode 100644 index 0c0ba7cb..00000000 --- a/rattail/pyramid/templates/customer_groups/crud.mako +++ /dev/null @@ -1,7 +0,0 @@ -<%inherit file="/crud.mako" /> - -<%def name="context_menu_items()"> -
  • ${h.link_to("Back to Customer Groups", url('customer_groups'))}
  • - - -${parent.body()} diff --git a/rattail/pyramid/templates/customer_groups/index.mako b/rattail/pyramid/templates/customer_groups/index.mako deleted file mode 100644 index c7849cbb..00000000 --- a/rattail/pyramid/templates/customer_groups/index.mako +++ /dev/null @@ -1,5 +0,0 @@ -<%inherit file="/grid.mako" /> - -<%def name="title()">Customer Groups - -${parent.body()} diff --git a/rattail/pyramid/templates/customergroups/crud.mako b/rattail/pyramid/templates/customergroups/crud.mako new file mode 100644 index 00000000..f616ce8b --- /dev/null +++ b/rattail/pyramid/templates/customergroups/crud.mako @@ -0,0 +1,12 @@ +<%inherit file="/crud.mako" /> + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to Customer Groups", url('customer_groups'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Customer Group", url('customer_group.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Customer Group", url('customer_group.read', uuid=form.fieldset.model.uuid))}
  • + % endif + + +${parent.body()} diff --git a/rattail/pyramid/templates/customergroups/index.mako b/rattail/pyramid/templates/customergroups/index.mako new file mode 100644 index 00000000..dc777762 --- /dev/null +++ b/rattail/pyramid/templates/customergroups/index.mako @@ -0,0 +1,11 @@ +<%inherit file="/grid.mako" /> + +<%def name="title()">Customer Groups + +<%def name="context_menu_items()"> + % if request.has_perm('customer_groups.create'): +
  • ${h.link_to("Create a new Customer Group", url('customer_group.create'))}
  • + % endif + + +${parent.body()} diff --git a/rattail/pyramid/templates/customers/crud.mako b/rattail/pyramid/templates/customers/crud.mako index c8393e94..98d13572 100644 --- a/rattail/pyramid/templates/customers/crud.mako +++ b/rattail/pyramid/templates/customers/crud.mako @@ -1,7 +1,12 @@ <%inherit file="/crud.mako" /> <%def name="context_menu_items()"> -

    ${h.link_to("Back to Customers", url('customers'))}

    +
  • ${h.link_to("Back to Customers", url('customers'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Customer", url('customer.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Customer", url('customer.read', uuid=form.fieldset.model.uuid))}
  • + % endif ${parent.body()} diff --git a/rattail/pyramid/templates/employees/crud.mako b/rattail/pyramid/templates/employees/crud.mako new file mode 100644 index 00000000..9a6bbe0a --- /dev/null +++ b/rattail/pyramid/templates/employees/crud.mako @@ -0,0 +1,12 @@ +<%inherit file="/crud.mako" /> + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to Employees", url('employees'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Employee", url('employee.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Employee", url('employee.read', uuid=form.fieldset.model.uuid))}
  • + % endif + + +${parent.body()} diff --git a/rattail/pyramid/templates/people/crud.mako b/rattail/pyramid/templates/people/crud.mako new file mode 100644 index 00000000..9f7a5b0d --- /dev/null +++ b/rattail/pyramid/templates/people/crud.mako @@ -0,0 +1,12 @@ +<%inherit file="/crud.mako" /> + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to People", url('people'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Person", url('person.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Person", url('person.read', uuid=form.fieldset.model.uuid))}
  • + % endif + + +${parent.body()} diff --git a/rattail/pyramid/templates/stores/crud.mako b/rattail/pyramid/templates/stores/crud.mako index 7c690077..9f885fbc 100644 --- a/rattail/pyramid/templates/stores/crud.mako +++ b/rattail/pyramid/templates/stores/crud.mako @@ -2,6 +2,11 @@ <%def name="context_menu_items()">
  • ${h.link_to("Back to Stores", url('stores'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Store", url('store.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Store", url('store.read', uuid=form.fieldset.model.uuid))}
  • + % endif ${parent.body()} diff --git a/rattail/pyramid/templates/users/crud.mako b/rattail/pyramid/templates/users/crud.mako new file mode 100644 index 00000000..2e5a70ef --- /dev/null +++ b/rattail/pyramid/templates/users/crud.mako @@ -0,0 +1,12 @@ +<%inherit file="/crud.mako" /> + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to Users", url('users'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}
  • + % endif + + +${parent.body()} diff --git a/rattail/pyramid/views/__init__.py b/rattail/pyramid/views/__init__.py index a8b3fc05..e5820a78 100644 --- a/rattail/pyramid/views/__init__.py +++ b/rattail/pyramid/views/__init__.py @@ -33,7 +33,7 @@ from rattail.pyramid.views.autocomplete import * def includeme(config): config.include('rattail.pyramid.views.batches') # config.include('rattail.pyramid.views.categories') - config.include('rattail.pyramid.views.customer_groups') + config.include('rattail.pyramid.views.customergroups') config.include('rattail.pyramid.views.customers') config.include('rattail.pyramid.views.departments') config.include('rattail.pyramid.views.employees') diff --git a/rattail/pyramid/views/brands.py b/rattail/pyramid/views/brands.py index 2a6ee618..5cc9009b 100644 --- a/rattail/pyramid/views/brands.py +++ b/rattail/pyramid/views/brands.py @@ -88,39 +88,38 @@ class BrandsAutocomplete(AutocompleteView): fieldname = 'name' -def includeme(config): - +def add_routes(config): config.add_route('brands', '/brands') + config.add_route('brands.autocomplete', '/brands/autocomplete') + config.add_route('brand.create', '/brands/new') + config.add_route('brand.read', '/brands/{uuid}') + config.add_route('brand.update', '/brands/{uuid}/edit') + config.add_route('brand.delete', '/brands/{uuid}/delete') + + +def includeme(config): + add_routes(config) + config.add_view(BrandsGrid, route_name='brands', renderer='/brands/index.mako', permission='brands.list') - - config.add_route('brands.autocomplete', '/brands/autocomplete') config.add_view(BrandsAutocomplete, route_name='brands.autocomplete', renderer='json', permission='brands.list') - - config.add_route('brand.create', '/brands/new') config.add_view(BrandCrud, attr='create', route_name='brand.create', renderer='/brands/crud.mako', permission='brands.create') - - config.add_route('brand.read', '/brands/{uuid}') config.add_view(BrandCrud, attr='read', route_name='brand.read', renderer='/brands/crud.mako', permission='brands.read') - - config.add_route('brand.update', '/brands/{uuid}/edit') config.add_view(BrandCrud, attr='update', route_name='brand.update', renderer='/brands/crud.mako', permission='brands.update') - - config.add_route('brand.delete', '/brands/{uuid}/delete') config.add_view(BrandCrud, attr='delete', route_name='brand.delete', permission='brands.delete') diff --git a/rattail/pyramid/views/customer_groups.py b/rattail/pyramid/views/customergroups.py similarity index 61% rename from rattail/pyramid/views/customer_groups.py rename to rattail/pyramid/views/customergroups.py index 83056dcc..c3832410 100644 --- a/rattail/pyramid/views/customer_groups.py +++ b/rattail/pyramid/views/customergroups.py @@ -56,6 +56,15 @@ class CustomerGroupsGrid(SearchableAlchemyGridView): g.name, ], readonly=True) + if self.request.has_perm('customer_groups.read'): + g.clickable = True + g.click_route_name = 'customer_group.read' + if self.request.has_perm('customer_groups.update'): + g.editable = True + g.edit_route_name = 'customer_group.update' + if self.request.has_perm('customer_groups.delete'): + g.deletable = True + g.delete_route_name = 'customer_group.delete' return g @@ -75,14 +84,28 @@ class CustomerGroupCrud(CrudView): return fs +def add_routes(config): + config.add_route('customer_groups', '/customer-groups') + config.add_route('customer_group.create', '/customer-groups/new') + config.add_route('customer_group.read', '/customer-groups/{uuid}') + config.add_route('customer_group.update', '/customer-groups/{uuid}/edit') + config.add_route('customer_group.delete', '/customer-groups/{uuid}/delete') + + def includeme(config): + add_routes(config) - config.add_route('customer_groups', '/customer-groups') config.add_view(CustomerGroupsGrid, route_name='customer_groups', - renderer='/customer_groups/index.mako', + renderer='/customergroups/index.mako', permission='customer_groups.list') - - config.add_route('customer_group.read', '/customer-groups/{uuid}') + config.add_view(CustomerGroupCrud, attr='create', route_name='customer_group.create', + renderer='/customergroups/crud.mako', + permission='customer_groups.create') config.add_view(CustomerGroupCrud, attr='read', route_name='customer_group.read', - renderer='/customer_groups/crud.mako', + renderer='/customergroups/crud.mako', permission='customer_groups.read') + config.add_view(CustomerGroupCrud, attr='update', route_name='customer_group.update', + renderer='/customergroups/crud.mako', + permission='customer_groups.update') + config.add_view(CustomerGroupCrud, attr='delete', route_name='customer_group.delete', + permission='customer_groups.delete') diff --git a/rattail/pyramid/views/customers.py b/rattail/pyramid/views/customers.py index 32591646..99206072 100644 --- a/rattail/pyramid/views/customers.py +++ b/rattail/pyramid/views/customers.py @@ -90,7 +90,17 @@ class CustomersGrid(SearchableAlchemyGridView): g.email.label("Email Address"), ], readonly=True) - g.click_route_name = 'customer.read' + + if self.request.has_perm('customers.read'): + g.clickable = True + g.click_route_name = 'customer.read' + if self.request.has_perm('customers.update'): + g.editable = True + g.edit_route_name = 'customer.update' + if self.request.has_perm('customers.delete'): + g.deletable = True + g.delete_route_name = 'customer.delete' + return g @@ -121,16 +131,19 @@ class CustomerCrud(CrudView): include=[ fs.id.label("ID"), fs.name, - fs.phone.label("Phone Number"), - fs.email.label("Email Address"), + fs.phone.label("Phone Number").readonly(), + fs.email.label("Email Address").readonly(), fs.email_preference, ]) return fs def add_routes(config): - config.add_route('customers', '/customers') - config.add_route('customer.read', '/customers/{uuid}') + config.add_route('customers', '/customers') + config.add_route('customer.create', '/customers/new') + config.add_route('customer.read', '/customers/{uuid}') + config.add_route('customer.update', '/customers/{uuid}/edit') + config.add_route('customer.delete', '/customers/{uuid}/delete') def includeme(config): @@ -139,6 +152,14 @@ def includeme(config): config.add_view(CustomersGrid, route_name='customers', renderer='/customers/index.mako', permission='customers.list') + config.add_view(CustomerCrud, attr='create', route_name='customer.create', + renderer='/customers/crud.mako', + permission='customers.create') config.add_view(CustomerCrud, attr='read', route_name='customer.read', renderer='/customers/read.mako', permission='customers.read') + config.add_view(CustomerCrud, attr='update', route_name='customer.update', + renderer='/customers/crud.mako', + permission='customers.update') + config.add_view(CustomerCrud, attr='delete', route_name='customer.delete', + permission='customers.delete') diff --git a/rattail/pyramid/views/employees.py b/rattail/pyramid/views/employees.py index 8fd7cbac..56fad799 100644 --- a/rattail/pyramid/views/employees.py +++ b/rattail/pyramid/views/employees.py @@ -28,33 +28,40 @@ from sqlalchemy import and_ -import edbob -from edbob.pyramid.forms import AssociationProxyField, EnumFieldRenderer from edbob.pyramid.views import SearchableAlchemyGridView -import rattail +from rattail.pyramid.views import CrudView from rattail.pyramid.grids import EnumSearchFilter +from rattail.pyramid.forms import AssociationProxyField, EnumFieldRenderer +from rattail.db.model import ( + Employee, EmployeePhoneNumber, EmployeeEmailAddress, Person) +from rattail.enum import EMPLOYEE_STATUS, EMPLOYEE_STATUS_CURRENT class EmployeesGrid(SearchableAlchemyGridView): - mapped_class = rattail.Employee + mapped_class = Employee config_prefix = 'employees' sort = 'first_name' def join_map(self): return { 'phone': - lambda q: q.outerjoin(rattail.EmployeePhoneNumber, and_( - rattail.EmployeePhoneNumber.parent_uuid == rattail.Employee.uuid, - rattail.EmployeePhoneNumber.preference == 1)), + lambda q: q.outerjoin(EmployeePhoneNumber, and_( + EmployeePhoneNumber.parent_uuid == Employee.uuid, + EmployeePhoneNumber.preference == 1)), + 'email': + lambda q: q.outerjoin(EmployeeEmailAddress, and_( + EmployeeEmailAddress.parent_uuid == Employee.uuid, + EmployeeEmailAddress.preference == 1)), } def filter_map(self): kwargs = dict( - first_name=self.filter_ilike(edbob.Person.first_name), - last_name=self.filter_ilike(edbob.Person.last_name), - phone=self.filter_ilike(rattail.EmployeePhoneNumber.number)) + first_name=self.filter_ilike(Person.first_name), + last_name=self.filter_ilike(Person.last_name), + phone=self.filter_ilike(EmployeePhoneNumber.number), + email=self.filter_ilike(EmployeeEmailAddress.address)) if self.request.has_perm('employees.edit'): kwargs.update(dict( exact=['id', 'status'])) @@ -66,27 +73,29 @@ class EmployeesGrid(SearchableAlchemyGridView): filter_type_first_name='lk', include_filter_last_name=True, filter_type_last_name='lk', - filter_label_phone="Phone Number") + filter_label_phone="Phone Number", + filter_label_email="Email Address") if self.request.has_perm('employees.edit'): kwargs.update(dict( filter_label_id="ID", include_filter_status=True, filter_type_status='is', - filter_factory_status=EnumSearchFilter(rattail.EMPLOYEE_STATUS), - status=rattail.EMPLOYEE_STATUS_CURRENT)) + filter_factory_status=EnumSearchFilter(EMPLOYEE_STATUS), + status=EMPLOYEE_STATUS_CURRENT)) return self.make_filter_config(**kwargs) def sort_map(self): return self.make_sort_map( - first_name=self.sorter(edbob.Person.first_name), - last_name=self.sorter(edbob.Person.last_name), - phone=self.sorter(rattail.EmployeePhoneNumber.number)) + first_name=self.sorter(Person.first_name), + last_name=self.sorter(Person.last_name), + phone=self.sorter(EmployeePhoneNumber.number), + email=self.sorter(EmployeeEmailAddress.address)) def query(self): q = self.make_query() - q = q.join(edbob.Person) + q = q.join(Person) if not self.request.has_perm('employees.edit'): - q = q.filter(rattail.Employee.status == rattail.EMPLOYEE_STATUS_CURRENT) + q = q.filter(Employee.status == EMPLOYEE_STATUS_CURRENT) return q def grid(self): @@ -99,18 +108,73 @@ class EmployeesGrid(SearchableAlchemyGridView): g.first_name, g.last_name, g.phone.label("Phone Number"), - g.status.with_renderer(EnumFieldRenderer(rattail.EMPLOYEE_STATUS)), + g.email.label("Email Address"), + g.status.with_renderer(EnumFieldRenderer(EMPLOYEE_STATUS)), ], readonly=True) + + # Hide ID and Status fields for unprivileged users. if not self.request.has_perm('employees.edit'): del g.id del g.status + + if self.request.has_perm('employees.read'): + g.clickable = True + g.click_route_name = 'employee.read' + if self.request.has_perm('employees.update'): + g.editable = True + g.edit_route_name = 'employee.update' + if self.request.has_perm('employees.delete'): + g.deletable = True + g.delete_route_name = 'employee.delete' + return g -def includeme(config): +class EmployeeCrud(CrudView): + + mapped_class = Employee + home_route = 'employees' + + def fieldset(self, model): + fs = self.make_fieldset(model) + fs.append(AssociationProxyField('first_name')) + fs.append(AssociationProxyField('last_name')) + fs.append(AssociationProxyField('display_name')) + fs.configure( + include=[ + fs.id.label("ID"), + fs.first_name, + fs.last_name, + fs.phone.label("Phone Number").readonly(), + fs.email.label("Email Address").readonly(), + fs.status.with_renderer(EnumFieldRenderer(EMPLOYEE_STATUS)), + ]) + return fs + + +def add_routes(config): + config.add_route('employees', '/employees') + config.add_route('employee.create', '/employees/new') + config.add_route('employee.read', '/employees/{uuid}') + config.add_route('employee.update', '/employees/{uuid}/edit') + config.add_route('employee.delete', '/employees/{uuid}/delete') + + +def includeme(config): + add_routes(config) - config.add_route('employees', '/employees') config.add_view(EmployeesGrid, route_name='employees', renderer='/employees/index.mako', permission='employees.list') + config.add_view(EmployeeCrud, attr='create', route_name='employee.create', + renderer='/employees/crud.mako', + permission='employees.create') + config.add_view(EmployeeCrud, attr='read', route_name='employee.read', + renderer='/employees/crud.mako', + permission='employees.read') + config.add_view(EmployeeCrud, attr='update', route_name='employee.update', + renderer='/employees/crud.mako', + permission='employees.update') + config.add_view(EmployeeCrud, attr='delete', route_name='employee.delete', + permission='employees.delete') diff --git a/rattail/pyramid/views/people.py b/rattail/pyramid/views/people.py index 0c0417f2..1d29e421 100644 --- a/rattail/pyramid/views/people.py +++ b/rattail/pyramid/views/people.py @@ -26,31 +26,132 @@ ``rattail.pyramid.views.people`` -- Person Views """ -import edbob -from edbob.pyramid.views import people +from sqlalchemy import and_ -from rattail.pyramid.views import AutocompleteView +from edbob.pyramid.views import SearchableAlchemyGridView + +from rattail.pyramid.views import CrudView, AutocompleteView +from rattail.pyramid import Session +from rattail.db.model import (Person, PersonEmailAddress, PersonPhoneNumber, + VendorContact) + + +class PeopleGrid(SearchableAlchemyGridView): + + mapped_class = Person + config_prefix = 'people' + sort = 'first_name' + + def join_map(self): + return { + 'email': + lambda q: q.outerjoin(PersonEmailAddress, and_( + PersonEmailAddress.parent_uuid == Person.uuid, + PersonEmailAddress.preference == 1)), + 'phone': + lambda q: q.outerjoin(PersonPhoneNumber, and_( + PersonPhoneNumber.parent_uuid == Person.uuid, + PersonPhoneNumber.preference == 1)), + } + + def filter_map(self): + return self.make_filter_map( + ilike=['first_name', 'last_name', 'display_name'], + email=self.filter_ilike(PersonEmailAddress.address), + phone=self.filter_ilike(PersonPhoneNumber.number)) + + def filter_config(self): + return self.make_filter_config( + include_filter_first_name=True, + filter_type_first_name='lk', + include_filter_last_name=True, + filter_type_last_name='lk', + filter_label_phone="Phone Number", + filter_label_email="Email Address") + + def sort_map(self): + return self.make_sort_map( + 'first_name', 'last_name', 'display_name', + email=self.sorter(PersonEmailAddress.address), + phone=self.sorter(PersonPhoneNumber.number)) + + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.first_name, + g.last_name, + g.display_name, + g.phone.label("Phone Number"), + g.email.label("Email Address"), + ], + readonly=True) + + if self.request.has_perm('people.read'): + g.clickable = True + g.click_route_name = 'person.read' + if self.request.has_perm('people.update'): + g.editable = True + g.edit_route_name = 'person.update' + # if self.request.has_perm('products.delete'): + # g.deletable = True + # g.delete_route_name = 'product.delete' + + return g + + +class PersonCrud(CrudView): + + mapped_class = Person + home_route = 'people' + + def get_model(self, key): + model = super(PersonCrud, self).get_model(key) + if model: + return model + model = Session.query(VendorContact).get(key) + if model: + return model.person + return None + + def fieldset(self, model): + fs = self.make_fieldset(model) + fs.configure( + include=[ + fs.first_name, + fs.last_name, + fs.display_name, + fs.phone.label("Phone Number").readonly(), + fs.email.label("Email Address").readonly(), + ]) + return fs class PeopleAutocomplete(AutocompleteView): - mapped_class = edbob.Person + mapped_class = Person fieldname = 'display_name' -def includeme(config): +def add_routes(config): + config.add_route('people', '/people') + config.add_route('people.autocomplete', '/people/autocomplete') + config.add_route('person.read', '/people/{uuid}') + config.add_route('person.update', '/people/{uuid}/edit') - config.add_route('people', '/people') - config.add_view(people.PeopleGrid, route_name='people', + +def includeme(config): + add_routes(config) + + config.add_view(PeopleGrid, route_name='people', renderer='/people/index.mako', permission='people.list') - - config.add_route('people.autocomplete', '/people/autocomplete') + config.add_view(PersonCrud, attr='read', route_name='person.read', + renderer='/people/crud.mako', + permission='people.read') + config.add_view(PersonCrud, attr='update', route_name='person.update', + renderer='/people/crud.mako', + permission='people.update') config.add_view(PeopleAutocomplete, route_name='people.autocomplete', renderer='json', permission='people.list') - - config.add_route('person.read', '/people/{uuid}') - config.add_view(people.PersonCrud, attr='read', route_name='person.read', - renderer='/people/crud.mako', - permission='people.read') diff --git a/rattail/pyramid/views/products.py b/rattail/pyramid/views/products.py index d670b687..c9ce401b 100644 --- a/rattail/pyramid/views/products.py +++ b/rattail/pyramid/views/products.py @@ -47,7 +47,8 @@ from rattail.exceptions import LabelPrintingError from rattail.db.model import ProductPrice from rattail.pyramid import Session -from rattail.pyramid.forms import GPCFieldRenderer, PriceFieldRenderer +from rattail.pyramid.forms import (AutocompleteFieldRenderer, + GPCFieldRenderer, PriceFieldRenderer) from rattail.pyramid.views import CrudView @@ -212,6 +213,8 @@ class ProductCrud(CrudView): def fieldset(self, model): fs = self.make_fieldset(model) fs.upc.set(renderer=GPCFieldRenderer) + fs.brand.set(renderer=AutocompleteFieldRenderer( + self.request.route_url('brands.autocomplete'))) fs.regular_price.set(renderer=PriceFieldRenderer) fs.current_price.set(renderer=PriceFieldRenderer) fs.configure( @@ -225,9 +228,9 @@ class ProductCrud(CrudView): fs.regular_price, fs.current_price, ]) - # if not self.readonly: - # del fs.regular_price - # del fs.current_price + if not self.readonly: + del fs.regular_price + del fs.current_price return fs diff --git a/rattail/pyramid/views/stores.py b/rattail/pyramid/views/stores.py index 476acadd..197c055f 100644 --- a/rattail/pyramid/views/stores.py +++ b/rattail/pyramid/views/stores.py @@ -37,7 +37,7 @@ class StoresGrid(SearchableAlchemyGridView): mapped_class = rattail.Store config_prefix = 'stores' - sort = 'name' + sort = 'id' def join_map(self): return { diff --git a/rattail/pyramid/views/users.py b/rattail/pyramid/views/users.py index e2b5ae66..169de42f 100644 --- a/rattail/pyramid/views/users.py +++ b/rattail/pyramid/views/users.py @@ -28,30 +28,11 @@ import formalchemy -from webhelpers.html import tags - import edbob from edbob.pyramid.views import users -from edbob.pyramid.forms import AutocompleteFieldRenderer from rattail.pyramid.views import CrudView - - -def PersonFieldRenderer(url): - - BaseRenderer = AutocompleteFieldRenderer(url) - - class PersonFieldRenderer(BaseRenderer): - - def render_readonly(self, **kwargs): - person = self.raw_value - if not person: - return '' - return tags.link_to( - str(person), - self.request.route_url('person.read', uuid=person.uuid)) - - return PersonFieldRenderer +from rattail.pyramid.forms import PersonFieldRenderer class UserCrud(CrudView): diff --git a/rattail/pyramid/views/vendors.py b/rattail/pyramid/views/vendors.py index 97097a9e..2b8d7c96 100644 --- a/rattail/pyramid/views/vendors.py +++ b/rattail/pyramid/views/vendors.py @@ -26,15 +26,16 @@ ``rattail.pyramid.views.vendors`` -- Vendor Views """ -from edbob.pyramid.views import (SearchableAlchemyGridView, CrudView, - AutocompleteView) +from edbob.pyramid.views import SearchableAlchemyGridView -import rattail +from rattail.db.model import Vendor +from rattail.pyramid.views import CrudView, AutocompleteView +from rattail.pyramid.forms import AssociationProxyField, PersonFieldRenderer class VendorsGrid(SearchableAlchemyGridView): - mapped_class = rattail.Vendor + mapped_class = Vendor config_prefix = 'vendors' sort = 'name' @@ -52,11 +53,13 @@ class VendorsGrid(SearchableAlchemyGridView): def grid(self): g = self.make_grid() + g.append(AssociationProxyField('contact')) g.configure( include=[ g.id.label("ID"), g.name, - g.phone, + g.phone.label("Phone Number"), + g.email.label("Email Address"), g.contact, ], readonly=True) @@ -74,24 +77,29 @@ class VendorsGrid(SearchableAlchemyGridView): class VendorCrud(CrudView): - mapped_class = rattail.Vendor + mapped_class = Vendor home_route = 'vendors' def fieldset(self, model): fs = self.make_fieldset(model) + fs.append(AssociationProxyField('contact')) + fs.contact.set(renderer=PersonFieldRenderer( + self.request.route_url('people.autocomplete'))) fs.configure( include=[ fs.id.label("ID"), fs.name, fs.special_discount, - fs.email.label("Email Address"), + fs.phone.label("Phone Number").readonly(), + fs.email.label("Email Address").readonly(), + fs.contact.readonly(), ]) return fs class VendorsAutocomplete(AutocompleteView): - mapped_class = rattail.Vendor + mapped_class = Vendor fieldname = 'name'