From b7fdd1f797e46cef7e74ab4105e1ae54e76491d3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 22 Jul 2013 21:32:54 -0700 Subject: [PATCH] Replaced `Grid.clickable` with `.viewable`. Clickable grid rows seemed to be more irritating than useful. Now a view icon is shown instead. --- MANIFEST.in | 10 +- rattail/pyramid/__init__.py | 1 + rattail/pyramid/forms/renderers/__init__.py | 5 +- rattail/pyramid/forms/renderers/common.py | 28 ++- rattail/pyramid/grids/alchemy.py | 9 +- rattail/pyramid/grids/core.py | 42 ++-- rattail/pyramid/static/__init__.py | 31 +++ rattail/pyramid/static/css/grids.css | 164 +++++++++++++ rattail/pyramid/static/img/delete.png | Bin 0 -> 641 bytes rattail/pyramid/static/img/edit.png | Bin 0 -> 533 bytes rattail/pyramid/static/img/view.png | Bin 0 -> 616 bytes .../templates/forms/field_autocomplete.mako | 3 + rattail/pyramid/templates/grids/grid.mako | 63 +++++ rattail/pyramid/views/__init__.py | 1 + rattail/pyramid/views/batches/core.py | 4 +- rattail/pyramid/views/batches/rows.py | 6 +- rattail/pyramid/views/brands.py | 4 +- rattail/pyramid/views/categories.py | 4 +- rattail/pyramid/views/customergroups.py | 4 +- rattail/pyramid/views/customers.py | 5 +- rattail/pyramid/views/departments.py | 4 +- rattail/pyramid/views/employees.py | 4 +- rattail/pyramid/views/grids/core.py | 2 - rattail/pyramid/views/labels.py | 4 +- rattail/pyramid/views/people.py | 4 +- rattail/pyramid/views/products.py | 4 +- rattail/pyramid/views/roles.py | 218 ++++++++++++++++++ rattail/pyramid/views/stores.py | 4 +- rattail/pyramid/views/subdepartments.py | 4 +- rattail/pyramid/views/users.py | 55 ++++- rattail/pyramid/views/vendors.py | 4 +- tests/views/test_departments.py | 10 +- 32 files changed, 627 insertions(+), 74 deletions(-) create mode 100644 rattail/pyramid/static/__init__.py create mode 100644 rattail/pyramid/static/css/grids.css create mode 100644 rattail/pyramid/static/img/delete.png create mode 100644 rattail/pyramid/static/img/edit.png create mode 100644 rattail/pyramid/static/img/view.png create mode 100644 rattail/pyramid/templates/forms/field_autocomplete.mako create mode 100644 rattail/pyramid/templates/grids/grid.mako create mode 100644 rattail/pyramid/views/roles.py diff --git a/MANIFEST.in b/MANIFEST.in index a5c09028..d6ac3959 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,8 @@ -include *.txt *.ini *.cfg *.rst -recursive-include rattail/pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml + +include *.txt *.py + +recursive-include rattail/pyramid/static *.css +recursive-include rattail/pyramid/static *.png + +recursive-include rattail/pyramid/templates *.mako +recursive-include rattail/pyramid/reports *.mako diff --git a/rattail/pyramid/__init__.py b/rattail/pyramid/__init__.py index 5dc85585..2eb735f7 100644 --- a/rattail/pyramid/__init__.py +++ b/rattail/pyramid/__init__.py @@ -32,5 +32,6 @@ from edbob.pyramid import Session def includeme(config): + config.include('rattail.pyramid.static') config.include('rattail.pyramid.subscribers') config.include('rattail.pyramid.views') diff --git a/rattail/pyramid/forms/renderers/__init__.py b/rattail/pyramid/forms/renderers/__init__.py index b1cd38dc..6bf1595f 100644 --- a/rattail/pyramid/forms/renderers/__init__.py +++ b/rattail/pyramid/forms/renderers/__init__.py @@ -32,13 +32,12 @@ from webhelpers.html import tags import formalchemy from edbob.pyramid.forms import pretty_datetime -from edbob.pyramid.forms.formalchemy.renderers import ( - AutocompleteFieldRenderer, YesNoFieldRenderer) +from edbob.pyramid.forms.formalchemy.renderers import YesNoFieldRenderer import rattail from rattail.gpc import GPC -from .common import EnumFieldRenderer +from .common import AutocompleteFieldRenderer, EnumFieldRenderer from .products import ProductFieldRenderer from .users import UserFieldRenderer diff --git a/rattail/pyramid/forms/renderers/common.py b/rattail/pyramid/forms/renderers/common.py index da945b51..ccc0b392 100644 --- a/rattail/pyramid/forms/renderers/common.py +++ b/rattail/pyramid/forms/renderers/common.py @@ -29,7 +29,33 @@ from formalchemy.fields import SelectFieldRenderer -__all__ = ['EnumFieldRenderer'] +__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer'] + + +def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'): + """ + Returns a custom renderer class for an autocomplete field. + """ + + class AutocompleteFieldRenderer(formalchemy.fields.FieldRenderer): + + @property + def focus_name(self): + return self.name + '-textbox' + + @property + def needs_focus(self): + return not bool(self.value or field_value) + + def render(self, **kwargs): + kwargs.setdefault('field_name', self.name) + kwargs.setdefault('field_value', self.value or field_value) + kwargs.setdefault('field_display', self.raw_value or field_display) + kwargs.setdefault('service_url', service_url) + kwargs.setdefault('width', width) + return render('/forms/field_autocomplete.mako', kwargs) + + return AutocompleteFieldRenderer def EnumFieldRenderer(enum): diff --git a/rattail/pyramid/grids/alchemy.py b/rattail/pyramid/grids/alchemy.py index d6d69bd3..39947b6f 100644 --- a/rattail/pyramid/grids/alchemy.py +++ b/rattail/pyramid/grids/alchemy.py @@ -53,7 +53,6 @@ class AlchemyGrid(Grid): self._formalchemy_grid = formalchemy.Grid( cls, instances, session=Session(), request=request) self._formalchemy_grid.prettify = prettify - self.noclick_fields = [] def __delattr__(self, attr): delattr(self._formalchemy_grid, attr) @@ -63,16 +62,11 @@ class AlchemyGrid(Grid): def cell_class(self, field): classes = [field.name] - if field.name in self.noclick_fields: - classes.append('noclick') return ' '.join(classes) def checkbox(self, row): return tags.checkbox('check-'+row.uuid) - def click_route_kwargs(self, row): - return {'uuid': row.uuid} - def column_header(self, field): class_ = None label = field.label() @@ -84,6 +78,9 @@ class AlchemyGrid(Grid): return HTML.tag('th', class_=class_, field=field.key, title=self.column_titles.get(field.key), c=label) + def view_route_kwargs(self, row): + return {'uuid': row.uuid} + def edit_route_kwargs(self, row): return {'uuid': row.uuid} diff --git a/rattail/pyramid/grids/core.py b/rattail/pyramid/grids/core.py index d1e05a49..c6e2787a 100644 --- a/rattail/pyramid/grids/core.py +++ b/rattail/pyramid/grids/core.py @@ -46,19 +46,18 @@ class Grid(Object): full = False hoverable = True - clickable = False checkboxes = False - editable = False - deletable = False - partial_only = False - click_route_name = None - click_route_kwargs = None + viewable = False + view_route_name = None + view_route_kwargs = None + editable = False edit_route_name = None edit_route_kwargs = None + deletable = False delete_route_name = None delete_route_kwargs = None @@ -82,22 +81,20 @@ class Grid(Object): classes = ['grid'] if self.full: classes.append('full') - if self.clickable: - classes.append('clickable') if self.hoverable: classes.append('hoverable') return format_attrs( class_=' '.join(classes), url=self.request.current_route_url()) - def get_delete_url(self, row): + def get_view_url(self, row): kwargs = {} - if self.delete_route_kwargs: - if callable(self.delete_route_kwargs): - kwargs = self.delete_route_kwargs(row) + if self.view_route_kwargs: + if callable(self.view_route_kwargs): + kwargs = self.view_route_kwargs(row) else: - kwargs = self.delete_route_kwargs - return self.request.route_url(self.delete_route_name, **kwargs) + kwargs = self.view_route_kwargs + return self.request.route_url(self.view_route_name, **kwargs) def get_edit_url(self, row): kwargs = {} @@ -108,16 +105,17 @@ class Grid(Object): kwargs = self.edit_route_kwargs return self.request.route_url(self.edit_route_name, **kwargs) + def get_delete_url(self, row): + kwargs = {} + if self.delete_route_kwargs: + if callable(self.delete_route_kwargs): + kwargs = self.delete_route_kwargs(row) + else: + kwargs = self.delete_route_kwargs + return self.request.route_url(self.delete_route_name, **kwargs) + def get_row_attrs(self, row, i): attrs = self.row_attrs(row, i) - if self.clickable: - kwargs = {} - if self.click_route_kwargs: - if callable(self.click_route_kwargs): - kwargs = self.click_route_kwargs(row) - else: - kwargs = self.click_route_kwargs - attrs['url'] = self.request.route_url(self.click_route_name, **kwargs) return format_attrs(**attrs) def iter_fields(self): diff --git a/rattail/pyramid/static/__init__.py b/rattail/pyramid/static/__init__.py new file mode 100644 index 00000000..13d8fe01 --- /dev/null +++ b/rattail/pyramid/static/__init__.py @@ -0,0 +1,31 @@ +#!/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.static`` -- Static Assets +""" + + +def includeme(config): + config.add_static_view('rattail', 'rattail.pyramid:static') diff --git a/rattail/pyramid/static/css/grids.css b/rattail/pyramid/static/css/grids.css new file mode 100644 index 00000000..18ecf41a --- /dev/null +++ b/rattail/pyramid/static/css/grids.css @@ -0,0 +1,164 @@ + +/****************************** + * Grid Header + ******************************/ + +table.grid-header { + padding-bottom: 5px; + width: 100%; +} + + +/****************************** + * Form (Filters etc.) + ******************************/ + +table.grid-header td.form { + vertical-align: bottom; +} + + +/****************************** + * Context Menu + ******************************/ + +table.grid-header td.context-menu { + vertical-align: top; +} + +table.grid-header td.context-menu ul { + list-style-type: none; + margin: 0px; + text-align: right; +} + +table.grid-header td.context-menu ul li { + line-height: 2em; +} + +/****************************** + * Tools + ******************************/ + +table.grid-header td.tools { + padding-bottom: 10px; + text-align: right; + vertical-align: bottom; +} + +table.grid-header td.tools div.buttons button { + margin-left: 5px; +} + + +/****************************** + * Grid + ******************************/ + +div.grid { + clear: both; +} + +div.grid table { + border-top: 1px solid black; + border-left: 1px solid black; + border-collapse: collapse; + font-size: 9pt; + line-height: normal; + white-space: nowrap; +} + +div.grid.full table { + width: 100%; +} + +div.grid table th, +div.grid table td { + border-right: 1px solid black; + border-bottom: 1px solid black; + padding: 2px 3px; +} + +div.grid table th.sortable a { + display: block; + padding-right: 18px; +} + +div.grid table th.sorted { + background-position: right center; + background-repeat: no-repeat; +} + +div.grid table th.sorted.asc { + background-image: url(../img/sort_arrow_up.png); +} + +div.grid table th.sorted.desc { + background-image: url(../img/sort_arrow_down.png); +} + +div.grid table tbody td { + text-align: left; +} + +div.grid table tbody td.center { + text-align: center; +} + +div.grid table tbody td.right { + float: none; + text-align: right; +} + +div.grid table tr.odd { + background-color: #e0e0e0; +} + +div.grid table tbody tr.hovering { + background-color: #bbbbbb; +} + +div.grid table tbody tr td.view, +div.grid table tbody tr td.edit, +div.grid table tbody tr td.delete { + background-repeat: no-repeat; + background-position: center; + cursor: pointer; + min-width: 18px; + text-align: center; + width: 18px; +} + +div.grid table tbody tr td.view { + background-image: url(../img/view.png); +} + +div.grid table tbody tr td.edit { + background-image: url(../img/edit.png); +} + +div.grid table tbody tr td.delete { + background-image: url(../img/delete.png); +} + +div.pager { + margin-bottom: 20px; + margin-top: 5px; +} + +div.pager p { + font-size: 10pt; + margin: 0px; +} + +div.pager p.showing { + float: left; +} + +div.pager #grid-page-count { + font-size: 8pt; +} + +div.pager p.page-links { + float: right; +} diff --git a/rattail/pyramid/static/img/delete.png b/rattail/pyramid/static/img/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..f3383853ef7fa39ba74afe2af1646900b0b28008 GIT binary patch literal 641 zcmV-{0)G98P)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RW1qK8iDtdUE_5c6^6G=otR4C7V zl1)z&Q51%sd+)T<4=62`g<=pA8yC1S>c)*BMq`XCQCBYg4@Tn$xG?b#s9P5r-5QA> z5jRMPx{!kDf<`R?4DHa;&XgJ2I&)oM(JJv>ot)%(-<-S$fETYO_(PFf<2NrqdHVSC zwd+^LA~7?{4ifF*ATOLO+4%X z??1SAb#!#}j_>bT+jNER`^?O|e>8UE_9XxVAQTRrO?3CL z+3*lTh~eRb^zZG*^IYm1b>aIyN+}|yd9Ix(0hX7Sa&z;uSXK$obukQscsx!xY=Rbe z?j}o%%dD;za~)u-*?Kqmc9MKPkL}phYE>+2jWuhHy5mqPT4d+uDVNLBZO{NnCX+IA zAcGMKkQs~*=YV# zhwSJ^2(;GdKobN32!Rk{ry93FjYbNIkg}s*CDs2y5GbXPLZCIsUj|D329=WA`|TTR z)oQ}?T!fSev1OvQMk$3-3NvgH3L6A|(AAN+u#hiCqp{Y?_a(xnL9^AMFWpBp79kdo zk?QTi_kCTpYhV8U)#%8`@k57?9yygrbRSEn(*r`v*hanHuqPx#32;bRa{vGe@Bjb`@Bu=sG?)MY00(qQO+^RX2ow_(0+{1gtpET4r%6OXR4C6) zlFe!qQ4obs-MY7X=wznHV1mm$fRErVJ6(%U;6}tZ5+A^o8<%k*xD!o8!6^8<5QIc# zdZxSYtsfU1iDy|Hiu%qe+H5vCXCh)|07OJYW+ozS+X9$|00I%k7@0YQ!2gs|$~kvk zhyN4-xckM$MM{a8`@V0QMpdUxtJSJqEEG=g0Km**jPB0N^Z8s=tEw`yuIqBnq8dPM z2mqLay8{pr5h0@OcFWAAl&Y%6aqM3od)EbbSC#d89YW~)-raM~?rvrW*KbsAZ{EKA z_3F&sB0%mQV;qJd#yF0nyPG)?4WrZ_?;ZYVuibybRTGH-z+Gdk%p|2GB9VjUlD^;F zo_*T8bKJ~!0Za#IUDv8QA&N)<@BVyHPEY9Qd714`VO;{{oJGVPGW0;2eSS#qPr9RL z*f|KWE9(RfAdiD6ExtVRZ>Q(Co|d#Xjzb7^X`M21_hA?~rX}9l*AKsMKCbr<=es)q z%>1$~GgH-Z80xxS{W-7Wl5f9QIN&Iyh)4*0RctAxT`q~JX?B$NbPx#32;bRa{vGf6951U69E94oEQKA00(qQO+^RY2Nwb`Gk%Db>;M1(`bk7VR4C7t z(o1L)Q544U@6KeJbn=`|I`I*0!<5<D;smrM6OH-fTPuzR>_!a=V5wd@WYw`G_gUW>%gabZ# zaL}VM13r%LJ}Ei${%m$W@lP0!U5*|*eJQ59ywtnX1XBkgLbn;5Bm)M9zb>TfnpR1! zhs?e{9~V->BcF8-YE2+tp=7BF!qBRKA zI`C5`u$w{l?iRpX9Rh%^Hkf!i-YEabtC9^w772O;pho~?5$F{FPFGPVe6InhwZT=j zY`l5(a)PUVgVO?dY@j=AfSU(8*%nq7J|guYH3^`)X*rpx>9oKz{gNmS+np}LnHbbG z6?4hAV4A;he!!~-GRe8g-&o#sj#Mf&zVszmFD)+)EzYJ*@18#?J-B-(p@^KJj5!YP z>1&*+%v2JyYkGOBPhkeI3xE>)s0>R9=&fLwA@QjDqj{hkLvzr5J2Y^4Dp|XVm42^F90000 + +${autocomplete(field_name, service_url, field_value, field_display, width=width, selected=selected, cleared=cleared)} diff --git a/rattail/pyramid/templates/grids/grid.mako b/rattail/pyramid/templates/grids/grid.mako new file mode 100644 index 00000000..0daf2a18 --- /dev/null +++ b/rattail/pyramid/templates/grids/grid.mako @@ -0,0 +1,63 @@ +
+ + + + % if grid.checkboxes: + + % endif + % for field in grid.iter_fields(): + ${grid.column_header(field)} + % endfor + % for col in grid.extra_columns: + + % endif + % if grid.editable: + + % endif + % if grid.deletable: + + % endif + + + + % for i, row in enumerate(grid.iter_rows(), 1): + + % if grid.checkboxes: + + % endif + % for field in grid.iter_fields(): + + % endfor + % for col in grid.extra_columns: + + % endfor + % if grid.viewable: + + % endif + % if grid.editable: + + % endif + % if grid.deletable: + + % endif + + % endfor + +
${h.checkbox('check-all')}${col.label} + % endfor + % if grid.viewable: +    
${grid.checkbox(row)}${grid.render_field(field)}${col.callback(row)}   
+ % if grid.pager: +
+

+ showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count} + (page ${grid.pager.page} of ${grid.pager.page_count}) +

+ +
+ % endif +
diff --git a/rattail/pyramid/views/__init__.py b/rattail/pyramid/views/__init__.py index 4503353c..1ec6624c 100644 --- a/rattail/pyramid/views/__init__.py +++ b/rattail/pyramid/views/__init__.py @@ -41,6 +41,7 @@ def includeme(config): config.include('rattail.pyramid.views.employees') config.include('rattail.pyramid.views.labels') config.include('rattail.pyramid.views.products') + config.include('rattail.pyramid.views.roles') config.include('rattail.pyramid.views.stores') config.include('rattail.pyramid.views.subdepartments') config.include('rattail.pyramid.views.vendors') diff --git a/rattail/pyramid/views/batches/core.py b/rattail/pyramid/views/batches/core.py index 5a9a2cc8..11ac551f 100644 --- a/rattail/pyramid/views/batches/core.py +++ b/rattail/pyramid/views/batches/core.py @@ -101,8 +101,8 @@ class BatchesGrid(SearchableAlchemyGridView): return tags.link_to("View Rows", self.request.route_url( 'batch.rows', uuid=row.uuid)) g.add_column('rows', "", rows) - g.clickable = True - g.click_route_name = 'batch.read' + g.viewable = True + g.view_route_name = 'batch.read' if self.request.has_perm('batches.update'): g.editable = True g.edit_route_name = 'batch.update' diff --git a/rattail/pyramid/views/batches/rows.py b/rattail/pyramid/views/batches/rows.py index 918f61d1..f6205d3e 100644 --- a/rattail/pyramid/views/batches/rows.py +++ b/rattail/pyramid/views/batches/rows.py @@ -97,9 +97,9 @@ def BatchRowsGrid(request): route_kwargs = lambda x: {'batch_uuid': x.batch.uuid, 'uuid': x.uuid} if self.request.has_perm('batch_rows.read'): - g.clickable = True - g.click_route_name = 'batch_row.read' - g.click_route_kwargs = route_kwargs + g.viewable = True + g.view_route_name = 'batch_row.read' + g.view_route_kwargs = route_kwargs if self.request.has_perm('batch_rows.update'): g.editable = True diff --git a/rattail/pyramid/views/brands.py b/rattail/pyramid/views/brands.py index a3b67fca..6fb31be8 100644 --- a/rattail/pyramid/views/brands.py +++ b/rattail/pyramid/views/brands.py @@ -57,8 +57,8 @@ class BrandsGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('brands.read'): - g.clickable = True - g.click_route_name = 'brand.read' + g.viewable = True + g.view_route_name = 'brand.read' if self.request.has_perm('brands.update'): g.editable = True g.edit_route_name = 'brand.update' diff --git a/rattail/pyramid/views/categories.py b/rattail/pyramid/views/categories.py index 2133d5f0..59b3509d 100644 --- a/rattail/pyramid/views/categories.py +++ b/rattail/pyramid/views/categories.py @@ -57,8 +57,8 @@ class CategoriesGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('categories.read'): - g.clickable = True - g.click_route_name = 'category.read' + g.viewable = True + g.view_route_name = 'category.read' if self.request.has_perm('categories.update'): g.editable = True g.edit_route_name = 'category.update' diff --git a/rattail/pyramid/views/customergroups.py b/rattail/pyramid/views/customergroups.py index 46da8784..121e2527 100644 --- a/rattail/pyramid/views/customergroups.py +++ b/rattail/pyramid/views/customergroups.py @@ -58,8 +58,8 @@ class CustomerGroupsGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('customer_groups.read'): - g.clickable = True - g.click_route_name = 'customer_group.read' + g.viewable = True + g.view_route_name = 'customer_group.read' if self.request.has_perm('customer_groups.update'): g.editable = True g.edit_route_name = 'customer_group.update' diff --git a/rattail/pyramid/views/customers.py b/rattail/pyramid/views/customers.py index 9a268d78..592a1b39 100644 --- a/rattail/pyramid/views/customers.py +++ b/rattail/pyramid/views/customers.py @@ -46,7 +46,6 @@ class CustomersGrid(SearchableAlchemyGridView): mapped_class = Customer config_prefix = 'customers' sort = 'name' - clickable = True def join_map(self): return { @@ -93,8 +92,8 @@ class CustomersGrid(SearchableAlchemyGridView): readonly=True) if self.request.has_perm('customers.read'): - g.clickable = True - g.click_route_name = 'customer.read' + g.viewable = True + g.view_route_name = 'customer.read' if self.request.has_perm('customers.update'): g.editable = True g.edit_route_name = 'customer.update' diff --git a/rattail/pyramid/views/departments.py b/rattail/pyramid/views/departments.py index baca4a9b..6e24001c 100644 --- a/rattail/pyramid/views/departments.py +++ b/rattail/pyramid/views/departments.py @@ -59,8 +59,8 @@ class DepartmentsGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('departments.read'): - g.clickable = True - g.click_route_name = 'department.read' + g.viewable = True + g.view_route_name = 'department.read' if self.request.has_perm('departments.update'): g.editable = True g.edit_route_name = 'department.update' diff --git a/rattail/pyramid/views/employees.py b/rattail/pyramid/views/employees.py index 4bd35ba7..340a7d80 100644 --- a/rattail/pyramid/views/employees.py +++ b/rattail/pyramid/views/employees.py @@ -117,8 +117,8 @@ class EmployeesGrid(SearchableAlchemyGridView): del g.status if self.request.has_perm('employees.read'): - g.clickable = True - g.click_route_name = 'employee.read' + g.viewable = True + g.view_route_name = 'employee.read' if self.request.has_perm('employees.update'): g.editable = True g.edit_route_name = 'employee.update' diff --git a/rattail/pyramid/views/grids/core.py b/rattail/pyramid/views/grids/core.py index c119b258..fa093ded 100644 --- a/rattail/pyramid/views/grids/core.py +++ b/rattail/pyramid/views/grids/core.py @@ -42,7 +42,6 @@ class GridView(View): full = False checkboxes = False - clickable = False deletable = False partial_only = False @@ -50,7 +49,6 @@ class GridView(View): def update_grid_kwargs(self, kwargs): kwargs.setdefault('full', self.full) kwargs.setdefault('checkboxes', self.checkboxes) - kwargs.setdefault('clickable', self.clickable) kwargs.setdefault('deletable', self.deletable) kwargs.setdefault('partial_only', self.partial_only) diff --git a/rattail/pyramid/views/labels.py b/rattail/pyramid/views/labels.py index b44327b8..7f8cc469 100644 --- a/rattail/pyramid/views/labels.py +++ b/rattail/pyramid/views/labels.py @@ -69,8 +69,8 @@ class ProfilesGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('label_profiles.read'): - g.clickable = True - g.click_route_name = 'label_profile.read' + g.viewable = True + g.view_route_name = 'label_profile.read' if self.request.has_perm('label_profiles.update'): g.editable = True g.edit_route_name = 'label_profile.update' diff --git a/rattail/pyramid/views/people.py b/rattail/pyramid/views/people.py index f183cd73..3de01766 100644 --- a/rattail/pyramid/views/people.py +++ b/rattail/pyramid/views/people.py @@ -88,8 +88,8 @@ class PeopleGrid(SearchableAlchemyGridView): readonly=True) if self.request.has_perm('people.read'): - g.clickable = True - g.click_route_name = 'person.read' + g.viewable = True + g.view_route_name = 'person.read' if self.request.has_perm('people.update'): g.editable = True g.edit_route_name = 'person.update' diff --git a/rattail/pyramid/views/products.py b/rattail/pyramid/views/products.py index 67998574..91c1f905 100644 --- a/rattail/pyramid/views/products.py +++ b/rattail/pyramid/views/products.py @@ -176,8 +176,8 @@ class ProductsGrid(SearchableAlchemyGridView): readonly=True) if self.request.has_perm('products.read'): - g.clickable = True - g.click_route_name = 'product.read' + g.viewable = True + g.view_route_name = 'product.read' if self.request.has_perm('products.update'): g.editable = True g.edit_route_name = 'product.update' diff --git a/rattail/pyramid/views/roles.py b/rattail/pyramid/views/roles.py new file mode 100644 index 00000000..76872a3c --- /dev/null +++ b/rattail/pyramid/views/roles.py @@ -0,0 +1,218 @@ +#!/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.views.roles`` -- Role Views +""" + +from pyramid.httpexceptions import HTTPFound + +import formalchemy +from webhelpers.html import tags +from webhelpers.html.builder import HTML + +from edbob.db import auth + +from rattail.pyramid import Session +from rattail.pyramid.views import SearchableAlchemyGridView, CrudView +from rattail.db.model import Role + + +default_permissions = [ + + ("People", [ + ('people.list', "List People"), + ('people.read', "View Person"), + ('people.create', "Create Person"), + ('people.update', "Edit Person"), + ('people.delete', "Delete Person"), + ]), + + ("Roles", [ + ('roles.list', "List Roles"), + ('roles.read', "View Role"), + ('roles.create', "Create Role"), + ('roles.update', "Edit Role"), + ('roles.delete', "Delete Role"), + ]), + + ("Users", [ + ('users.list', "List Users"), + ('users.read', "View User"), + ('users.create', "Create User"), + ('users.update', "Edit User"), + ('users.delete', "Delete User"), + ]), + ] + + +class RolesGrid(SearchableAlchemyGridView): + + mapped_class = Role + config_prefix = 'roles' + sort = 'name' + + def filter_map(self): + return self.make_filter_map(ilike=['name']) + + def filter_config(self): + return self.make_filter_config( + include_filter_name=True, + filter_type_name='lk') + + def sort_map(self): + return self.make_sort_map('name') + + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.name, + ], + readonly=True) + if self.request.has_perm('roles.read'): + g.viewable = True + g.view_route_name = 'role.read' + if self.request.has_perm('roles.update'): + g.editable = True + g.edit_route_name = 'role.update' + if self.request.has_perm('roles.delete'): + g.deletable = True + g.delete_route_name = 'role.delete' + return g + + +class PermissionsField(formalchemy.Field): + + def sync(self): + if not self.is_readonly(): + role = self.model + role.permissions = self.renderer.deserialize() + + +def PermissionsFieldRenderer(permissions, *args, **kwargs): + + perms = permissions + + class PermissionsFieldRenderer(formalchemy.FieldRenderer): + + permissions = perms + + def deserialize(self): + perms = [] + i = len(self.name) + 1 + for key in self.params: + if key.startswith(self.name): + perms.append(key[i:]) + return perms + + def _render(self, readonly=False, **kwargs): + role = self.field.model + admin = auth.administrator_role(Session()) + if role is admin: + html = HTML.tag('p', c="This is the administrative role; " + "it has full access to the entire system.") + if not readonly: + html += tags.hidden(self.name, value='') # ugly hack..or good idea? + else: + html = '' + for group, perms in self.permissions: + inner = HTML.tag('p', c=group) + for perm, title in perms: + checked = auth.has_permission( + role, perm, include_guest=False, session=Session()) + if readonly: + span = HTML.tag('span', c="[X]" if checked else "[ ]") + inner += HTML.tag('p', class_='perm', c=span + ' ' + title) + else: + inner += tags.checkbox(self.name + '-' + perm, + checked=checked, label=title) + html += HTML.tag('div', class_='group', c=inner) + return html + + def render(self, **kwargs): + return self._render(**kwargs) + + def render_readonly(self, **kwargs): + return self._render(readonly=True, **kwargs) + + return PermissionsFieldRenderer + + +class RoleCrud(CrudView): + + mapped_class = Role + home_route = 'roles' + permissions = default_permissions + + def fieldset(self, role): + fs = self.make_fieldset(role) + fs.append(PermissionsField( + 'permissions', + renderer=PermissionsFieldRenderer(self.permissions))) + fs.configure( + include=[ + fs.name, + fs.permissions, + ]) + return fs + + def pre_delete(self, model): + admin = auth.administrator_role(Session()) + guest = auth.guest_role(Session()) + if model in (admin, guest): + self.request.session.flash("You may not delete the %s role." % str(model), 'error') + return HTTPFound(location=self.request.get_referrer()) + + +def includeme(config): + + config.add_route('roles', '/roles') + config.add_view(RolesGrid, route_name='roles', + renderer='/roles/index.mako', + permission='roles.list') + + settings = config.get_settings() + perms = settings.get('edbob.permissions') + if perms: + RoleCrud.permissions = perms + + config.add_route('role.create', '/roles/new') + config.add_view(RoleCrud, attr='create', route_name='role.create', + renderer='/roles/crud.mako', + permission='roles.create') + + config.add_route('role.read', '/roles/{uuid}') + config.add_view(RoleCrud, attr='read', route_name='role.read', + renderer='/roles/crud.mako', + permission='roles.read') + + config.add_route('role.update', '/roles/{uuid}/edit') + config.add_view(RoleCrud, attr='update', route_name='role.update', + renderer='/roles/crud.mako', + permission='roles.update') + + config.add_route('role.delete', '/roles/{uuid}/delete') + config.add_view(RoleCrud, attr='delete', route_name='role.delete', + permission='roles.delete') diff --git a/rattail/pyramid/views/stores.py b/rattail/pyramid/views/stores.py index 93662ffc..864f2ec0 100644 --- a/rattail/pyramid/views/stores.py +++ b/rattail/pyramid/views/stores.py @@ -79,8 +79,8 @@ class StoresGrid(SearchableAlchemyGridView): g.email.label("Email Address"), ], readonly=True) - g.clickable = True - g.click_route_name = 'store.read' + g.viewable = True + g.view_route_name = 'store.read' if self.request.has_perm('stores.update'): g.editable = True g.edit_route_name = 'store.update' diff --git a/rattail/pyramid/views/subdepartments.py b/rattail/pyramid/views/subdepartments.py index 5d71d698..45218239 100644 --- a/rattail/pyramid/views/subdepartments.py +++ b/rattail/pyramid/views/subdepartments.py @@ -58,8 +58,8 @@ class SubdepartmentsGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('subdepartments.read'): - g.clickable = True - g.click_route_name = 'subdepartment.read' + g.viewable = True + g.view_route_name = 'subdepartment.read' if self.request.has_perm('subdepartments.update'): g.editable = True g.edit_route_name = 'subdepartment.update' diff --git a/rattail/pyramid/views/users.py b/rattail/pyramid/views/users.py index b7d4c5de..740229a7 100644 --- a/rattail/pyramid/views/users.py +++ b/rattail/pyramid/views/users.py @@ -30,9 +30,58 @@ import formalchemy from edbob.pyramid.views import users -from rattail.pyramid.views import CrudView +from rattail.pyramid.views import SearchableAlchemyGridView, CrudView from rattail.pyramid.forms import PersonFieldRenderer -from rattail.db.model import User +from rattail.db.model import User, Person + + +class UsersGrid(SearchableAlchemyGridView): + + mapped_class = User + config_prefix = 'users' + sort = 'username' + + def join_map(self): + return { + 'person': + lambda q: q.outerjoin(Person), + } + + def filter_map(self): + return self.make_filter_map( + ilike=['username'], + person=self.filter_ilike(Person.display_name)) + + def filter_config(self): + return self.make_filter_config( + include_filter_username=True, + filter_type_username='lk', + include_filter_person=True, + filter_type_person='lk') + + def sort_map(self): + return self.make_sort_map( + 'username', + person=self.sorter(Person.display_name)) + + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.username, + g.person, + ], + readonly=True) + if self.request.has_perm('users.read'): + g.viewable = True + g.view_route_name = 'user.read' + if self.request.has_perm('users.update'): + g.editable = True + g.edit_route_name = 'user.update' + if self.request.has_perm('users.delete'): + g.deletable = True + g.delete_route_name = 'user.delete' + return g class UserCrud(CrudView): @@ -73,7 +122,7 @@ class UserCrud(CrudView): def includeme(config): config.add_route('users', '/users') - config.add_view(users.UsersGrid, route_name='users', + config.add_view(UsersGrid, route_name='users', renderer='/users/index.mako', permission='users.list') diff --git a/rattail/pyramid/views/vendors.py b/rattail/pyramid/views/vendors.py index 17584f56..3b24b66b 100644 --- a/rattail/pyramid/views/vendors.py +++ b/rattail/pyramid/views/vendors.py @@ -63,8 +63,8 @@ class VendorsGrid(SearchableAlchemyGridView): ], readonly=True) if self.request.has_perm('vendors.read'): - g.clickable = True - g.click_route_name = 'vendor.read' + g.viewable = True + g.view_route_name = 'vendor.read' if self.request.has_perm('vendors.update'): g.editable = True g.edit_route_name = 'vendor.update' diff --git a/tests/views/test_departments.py b/tests/views/test_departments.py index d64a0809..3b414619 100644 --- a/tests/views/test_departments.py +++ b/tests/views/test_departments.py @@ -36,8 +36,8 @@ class DepartmentsGridTests(TestCase): view.request.has_perm = Mock(return_value=True) view.make_grid = Mock() g = view.grid() - self.assertTrue(g.clickable) - self.assertEqual(g.click_route_name, 'department.read') + self.assertTrue(g.viewable) + self.assertEqual(g.view_route_name, 'department.read') self.assertTrue(g.editable) self.assertEqual(g.edit_route_name, 'department.update') self.assertTrue(g.deletable) @@ -47,13 +47,13 @@ class DepartmentsGridTests(TestCase): view = self.view() view.request.has_perm = Mock(return_value=False) grid = Mock( - clickable=False, click_route_name=None, + viewable=False, view_route_name=None, editable=False, edit_route_name=None, deletable=False, delete_route_name=None) view.make_grid = Mock(return_value=grid) g = view.grid() - self.assertFalse(g.clickable) - self.assertEqual(g.click_route_name, None) + self.assertFalse(g.viewable) + self.assertEqual(g.view_route_name, None) self.assertFalse(g.editable) self.assertEqual(g.edit_route_name, None) self.assertFalse(g.deletable)