Replaced Grid.clickable with .viewable.

Clickable grid rows seemed to be more irritating than useful.  Now a view icon
is shown instead.
This commit is contained in:
Lance Edgar 2013-07-22 21:32:54 -07:00
parent 134613aadd
commit b7fdd1f797
32 changed files with 627 additions and 74 deletions

View file

@ -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

View file

@ -32,5 +32,6 @@ from edbob.pyramid import Session
def includeme(config): def includeme(config):
config.include('rattail.pyramid.static')
config.include('rattail.pyramid.subscribers') config.include('rattail.pyramid.subscribers')
config.include('rattail.pyramid.views') config.include('rattail.pyramid.views')

View file

@ -32,13 +32,12 @@ from webhelpers.html import tags
import formalchemy import formalchemy
from edbob.pyramid.forms import pretty_datetime from edbob.pyramid.forms import pretty_datetime
from edbob.pyramid.forms.formalchemy.renderers import ( from edbob.pyramid.forms.formalchemy.renderers import YesNoFieldRenderer
AutocompleteFieldRenderer, YesNoFieldRenderer)
import rattail import rattail
from rattail.gpc import GPC from rattail.gpc import GPC
from .common import EnumFieldRenderer from .common import AutocompleteFieldRenderer, EnumFieldRenderer
from .products import ProductFieldRenderer from .products import ProductFieldRenderer
from .users import UserFieldRenderer from .users import UserFieldRenderer

View file

@ -29,7 +29,33 @@
from formalchemy.fields import SelectFieldRenderer 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): def EnumFieldRenderer(enum):

View file

@ -53,7 +53,6 @@ class AlchemyGrid(Grid):
self._formalchemy_grid = formalchemy.Grid( self._formalchemy_grid = formalchemy.Grid(
cls, instances, session=Session(), request=request) cls, instances, session=Session(), request=request)
self._formalchemy_grid.prettify = prettify self._formalchemy_grid.prettify = prettify
self.noclick_fields = []
def __delattr__(self, attr): def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr) delattr(self._formalchemy_grid, attr)
@ -63,16 +62,11 @@ class AlchemyGrid(Grid):
def cell_class(self, field): def cell_class(self, field):
classes = [field.name] classes = [field.name]
if field.name in self.noclick_fields:
classes.append('noclick')
return ' '.join(classes) return ' '.join(classes)
def checkbox(self, row): def checkbox(self, row):
return tags.checkbox('check-'+row.uuid) return tags.checkbox('check-'+row.uuid)
def click_route_kwargs(self, row):
return {'uuid': row.uuid}
def column_header(self, field): def column_header(self, field):
class_ = None class_ = None
label = field.label() label = field.label()
@ -84,6 +78,9 @@ class AlchemyGrid(Grid):
return HTML.tag('th', class_=class_, field=field.key, return HTML.tag('th', class_=class_, field=field.key,
title=self.column_titles.get(field.key), c=label) 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): def edit_route_kwargs(self, row):
return {'uuid': row.uuid} return {'uuid': row.uuid}

View file

@ -46,19 +46,18 @@ class Grid(Object):
full = False full = False
hoverable = True hoverable = True
clickable = False
checkboxes = False checkboxes = False
editable = False
deletable = False
partial_only = False partial_only = False
click_route_name = None viewable = False
click_route_kwargs = None view_route_name = None
view_route_kwargs = None
editable = False
edit_route_name = None edit_route_name = None
edit_route_kwargs = None edit_route_kwargs = None
deletable = False
delete_route_name = None delete_route_name = None
delete_route_kwargs = None delete_route_kwargs = None
@ -82,22 +81,20 @@ class Grid(Object):
classes = ['grid'] classes = ['grid']
if self.full: if self.full:
classes.append('full') classes.append('full')
if self.clickable:
classes.append('clickable')
if self.hoverable: if self.hoverable:
classes.append('hoverable') classes.append('hoverable')
return format_attrs( return format_attrs(
class_=' '.join(classes), class_=' '.join(classes),
url=self.request.current_route_url()) url=self.request.current_route_url())
def get_delete_url(self, row): def get_view_url(self, row):
kwargs = {} kwargs = {}
if self.delete_route_kwargs: if self.view_route_kwargs:
if callable(self.delete_route_kwargs): if callable(self.view_route_kwargs):
kwargs = self.delete_route_kwargs(row) kwargs = self.view_route_kwargs(row)
else: else:
kwargs = self.delete_route_kwargs kwargs = self.view_route_kwargs
return self.request.route_url(self.delete_route_name, **kwargs) return self.request.route_url(self.view_route_name, **kwargs)
def get_edit_url(self, row): def get_edit_url(self, row):
kwargs = {} kwargs = {}
@ -108,16 +105,17 @@ class Grid(Object):
kwargs = self.edit_route_kwargs kwargs = self.edit_route_kwargs
return self.request.route_url(self.edit_route_name, **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): def get_row_attrs(self, row, i):
attrs = self.row_attrs(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) return format_attrs(**attrs)
def iter_fields(self): def iter_fields(self):

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.static`` -- Static Assets
"""
def includeme(config):
config.add_static_view('rattail', 'rattail.pyramid:static')

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

View file

@ -0,0 +1,3 @@
<%namespace file="/autocomplete.mako" import="autocomplete" />
${autocomplete(field_name, service_url, field_value, field_display, width=width, selected=selected, cleared=cleared)}

View file

@ -0,0 +1,63 @@
<div ${grid.div_attrs()}>
<table>
<thead>
<tr>
% if grid.checkboxes:
<th class="checkbox">${h.checkbox('check-all')}</th>
% endif
% for field in grid.iter_fields():
${grid.column_header(field)}
% endfor
% for col in grid.extra_columns:
<th>${col.label}</td>
% endfor
% if grid.viewable:
<th>&nbsp;</th>
% endif
% if grid.editable:
<th>&nbsp;</th>
% endif
% if grid.deletable:
<th>&nbsp;</th>
% endif
</tr>
</thead>
<tbody>
% for i, row in enumerate(grid.iter_rows(), 1):
<tr ${grid.get_row_attrs(row, i)}>
% if grid.checkboxes:
<td class="checkbox">${grid.checkbox(row)}</td>
% endif
% for field in grid.iter_fields():
<td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
% endfor
% for col in grid.extra_columns:
<td class="${col.name}">${col.callback(row)}</td>
% endfor
% if grid.viewable:
<td class="view" url="${grid.get_view_url(row)}">&nbsp;</td>
% endif
% if grid.editable:
<td class="edit" url="${grid.get_edit_url(row)}">&nbsp;</td>
% endif
% if grid.deletable:
<td class="delete" url="${grid.get_delete_url(row)}">&nbsp;</td>
% endif
</tr>
% endfor
</tbody>
</table>
% if grid.pager:
<div class="pager">
<p class="showing">
showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
(page ${grid.pager.page} of ${grid.pager.page_count})
</p>
<p class="page-links">
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
per page&nbsp;
${grid.page_links()}
</p>
</div>
% endif
</div>

View file

@ -41,6 +41,7 @@ def includeme(config):
config.include('rattail.pyramid.views.employees') config.include('rattail.pyramid.views.employees')
config.include('rattail.pyramid.views.labels') config.include('rattail.pyramid.views.labels')
config.include('rattail.pyramid.views.products') config.include('rattail.pyramid.views.products')
config.include('rattail.pyramid.views.roles')
config.include('rattail.pyramid.views.stores') config.include('rattail.pyramid.views.stores')
config.include('rattail.pyramid.views.subdepartments') config.include('rattail.pyramid.views.subdepartments')
config.include('rattail.pyramid.views.vendors') config.include('rattail.pyramid.views.vendors')

View file

@ -101,8 +101,8 @@ class BatchesGrid(SearchableAlchemyGridView):
return tags.link_to("View Rows", self.request.route_url( return tags.link_to("View Rows", self.request.route_url(
'batch.rows', uuid=row.uuid)) 'batch.rows', uuid=row.uuid))
g.add_column('rows', "", rows) g.add_column('rows', "", rows)
g.clickable = True g.viewable = True
g.click_route_name = 'batch.read' g.view_route_name = 'batch.read'
if self.request.has_perm('batches.update'): if self.request.has_perm('batches.update'):
g.editable = True g.editable = True
g.edit_route_name = 'batch.update' g.edit_route_name = 'batch.update'

View file

@ -97,9 +97,9 @@ def BatchRowsGrid(request):
route_kwargs = lambda x: {'batch_uuid': x.batch.uuid, 'uuid': x.uuid} route_kwargs = lambda x: {'batch_uuid': x.batch.uuid, 'uuid': x.uuid}
if self.request.has_perm('batch_rows.read'): if self.request.has_perm('batch_rows.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'batch_row.read' g.view_route_name = 'batch_row.read'
g.click_route_kwargs = route_kwargs g.view_route_kwargs = route_kwargs
if self.request.has_perm('batch_rows.update'): if self.request.has_perm('batch_rows.update'):
g.editable = True g.editable = True

View file

@ -57,8 +57,8 @@ class BrandsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('brands.read'): if self.request.has_perm('brands.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'brand.read' g.view_route_name = 'brand.read'
if self.request.has_perm('brands.update'): if self.request.has_perm('brands.update'):
g.editable = True g.editable = True
g.edit_route_name = 'brand.update' g.edit_route_name = 'brand.update'

View file

@ -57,8 +57,8 @@ class CategoriesGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('categories.read'): if self.request.has_perm('categories.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'category.read' g.view_route_name = 'category.read'
if self.request.has_perm('categories.update'): if self.request.has_perm('categories.update'):
g.editable = True g.editable = True
g.edit_route_name = 'category.update' g.edit_route_name = 'category.update'

View file

@ -58,8 +58,8 @@ class CustomerGroupsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('customer_groups.read'): if self.request.has_perm('customer_groups.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'customer_group.read' g.view_route_name = 'customer_group.read'
if self.request.has_perm('customer_groups.update'): if self.request.has_perm('customer_groups.update'):
g.editable = True g.editable = True
g.edit_route_name = 'customer_group.update' g.edit_route_name = 'customer_group.update'

View file

@ -46,7 +46,6 @@ class CustomersGrid(SearchableAlchemyGridView):
mapped_class = Customer mapped_class = Customer
config_prefix = 'customers' config_prefix = 'customers'
sort = 'name' sort = 'name'
clickable = True
def join_map(self): def join_map(self):
return { return {
@ -93,8 +92,8 @@ class CustomersGrid(SearchableAlchemyGridView):
readonly=True) readonly=True)
if self.request.has_perm('customers.read'): if self.request.has_perm('customers.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'customer.read' g.view_route_name = 'customer.read'
if self.request.has_perm('customers.update'): if self.request.has_perm('customers.update'):
g.editable = True g.editable = True
g.edit_route_name = 'customer.update' g.edit_route_name = 'customer.update'

View file

@ -59,8 +59,8 @@ class DepartmentsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('departments.read'): if self.request.has_perm('departments.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'department.read' g.view_route_name = 'department.read'
if self.request.has_perm('departments.update'): if self.request.has_perm('departments.update'):
g.editable = True g.editable = True
g.edit_route_name = 'department.update' g.edit_route_name = 'department.update'

View file

@ -117,8 +117,8 @@ class EmployeesGrid(SearchableAlchemyGridView):
del g.status del g.status
if self.request.has_perm('employees.read'): if self.request.has_perm('employees.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'employee.read' g.view_route_name = 'employee.read'
if self.request.has_perm('employees.update'): if self.request.has_perm('employees.update'):
g.editable = True g.editable = True
g.edit_route_name = 'employee.update' g.edit_route_name = 'employee.update'

View file

@ -42,7 +42,6 @@ class GridView(View):
full = False full = False
checkboxes = False checkboxes = False
clickable = False
deletable = False deletable = False
partial_only = False partial_only = False
@ -50,7 +49,6 @@ class GridView(View):
def update_grid_kwargs(self, kwargs): def update_grid_kwargs(self, kwargs):
kwargs.setdefault('full', self.full) kwargs.setdefault('full', self.full)
kwargs.setdefault('checkboxes', self.checkboxes) kwargs.setdefault('checkboxes', self.checkboxes)
kwargs.setdefault('clickable', self.clickable)
kwargs.setdefault('deletable', self.deletable) kwargs.setdefault('deletable', self.deletable)
kwargs.setdefault('partial_only', self.partial_only) kwargs.setdefault('partial_only', self.partial_only)

View file

@ -69,8 +69,8 @@ class ProfilesGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('label_profiles.read'): if self.request.has_perm('label_profiles.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'label_profile.read' g.view_route_name = 'label_profile.read'
if self.request.has_perm('label_profiles.update'): if self.request.has_perm('label_profiles.update'):
g.editable = True g.editable = True
g.edit_route_name = 'label_profile.update' g.edit_route_name = 'label_profile.update'

View file

@ -88,8 +88,8 @@ class PeopleGrid(SearchableAlchemyGridView):
readonly=True) readonly=True)
if self.request.has_perm('people.read'): if self.request.has_perm('people.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'person.read' g.view_route_name = 'person.read'
if self.request.has_perm('people.update'): if self.request.has_perm('people.update'):
g.editable = True g.editable = True
g.edit_route_name = 'person.update' g.edit_route_name = 'person.update'

View file

@ -176,8 +176,8 @@ class ProductsGrid(SearchableAlchemyGridView):
readonly=True) readonly=True)
if self.request.has_perm('products.read'): if self.request.has_perm('products.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'product.read' g.view_route_name = 'product.read'
if self.request.has_perm('products.update'): if self.request.has_perm('products.update'):
g.editable = True g.editable = True
g.edit_route_name = 'product.update' g.edit_route_name = 'product.update'

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``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')

View file

@ -79,8 +79,8 @@ class StoresGrid(SearchableAlchemyGridView):
g.email.label("Email Address"), g.email.label("Email Address"),
], ],
readonly=True) readonly=True)
g.clickable = True g.viewable = True
g.click_route_name = 'store.read' g.view_route_name = 'store.read'
if self.request.has_perm('stores.update'): if self.request.has_perm('stores.update'):
g.editable = True g.editable = True
g.edit_route_name = 'store.update' g.edit_route_name = 'store.update'

View file

@ -58,8 +58,8 @@ class SubdepartmentsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('subdepartments.read'): if self.request.has_perm('subdepartments.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'subdepartment.read' g.view_route_name = 'subdepartment.read'
if self.request.has_perm('subdepartments.update'): if self.request.has_perm('subdepartments.update'):
g.editable = True g.editable = True
g.edit_route_name = 'subdepartment.update' g.edit_route_name = 'subdepartment.update'

View file

@ -30,9 +30,58 @@ import formalchemy
from edbob.pyramid.views import users 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.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): class UserCrud(CrudView):
@ -73,7 +122,7 @@ class UserCrud(CrudView):
def includeme(config): def includeme(config):
config.add_route('users', '/users') config.add_route('users', '/users')
config.add_view(users.UsersGrid, route_name='users', config.add_view(UsersGrid, route_name='users',
renderer='/users/index.mako', renderer='/users/index.mako',
permission='users.list') permission='users.list')

View file

@ -63,8 +63,8 @@ class VendorsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('vendors.read'): if self.request.has_perm('vendors.read'):
g.clickable = True g.viewable = True
g.click_route_name = 'vendor.read' g.view_route_name = 'vendor.read'
if self.request.has_perm('vendors.update'): if self.request.has_perm('vendors.update'):
g.editable = True g.editable = True
g.edit_route_name = 'vendor.update' g.edit_route_name = 'vendor.update'

View file

@ -36,8 +36,8 @@ class DepartmentsGridTests(TestCase):
view.request.has_perm = Mock(return_value=True) view.request.has_perm = Mock(return_value=True)
view.make_grid = Mock() view.make_grid = Mock()
g = view.grid() g = view.grid()
self.assertTrue(g.clickable) self.assertTrue(g.viewable)
self.assertEqual(g.click_route_name, 'department.read') self.assertEqual(g.view_route_name, 'department.read')
self.assertTrue(g.editable) self.assertTrue(g.editable)
self.assertEqual(g.edit_route_name, 'department.update') self.assertEqual(g.edit_route_name, 'department.update')
self.assertTrue(g.deletable) self.assertTrue(g.deletable)
@ -47,13 +47,13 @@ class DepartmentsGridTests(TestCase):
view = self.view() view = self.view()
view.request.has_perm = Mock(return_value=False) view.request.has_perm = Mock(return_value=False)
grid = Mock( grid = Mock(
clickable=False, click_route_name=None, viewable=False, view_route_name=None,
editable=False, edit_route_name=None, editable=False, edit_route_name=None,
deletable=False, delete_route_name=None) deletable=False, delete_route_name=None)
view.make_grid = Mock(return_value=grid) view.make_grid = Mock(return_value=grid)
g = view.grid() g = view.grid()
self.assertFalse(g.clickable) self.assertFalse(g.viewable)
self.assertEqual(g.click_route_name, None) self.assertEqual(g.view_route_name, None)
self.assertFalse(g.editable) self.assertFalse(g.editable)
self.assertEqual(g.edit_route_name, None) self.assertEqual(g.edit_route_name, None)
self.assertFalse(g.deletable) self.assertFalse(g.deletable)