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:
parent
134613aadd
commit
b7fdd1f797
10
MANIFEST.in
10
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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
31
rattail/pyramid/static/__init__.py
Normal file
31
rattail/pyramid/static/__init__.py
Normal 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')
|
164
rattail/pyramid/static/css/grids.css
Normal file
164
rattail/pyramid/static/css/grids.css
Normal 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;
|
||||
}
|
BIN
rattail/pyramid/static/img/delete.png
Normal file
BIN
rattail/pyramid/static/img/delete.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 641 B |
BIN
rattail/pyramid/static/img/edit.png
Normal file
BIN
rattail/pyramid/static/img/edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 533 B |
BIN
rattail/pyramid/static/img/view.png
Normal file
BIN
rattail/pyramid/static/img/view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 616 B |
3
rattail/pyramid/templates/forms/field_autocomplete.mako
Normal file
3
rattail/pyramid/templates/forms/field_autocomplete.mako
Normal 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)}
|
63
rattail/pyramid/templates/grids/grid.mako
Normal file
63
rattail/pyramid/templates/grids/grid.mako
Normal 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> </th>
|
||||
% endif
|
||||
% if grid.editable:
|
||||
<th> </th>
|
||||
% endif
|
||||
% if grid.deletable:
|
||||
<th> </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)}"> </td>
|
||||
% endif
|
||||
% if grid.editable:
|
||||
<td class="edit" url="${grid.get_edit_url(row)}"> </td>
|
||||
% endif
|
||||
% if grid.deletable:
|
||||
<td class="delete" url="${grid.get_delete_url(row)}"> </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
|
||||
${grid.page_links()}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
218
rattail/pyramid/views/roles.py
Normal file
218
rattail/pyramid/views/roles.py
Normal 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')
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue