Final grid refactor; we now have just 'grids' :)

this also removes some old UI stuff for the first attempt at continuum
versioning..among other cruft
This commit is contained in:
Lance Edgar 2017-07-14 20:30:00 -05:00
parent 52c7f485ab
commit c57e2e17cc
61 changed files with 179 additions and 1590 deletions

6
docs/api/grids.rst Normal file
View file

@ -0,0 +1,6 @@
``tailbone.grids``
==================
.. automodule:: tailbone.grids
:members:

View file

@ -1,10 +0,0 @@
.. -*- coding: utf-8 -*-
``tailbone.newgrids``
=====================
.. automodule:: tailbone.newgrids
:members:
.. automodule:: tailbone.newgrids.alchemy
:members:

View file

@ -67,7 +67,7 @@ override when defining your subclass.
.. attribute:: MasterView.grid_factory
Factory callable to be used when creating new grid instances; defaults to
:class:`tailbone.newgrids.alchemy.AlchemyGrid`.
:class:`tailbone.grids.Grid`.
.. Methods to Override
.. -------------------

View file

@ -22,7 +22,7 @@ Package API:
.. toctree::
:maxdepth: 1
api/newgrids
api/grids
api/subscribers
api/views/batch
api/views/master

View file

@ -26,8 +26,6 @@ Grids and Friends
from __future__ import unicode_literals, absolute_import
from . import filters
from .core import Grid, GridAction
from .mobile import MobileGrid
# TODO
from tailbone.newgrids import filters

View file

@ -26,23 +26,23 @@ Core Grid Classes
from __future__ import unicode_literals, absolute_import
import urllib
import six
import sqlalchemy as sa
from sqlalchemy import orm
from rattail.db import api
from rattail.db.types import GPCType
from rattail.util import pretty_boolean, pretty_quantity
from rattail.util import pretty_boolean, pretty_quantity, prettify
import webhelpers2_grid
from pyramid.renderers import render
from webhelpers2.html import HTML, tags
from paginate_sqlalchemy import SqlalchemyOrmPage
from . import filters as gridfilters
from tailbone.db import Session
from tailbone import newgrids
from tailbone.newgrids import GridAction
from tailbone.newgrids.alchemy import URLMaker
from tailbone.util import raw_datetime
@ -301,7 +301,7 @@ class Grid(object):
def actions_column_format(self, column_number, row_number, item):
return HTML.td(self.render_actions(item, row_number), class_='actions')
def render_grid(self, template='/grids3/grid.mako', **kwargs):
def render_grid(self, template='/grids/grid.mako', **kwargs):
context = kwargs
context['grid'] = self
grid_class = ''
@ -320,7 +320,7 @@ class Grid(object):
if callable(self.default_filters):
return self.default_filters()
return self.default_filters
filters = newgrids.filters.GridFilterSet()
filters = gridfilters.GridFilterSet()
if self.model_class:
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
@ -344,22 +344,22 @@ class Grid(object):
"""
factory = kwargs.pop('factory', None)
if not factory:
factory = newgrids.filters.AlchemyGridFilter
factory = gridfilters.AlchemyGridFilter
if isinstance(column.type, sa.String):
factory = newgrids.filters.AlchemyStringFilter
factory = gridfilters.AlchemyStringFilter
elif isinstance(column.type, sa.Numeric):
factory = newgrids.filters.AlchemyNumericFilter
factory = gridfilters.AlchemyNumericFilter
elif isinstance(column.type, sa.Integer):
factory = newgrids.filters.AlchemyNumericFilter
factory = gridfilters.AlchemyNumericFilter
elif isinstance(column.type, sa.Boolean):
# TODO: check column for nullable here?
factory = newgrids.filters.AlchemyNullableBooleanFilter
factory = gridfilters.AlchemyNullableBooleanFilter
elif isinstance(column.type, sa.Date):
factory = newgrids.filters.AlchemyDateFilter
factory = gridfilters.AlchemyDateFilter
elif isinstance(column.type, sa.DateTime):
factory = newgrids.filters.AlchemyDateTimeFilter
factory = gridfilters.AlchemyDateTimeFilter
elif isinstance(column.type, GPCType):
factory = newgrids.filters.AlchemyGPCFilter
factory = gridfilters.AlchemyGPCFilter
return factory(key, column=column, config=self.request.rattail_config, **kwargs)
def iter_filters(self):
@ -799,7 +799,7 @@ class Grid(object):
data = self.pager
return data
def render_complete(self, template='/newgrids/complete.mako', **kwargs):
def render_complete(self, template='/grids/complete.mako', **kwargs):
"""
Render the complete grid, including filters.
"""
@ -808,7 +808,7 @@ class Grid(object):
context.setdefault('allow_save_defaults', True)
return render(template, context)
def render_filters(self, template='/newgrids/filters.mako', **kwargs):
def render_filters(self, template='/grids/filters.mako', **kwargs):
"""
Render the filters to a Unicode string, using the specified template.
Additional kwargs are passed along as context to the template.
@ -821,31 +821,13 @@ class Grid(object):
data['{}.verb'.format(filtr.key)] = filtr.verb
data[filtr.key] = filtr.value
form = newgrids.filters.GridFiltersForm(self.request, self.filters, defaults=data)
form = gridfilters.GridFiltersForm(self.request, self.filters, defaults=data)
kwargs['request'] = self.request
kwargs['grid'] = self
kwargs['form'] = newgrids.filters.GridFiltersFormRenderer(form)
kwargs['form'] = gridfilters.GridFiltersFormRenderer(form)
return render(template, kwargs)
# def get_div_attrs(self):
# """
# Returns a properly-formatted set of attributes which will be applied to
# the parent ``<div>`` element which contains the grid, when the grid is
# rendered.
# """
# classes = ['newgrid']
# if self.width == 'full':
# classes.append('full')
# if self.checkboxes:
# classes.append('selectable')
# attrs = {'class_': ' '.join(classes),
# 'data-url': self.request.current_route_url(_query=None),
# 'data-permalink': self.request.current_route_url()}
# if self.delete_speedbump:
# attrs['data-delete-speedbump'] = 'true'
# return attrs
def render_actions(self, row, i):
"""
Returns the rendered contents of the 'actions' column for a given row.
@ -978,3 +960,42 @@ class CustomWebhelpersGrid(webhelpers2_grid.Grid):
value = tags.link_to(value, url)
class_name = 'c{} {}'.format(column_number, column_name)
return HTML.tag('td', value, class_=class_name)
class GridAction(object):
"""
Represents an action available to a grid. This is used to construct the
'actions' column when rendering the grid.
"""
def __init__(self, key, label=None, url='#', icon=None, target=None):
self.key = key
self.label = label or prettify(key)
self.icon = icon
self.url = url
self.target = target
def get_url(self, row, i):
"""
Returns an action URL for the given row.
"""
if callable(self.url):
return self.url(row, i)
return self.url
class URLMaker(object):
"""
URL constructor for use with SQLAlchemy grid pagers. Logic for this was
basically copied from the old `webhelpers.paginate` module
"""
def __init__(self, request):
self.request = request
def __call__(self, page):
params = self.request.GET.copy()
params["page"] = page
params["partial"] = "1"
qs = urllib.urlencode(params, True)
return '{}?{}'.format(self.request.path, qs)

View file

@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import
from pyramid.renderers import render
from tailbone.grids3 import Grid
from .core import Grid
class MobileGrid(Grid):
@ -36,18 +36,18 @@ class MobileGrid(Grid):
Base class for all mobile grids
"""
def render_filters(self, template='/mobile/newgrids/filters_simple.mako', **kwargs):
def render_filters(self, template='/mobile/grids/filters_simple.mako', **kwargs):
context = kwargs
context['request'] = self.request
context['grid'] = self
return render(template, context)
def render_grid(self, template='/mobile/newgrids/grid.mako', **kwargs):
def render_grid(self, template='/mobile/grids/grid.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)
def render_complete(self, template='/mobile/newgrids/complete.mako', **kwargs):
def render_complete(self, template='/mobile/grids/complete.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)

View file

@ -1,32 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grids and Friends
"""
from __future__ import unicode_literals, absolute_import
from . import filters
from .core import Grid, GridColumn, GridAction
from .alchemy import AlchemyGrid
from .mobile import MobileGrid

View file

@ -1,226 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Grid Classes
"""
from __future__ import unicode_literals, absolute_import
import urllib
import logging
import sqlalchemy as sa
from sqlalchemy import orm
from rattail.db.types import GPCType
from rattail.util import prettify
import formalchemy as fa
import paginate
from paginate_sqlalchemy import SqlalchemyOrmPage
from tailbone.db import Session
from tailbone.newgrids import Grid, GridColumn, filters
log = logging.getLogger(__name__)
class URLMaker(object):
"""
URL constructor for use with SQLAlchemy grid pagers. Logic for this was
basically copied from the old `webhelpers.paginate` module
"""
def __init__(self, request):
self.request = request
def __call__(self, page):
params = self.request.GET.copy()
params["page"] = page
params["partial"] = "1"
qs = urllib.urlencode(params, True)
return '{}?{}'.format(self.request.path, qs)
class AlchemyGrid(Grid):
"""
Grid class for use with SQLAlchemy data models.
Note that this is partially just a wrapper around the FormAlchemy grid, and
that you may use this in much the same way, e.g.::
grid = AlchemyGrid(...)
grid.configure(
include=[
field1,
field2,
])
grid.append(field3)
del grid.field1
grid.field2.set(renderer=SomeFieldRenderer)
"""
def __init__(self, *args, **kwargs):
super(AlchemyGrid, self).__init__(*args, **kwargs)
fa_grid = fa.Grid(self.model_class, instances=self.data,
session=kwargs.get('session', Session()),
request=self.request)
fa_grid.prettify = prettify
self._fa_grid = fa_grid
def __delattr__(self, attr):
delattr(self._fa_grid, attr)
def __getattr__(self, attr):
return getattr(self._fa_grid, attr)
def default_filters(self):
"""
SQLAlchemy grids are able to provide a default set of filters based on
the column properties mapped to the model class.
"""
filtrs = filters.GridFilterSet()
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
filtrs[prop.key] = self.make_filter(prop.key, prop.columns[0])
return filtrs
def make_filter(self, key, column, **kwargs):
"""
Make a filter suitable for use with the given column.
"""
factory = kwargs.pop('factory', None)
if not factory:
factory = filters.AlchemyGridFilter
if isinstance(column.type, sa.String):
factory = filters.AlchemyStringFilter
elif isinstance(column.type, sa.Numeric):
factory = filters.AlchemyNumericFilter
elif isinstance(column.type, sa.Integer):
factory = filters.AlchemyNumericFilter
elif isinstance(column.type, sa.Boolean):
# TODO: check column for nullable here?
factory = filters.AlchemyNullableBooleanFilter
elif isinstance(column.type, sa.Date):
factory = filters.AlchemyDateFilter
elif isinstance(column.type, sa.DateTime):
factory = filters.AlchemyDateTimeFilter
elif isinstance(column.type, GPCType):
factory = filters.AlchemyGPCFilter
return factory(key, column=column, config=self.request.rattail_config, **kwargs)
def iter_filters(self):
"""
Iterate over the grid's complete set of filters.
"""
return self.filters.itervalues()
def filter_data(self, query):
"""
Filter and return the given data set, according to current settings.
"""
# This overrides the core version only slightly, in that it will only
# invoke a join if any particular filter(s) actually modifies the
# query. The main motivation for this is on the products page, where
# the tricky "vendor (any)" filter has a weird join and causes
# unpredictable results. Now we can skip the join for that unless the
# user actually enters some criteria for it.
for filtr in self.iter_active_filters():
original = query
query = filtr.filter(query)
if query is not original and filtr.key in self.joiners and filtr.key not in self.joined:
query = self.joiners[filtr.key](query)
self.joined.add(filtr.key)
return query
def make_sorters(self, sorters):
"""
Returns a mapping of sort options for the grid. Keyword args override
the defaults, which are obtained via the SQLAlchemy ORM.
"""
sorters, updates = {}, sorters
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
sorters[prop.key] = self.make_sorter(prop)
if updates:
sorters.update(updates)
return sorters
def make_sorter(self, model_property):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
class_ = getattr(model_property, 'class_', self.model_class)
column = getattr(class_, model_property.key)
return lambda q, d: q.order_by(getattr(column, d)())
def load_settings(self):
"""
When a SQLAlchemy grid loads its settings, it must update the
underlying FormAlchemy grid instance with the final (filtered/etc.)
data set.
"""
super(AlchemyGrid, self).load_settings()
self._fa_grid.rebind(self.make_visible_data(), session=Session(),
request=self.request)
def paginate_data(self, query):
"""
Paginate the given data set according to current settings, and return
the result.
"""
return SqlalchemyOrmPage(query,
items_per_page=self.pagesize,
page=self.page,
url_maker=URLMaker(self.request))
def iter_visible_columns(self):
"""
Returns an iterator for all currently-visible columns.
"""
for field in self._fa_grid.render_fields.itervalues():
if getattr(field, 'label_literal', False):
label = field.label_text
else:
label = field.label()
column = GridColumn(field.key, label=label,
title=getattr(field, '_column_header_title', None))
column.field = field
yield column
def iter_rows(self):
for row in self._fa_grid.rows:
self._fa_grid._set_active(row, orm.object_session(row))
yield row
def get_row_key(self, row):
mapper = orm.object_mapper(row)
assert len(mapper.primary_key) == 1
return getattr(row, mapper.primary_key[0].key)
def render_cell(self, row, column):
return column.field.render_readonly()

View file

@ -1,732 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Grid Classes
"""
from __future__ import unicode_literals, absolute_import
import six
from rattail.db.api import get_setting, save_setting
from rattail.util import prettify
from pyramid.renderers import render
from webhelpers2.html import HTML, tags
from tailbone.db import Session
from tailbone.newgrids import filters
class Grid(object):
"""
Core grid class. In sore need of documentation.
"""
def __init__(self, key, request, columns=[], data=[], main_actions=[], more_actions=[],
joiners={}, filterable=False, filters={},
sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc',
pageable=False, default_pagesize=20, default_page=1,
width='auto', checkboxes=False, row_attrs={}, cell_attrs={},
delete_speedbump=False, **kwargs):
self.key = key
self.request = request
self.columns = columns
self.data = data
self.main_actions = main_actions
self.more_actions = more_actions
self.joiners = joiners or {} # use new/different empty dict for each instance
self.delete_speedbump = delete_speedbump
# Set extra attributes first, in case other init logic depends on any
# of them (i.e. in subclasses).
for kw, val in kwargs.iteritems():
setattr(self, kw, val)
self.filterable = filterable
if self.filterable:
self.filters = self.make_filters(filters)
self.sortable = sortable
if self.sortable:
self.sorters = self.make_sorters(sorters)
self.default_sortkey = default_sortkey
self.default_sortdir = default_sortdir
self.pageable = pageable
if self.pageable:
self.default_pagesize = default_pagesize
self.default_page = default_page
self.width = width
self.checkboxes = checkboxes
self.row_attrs = row_attrs or {}
self.cell_attrs = cell_attrs
def get_default_filters(self):
"""
Returns the default set of filters provided by the grid.
"""
if hasattr(self, 'default_filters'):
if callable(self.default_filters):
return self.default_filters()
return self.default_filters
return filters.GridFilterSet()
def make_filters(self, filters=None):
"""
Returns an initial set of filters which will be available to the grid.
The grid itself may or may not provide some default filters, and the
``filters`` kwarg may contain additions and/or overrides.
"""
filters, updates = self.get_default_filters(), filters
if updates:
filters.update(updates)
return filters
def iter_filters(self):
"""
Iterate over all filters available to the grid.
"""
return six.itervalues(self.filters)
def iter_active_filters(self):
"""
Iterate over all *active* filters for the grid. Whether a filter is
active is determined by current grid settings.
"""
for filtr in self.iter_filters():
if filtr.active:
yield filtr
def has_active_filters(self):
"""
Returns boolean indicating whether the grid contains any *active*
filters, according to current settings.
"""
for filtr in self.iter_active_filters():
return True
return False
def make_sorters(self, sorters=None):
"""
Returns an initial set of sorters which will be available to the grid.
The grid itself may or may not provide some default sorters, and the
``sorters`` kwarg may contain additions and/or overrides.
"""
sorters, updates = {}, sorters
if updates:
sorters.update(updates)
return sorters
def make_sorter(self, key, foldcase=False):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting a data set comprised of dicts, on the given key.
"""
if foldcase:
keyfunc = lambda v: v[key].lower()
else:
keyfunc = lambda v: v[key]
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
def load_settings(self, store=True):
"""
Load current/effective settings for the grid, from the request query
string and/or session storage. If ``store`` is true, then once
settings have been fully read, they are stored in current session for
next time. Finally, various instance attributes of the grid and its
filters are updated in-place to reflect the settings; this is so code
needn't access the settings dict directly, but the more Pythonic
instance attributes.
"""
# Initial settings come from class defaults.
settings = {}
if self.sortable:
settings['sortkey'] = self.default_sortkey
settings['sortdir'] = self.default_sortdir
if self.pageable:
settings['pagesize'] = self.default_pagesize
settings['page'] = self.default_page
if self.filterable:
for filtr in self.iter_filters():
settings['filter.{0}.active'.format(filtr.key)] = filtr.default_active
settings['filter.{0}.verb'.format(filtr.key)] = filtr.default_verb
settings['filter.{0}.value'.format(filtr.key)] = filtr.default_value
# If user has default settings on file, apply those first.
if self.user_has_defaults():
self.apply_user_defaults(settings)
# If request contains instruction to reset to default filters, then we
# can skip the rest of the request/session checks.
if self.request.GET.get('reset-to-default-filters') == 'true':
pass
# If request has filter settings, grab those, then grab sort/pager
# settings from request or session.
elif self.filterable and self.request_has_settings('filter'):
self.update_filter_settings(settings, 'request')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
else:
self.update_sort_settings(settings, 'session')
self.update_page_settings(settings)
# If request has no filter settings but does have sort settings, grab
# those, then grab filter settings from session, then grab pager
# settings from request or session.
elif self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
self.update_filter_settings(settings, 'session')
self.update_page_settings(settings)
# NOTE: These next two are functionally equivalent, but are kept
# separate to maintain the narrative...
# If request has no filter/sort settings but does have pager settings,
# grab those, then grab filter/sort settings from session.
elif self.request_has_settings('page'):
self.update_page_settings(settings)
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
# If request has no settings, grab all from session.
elif self.session_has_settings():
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
self.update_page_settings(settings)
# If no settings were found in request or session, don't store result.
else:
store = False
# Maybe store settings for next time.
if store:
self.persist_settings(settings, 'session')
# If request contained instruction to save current settings as defaults
# for the current user, then do that.
if self.request.GET.get('save-current-filters-as-defaults') == 'true':
self.persist_settings(settings, 'defaults')
# Update ourself and our filters, to reflect settings.
if self.filterable:
for filtr in self.iter_filters():
filtr.active = settings['filter.{0}.active'.format(filtr.key)]
filtr.verb = settings['filter.{0}.verb'.format(filtr.key)]
filtr.value = settings['filter.{0}.value'.format(filtr.key)]
if self.sortable:
self.sortkey = settings['sortkey']
self.sortdir = settings['sortdir']
if self.pageable:
self.pagesize = settings['pagesize']
self.page = settings['page']
def user_has_defaults(self):
"""
Check to see if the current user has default settings on file for this grid.
"""
user = self.request.user
if not user:
return False
# NOTE: we used to leverage `self.session` here, but sometimes we might
# be showing a grid of data from another system...so always use
# Tailbone Session now, for the settings. hopefully that didn't break
# anything...
session = Session()
if user not in session:
user = session.merge(user)
# User defaults should have all or nothing, so just check one key.
key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key)
return get_setting(session, key) is not None
def apply_user_defaults(self, settings):
"""
Update the given settings dict with user defaults, if any exist.
"""
def merge(key, coerce=lambda v: v):
skey = 'tailbone.{0}.grid.{1}.{2}'.format(self.request.user.uuid, self.key, key)
value = get_setting(Session(), skey)
settings[key] = coerce(value)
if self.filterable:
for filtr in self.iter_filters():
merge('filter.{0}.active'.format(filtr.key), lambda v: v == 'true')
merge('filter.{0}.verb'.format(filtr.key))
merge('filter.{0}.value'.format(filtr.key))
if self.sortable:
merge('sortkey')
merge('sortdir')
if self.pageable:
merge('pagesize', int)
merge('page', int)
def request_has_settings(self, type_):
"""
Determine if the current request (GET query string) contains any
filter/sort settings for the grid.
"""
if type_ == 'filter':
for filtr in self.iter_filters():
if filtr.key in self.request.GET:
return True
if 'filter' in self.request.GET: # user may be applying empty filters
return True
elif type_ == 'sort':
for key in ['sortkey', 'sortdir']:
if key in self.request.GET:
return True
elif type_ == 'page':
for key in ['pagesize', 'page']:
if key in self.request.GET:
return True
return False
def session_has_settings(self):
"""
Determine if the current session contains any settings for the grid.
"""
# session should have all or nothing, so just check a few keys which
# should be guaranteed present if anything has been stashed
for key in ['page', 'sortkey']:
if 'grid.{}.{}'.format(self.key, key) in self.request.session:
return True
return False
def get_setting(self, source, settings, key, coerce=lambda v: v, default=None):
"""
Get the effective value for a particular setting, preferring ``source``
but falling back to existing ``settings`` and finally the ``default``.
"""
if source not in ('request', 'session'):
raise ValueError("Invalid source identifier: {0}".format(repr(source)))
# If source is query string, try that first.
if source == 'request':
value = self.request.GET.get(key)
if value is not None:
try:
value = coerce(value)
except ValueError:
pass
else:
return value
# Or, if source is session, try that first.
else:
value = self.request.session.get('grid.{0}.{1}'.format(self.key, key))
if value is not None:
return coerce(value)
# If source had nothing, try default/existing settings.
value = settings.get(key)
if value is not None:
try:
value = coerce(value)
except ValueError:
pass
else:
return value
# Okay then, default it is.
return default
def update_filter_settings(self, settings, source):
"""
Updates a settings dictionary according to filter settings data found
in either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.filterable:
return
for filtr in self.iter_filters():
prefix = 'filter.{0}'.format(filtr.key)
if source == 'request':
# consider filter active if query string contains a value for it
settings['{0}.active'.format(prefix)] = filtr.key in self.request.GET
settings['{0}.verb'.format(prefix)] = self.get_setting(
source, settings, '{0}.verb'.format(filtr.key), default='')
settings['{0}.value'.format(prefix)] = self.get_setting(
source, settings, filtr.key, default='')
else: # source = session
settings['{0}.active'.format(prefix)] = self.get_setting(
source, settings, '{0}.active'.format(prefix),
coerce=lambda v: unicode(v).lower() == 'true', default=False)
settings['{0}.verb'.format(prefix)] = self.get_setting(
source, settings, '{0}.verb'.format(prefix), default='')
settings['{0}.value'.format(prefix)] = self.get_setting(
source, settings, '{0}.value'.format(prefix), default='')
def update_sort_settings(self, settings, source):
"""
Updates a settings dictionary according to sort settings data found in
either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.sortable:
return
settings['sortkey'] = self.get_setting(source, settings, 'sortkey')
settings['sortdir'] = self.get_setting(source, settings, 'sortdir')
def update_page_settings(self, settings):
"""
Updates a settings dictionary according to pager settings data found in
either the GET query string, or session storage.
Note that due to how the actual pager functions, the effective settings
will often come from *both* the request and session. This is so that
e.g. the page size will remain constant (coming from the session) while
the user jumps between pages (which only provides the single setting).
:param settings: Dictionary of initial settings, which is to be updated.
"""
if not self.pageable:
return
pagesize = self.request.GET.get('pagesize')
if pagesize is not None:
if pagesize.isdigit():
settings['pagesize'] = int(pagesize)
else:
pagesize = self.request.session.get('grid.{0}.pagesize'.format(self.key))
if pagesize is not None:
settings['pagesize'] = pagesize
page = self.request.GET.get('page')
if page is not None:
if page.isdigit():
settings['page'] = page
else:
page = self.request.session.get('grid.{0}.page'.format(self.key))
if page is not None:
settings['page'] = page
def persist_settings(self, settings, to='session'):
"""
Persist the given settings in some way, as defined by ``func``.
"""
def persist(key, value=lambda k: settings[k]):
if to == 'defaults':
skey = 'tailbone.{0}.grid.{1}.{2}'.format(self.request.user.uuid, self.key, key)
save_setting(Session(), skey, value(key))
else: # to == session
skey = 'grid.{0}.{1}'.format(self.key, key)
self.request.session[skey] = value(key)
if self.filterable:
for filtr in self.iter_filters():
persist('filter.{0}.active'.format(filtr.key), value=lambda k: unicode(settings[k]).lower())
persist('filter.{0}.verb'.format(filtr.key))
persist('filter.{0}.value'.format(filtr.key))
if self.sortable:
persist('sortkey')
persist('sortdir')
if self.pageable:
persist('pagesize')
persist('page')
def filter_data(self, data):
"""
Filter and return the given data set, according to current settings.
"""
for filtr in self.iter_active_filters():
if filtr.key in self.joiners and filtr.key not in self.joined:
data = self.joiners[filtr.key](data)
self.joined.add(filtr.key)
data = filtr.filter(data)
return data
def sort_data(self, data):
"""
Sort the given query according to current settings, and return the result.
"""
# Cannot sort unless we know which column to sort by.
if not self.sortkey:
return data
# Cannot sort unless we have a sort function.
sortfunc = self.sorters.get(self.sortkey)
if not sortfunc:
return data
# We can provide a default sort direction though.
sortdir = getattr(self, 'sortdir', 'asc')
if self.sortkey in self.joiners and self.sortkey not in self.joined:
data = self.joiners[self.sortkey](data)
self.joined.add(self.sortkey)
return sortfunc(data, sortdir)
def paginate_data(self, data):
"""
Paginate the given data set according to current settings, and return
the result. Note that the default implementation does nothing.
"""
return data
def make_visible_data(self):
"""
Apply various settings to the raw data set, to produce a final data
set. This will page / sort / filter as necessary, according to the
grid's defaults and the current request etc.
"""
self.joined = set()
data = self.data
if self.filterable:
data = self.filter_data(data)
if self.sortable:
data = self.sort_data(data)
if self.pageable:
self.pager = self.paginate_data(data)
data = self.pager
return data
def render_complete(self, template='/newgrids/complete.mako', **kwargs):
"""
Render the complete grid, including filters.
"""
kwargs['grid'] = self
kwargs.setdefault('allow_save_defaults', True)
return render(template, kwargs)
def render_grid(self, template='/newgrids/grid.mako', **kwargs):
"""
Render the grid to a Unicode string, using the specified template.
Addition kwargs are passed along as context to the template.
"""
kwargs['grid'] = self
return render(template, kwargs)
def render_filters(self, template='/newgrids/filters.mako', **kwargs):
"""
Render the filters to a Unicode string, using the specified template.
Additional kwargs are passed along as context to the template.
"""
# Provide default data to filters form, so renderer can do some of the
# work for us.
data = {}
for filtr in self.iter_active_filters():
data['{0}.active'.format(filtr.key)] = filtr.active
data['{0}.verb'.format(filtr.key)] = filtr.verb
data[filtr.key] = filtr.value
form = filters.GridFiltersForm(self.request, self.filters, defaults=data)
kwargs['request'] = self.request
kwargs['grid'] = self
kwargs['form'] = filters.GridFiltersFormRenderer(form)
return render(template, kwargs)
def get_div_attrs(self):
"""
Returns a properly-formatted set of attributes which will be applied to
the parent ``<div>`` element which contains the grid, when the grid is
rendered.
"""
classes = ['newgrid']
if self.width == 'full':
classes.append('full')
if self.checkboxes:
classes.append('selectable')
attrs = {'class_': ' '.join(classes),
'data-url': self.request.current_route_url(_query=None),
'data-permalink': self.request.current_route_url()}
if self.delete_speedbump:
attrs['data-delete-speedbump'] = 'true'
return attrs
def iter_visible_columns(self):
"""
Returns an iterator for all currently-visible columns.
"""
return iter(self.columns)
def column_header(self, column):
"""
Render a header (``<th>`` element) for a grid column.
"""
kwargs = {'c': column.label}
if self.sortable and column.key in self.sorters:
if column.key == self.sortkey:
kwargs['class_'] = 'sortable sorted {0}'.format(self.sortdir)
else:
kwargs['class_'] = 'sortable'
kwargs['data-sortkey'] = column.key
kwargs['c'] = tags.link_to(column.label, '#', title=column.title)
elif column.title:
kwargs['c'] = HTML.tag('span', title=column.title, c=column.label)
kwargs['class_'] = '{} {}'.format(kwargs.get('class_', ''), column.key)
return HTML.tag('th', **kwargs)
@property
def show_actions_column(self):
"""
Whether or not an "Actions" column should be rendered for the grid.
"""
return bool(self.main_actions or self.more_actions)
def render_actions(self, row, i):
"""
Returns the rendered contents of the 'actions' column for a given row.
"""
main_actions = filter(None, [self.render_action(a, row, i) for a in self.main_actions])
more_actions = filter(None, [self.render_action(a, row, i) for a in self.more_actions])
if more_actions:
icon = HTML.tag('span', class_='ui-icon ui-icon-carat-1-e')
link = tags.link_to("More" + icon, '#', class_='more')
main_actions.append(link + HTML.tag('div', class_='more', c=more_actions))
return HTML.literal('').join(main_actions)
def render_action(self, action, row, i):
"""
Renders an action menu item (link) for the given row.
"""
url = action.get_url(row, i)
if url:
kwargs = {'class_': action.key, 'target': action.target}
if action.icon:
icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon))
return tags.link_to(icon + action.label, url, **kwargs)
return tags.link_to(action.label, url, **kwargs)
def iter_rows(self):
return self.make_visible_data()
def get_row_attrs(self, row, i):
"""
Returns a dict of HTML attributes which is to be applied to the row's
``<tr>`` element. Note that ``i`` will be a 1-based index value for
the row within its table. The meaning of ``row`` is basically not
defined; it depends on the type of data the grid deals with.
"""
if callable(self.row_attrs):
return self.row_attrs(row, i)
return self.row_attrs
# def get_row_class(self, row, i):
# class_ = self.default_row_class(row, i)
# if callable(self.extra_row_class):
# extra = self.extra_row_class(row, i)
# if extra:
# class_ = '{0} {1}'.format(class_, extra)
# return class_
def get_row_key(self, row):
raise NotImplementedError
def checkbox(self, row):
return True
def checked(self, row):
return False
def render_checkbox(self, row):
"""
Returns a boolean indicating whether ot not a checkbox should be
rendererd for the given row. Default implementation returns ``True``
in all cases.
"""
if not self.checkbox(row):
return ''
return tags.checkbox('checkbox-{0}-{1}'.format(self.key, self.get_row_key(row)),
checked=self.checked(row))
def get_cell_attrs(self, row, column):
"""
Returns a dictionary of HTML attributes which should be applied to the
``<td>`` element in which the given row and column "intersect".
"""
if callable(self.cell_attrs):
return self.cell_attrs(row, column)
return self.cell_attrs
def render_cell(self, row, column):
return column.render(row[column.key])
def get_pagesize_options(self):
# TODO: Make configurable or something...
return [5, 10, 20, 50, 100]
class GridColumn(object):
"""
Simple class to represent a column displayed within a grid table.
.. attribute:: key
Key for the column, within the context of the grid.
.. attribute:: label
Human-facing label for the column, i.e. displayed in the header.
"""
def __init__(self, key, label=None, title=None):
self.key = key
self.label = label or prettify(key)
self.title = title
def render(self, value):
"""
Render the given value, to be displayed within a grid cell.
"""
return unicode(value)
class GridAction(object):
"""
Represents an action available to a grid. This is used to construct the
'actions' column when rendering the grid.
"""
def __init__(self, key, label=None, url='#', icon=None, target=None):
self.key = key
self.label = label or prettify(key)
self.icon = icon
self.url = url
self.target = target
def get_url(self, row, i):
"""
Returns an action URL for the given row.
"""
if callable(self.url):
return self.url(row, i)
return self.url

View file

@ -1,54 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Mobile Grids
"""
from __future__ import unicode_literals, absolute_import
from pyramid.renderers import render
from webhelpers2.html import HTML
from tailbone.newgrids import filters, AlchemyGrid
class MobileGrid(AlchemyGrid):
"""
Base class for all mobile grids
"""
default_filters = filters.GridFilterSet()
def column_header(self, column):
kwargs = {'c': column.label}
return HTML.tag('th', **kwargs)
def render_filters(self, template='/mobile/newgrids/filters_simple.mako', **kwargs):
context = kwargs
context['request'] = self.request
context['grid'] = self
return render(template, context)
def render_complete(self, template='/mobile/newgrids/complete.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)

View file

@ -1,12 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/grid.mako" />
<%def name="title()">${batch_display_plural}</%def>
<%def name="context_menu_items()">
% if request.has_perm('{0}.create'.format(permission_prefix)):
<li>${h.link_to("Create a new {0}".format(batch_display), url('{0}.create'.format(route_prefix)))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,38 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako" />
<%def name="context_menu_items()"></%def>
<%def name="form()">
% if search:
${search.render()}
% else:
&nbsp;
% endif
</%def>
<%def name="tools()"></%def>
<div class="grid-wrapper">
<table class="grid-header">
<tr>
<td rowspan="2" class="form">
${self.form()}
</td>
<td class="context-menu">
<ul>
${self.context_menu_items()}
</ul>
</td>
</tr>
<tr>
<td class="tools">
${self.tools()}
</td>
</tr>
</table><!-- grid-header -->
${grid}
</div><!-- grid-wrapper -->

View file

@ -1,63 +1,20 @@
## -*- coding: utf-8 -*-
<div ${grid.div_attrs()}>
## -*- coding: utf-8; -*-
<div class="newgrid grid3 ${grid_class}">
<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>
${grid.make_webhelpers_grid()}
</table>
% if grid.pager:
% if grid.pageable and 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})
${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
% if grid.pager.page_count > 1:
${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
% endif
</p>
<p class="page-links">
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
${h.select('pagesize', grid.pager.items_per_page, grid.get_pagesize_options())}
per page&nbsp;
${grid.page_links()}
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
</p>
</div>
% endif

View file

@ -1,21 +0,0 @@
## -*- coding: utf-8; -*-
<div class="newgrid grid3 ${grid_class}">
<table>
${grid.make_webhelpers_grid()}
</table>
% if grid.pageable and grid.pager:
<div class="pager">
<p class="showing">
${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
% if grid.pager.page_count > 1:
${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
% endif
</p>
<p class="page-links">
${h.select('pagesize', grid.pager.items_per_page, grid.get_pagesize_options())}
per page&nbsp;
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
</p>
</div>
% endif
</div>

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,4 +1,4 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="head_tags()">
@ -22,9 +22,6 @@
<li>${h.link_to("Edit Printer Settings", url('labelprofiles.printer_settings', uuid=instance.uuid))}</li>
% endif
% endif
% if version_count is not Undefined and request.has_perm('labelprofile.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('labelprofile.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,50 +0,0 @@
## -*- coding: utf-8 -*-
<div ${h.render_attrs(**grid.get_div_attrs())}>
<table>
<thead>
<tr>
% if grid.checkboxes:
<th class="checkbox">${h.checkbox('check-all')}</th>
% endif
% for column in grid.iter_visible_columns():
${grid.column_header(column)}
% endfor
% if grid.show_actions_column:
<th class="actions">Actions</th>
% endif
</tr>
</thead>
<tbody>
% for i, row in enumerate(grid.iter_rows(), 1):
<tr ${h.render_attrs(**grid.get_row_attrs(row, i))}>
% if grid.checkboxes:
<td class="checkbox">${grid.render_checkbox(row)}</td>
% endif
% for column in grid.iter_visible_columns():
<td ${h.render_attrs(**grid.get_cell_attrs(row, column))}>${grid.render_cell(row, column)}</td>
% endfor
% if grid.show_actions_column:
<td class="actions">
${grid.render_actions(row, i)}
</td>
% endif
</tr>
% endfor
</tbody>
</table>
% if grid.pageable and grid.pager:
<div class="pager">
<p class="showing">
${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
% if grid.pager.page_count > 1:
${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
% endif
</p>
<p class="page-links">
${h.select('pagesize', grid.pager.items_per_page, grid.get_pagesize_options())}
per page&nbsp;
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
</p>
</div>
% endif
</div>

View file

@ -1,11 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/edit.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('product.versions.view'):
<li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -62,13 +62,6 @@
## rendering methods
##############################
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('instance.versions.view'):
<li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}</li>
% endif
</%def>
<%def name="render_main_fields(form)">
${render_field_readonly(form.fieldset.upc)}
${render_field_readonly(form.fieldset.brand)}

View file

@ -1,16 +1,9 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
<%inherit file="/master/edit.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_styles()">
${parent.extra_styles()}
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('role.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('role.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -6,13 +6,6 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('role.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('role.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}
<h2>Users</h2>

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,7 +1,7 @@
## -*- coding: utf-8; -*-
<%namespace name="base" file="tailbone:templates/base.mako" />
<%namespace file="/menu.mako" import="main_menu_items" />
<%namespace file="/newgrids/nav.mako" import="grid_index_nav" />
<%namespace file="/grids/nav.mako" import="grid_index_nav" />
<!DOCTYPE html>
<html lang="en">
<head>

View file

@ -1,11 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/edit.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,16 +1,9 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_styles()">
${parent.extra_styles()}
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/index.mako" />
${parent.body()}

View file

@ -1,3 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/versions/view.mako" />
${parent.body()}

View file

@ -1,15 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/grid.mako" />
<%def name="title()">${model_title} Change History</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to all {0}".format(model_title_plural), url(route_model_list))}</li>
<li>${h.link_to("Back to current {0}".format(model_title), url(route_model_view, uuid=model_instance.uuid))}</li>
</%def>
<%def name="form()">
<h2>Changes for ${model_title}:&nbsp; ${model_instance}</h2>
</%def>
${parent.body()}

View file

@ -1,103 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako" />
<%def name="title()">${model_title} Version Details</%def>
<%def name="head_tags()">
<style type="text/css">
td.oldvalue {
background-color: #fcc;
}
td.newvalue {
background-color: #cfc;
}
</style>
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to all {0}".format(model_title_plural), url(route_model_list))}</li>
<li>${h.link_to("Back to current {0}".format(model_title), url(route_model_view, uuid=model_instance.uuid))}</li>
<li>${h.link_to("Back to Version History", url('{0}.versions'.format(route_prefix), uuid=model_instance.uuid))}</li>
</%def>
<div class="form-wrapper">
<ul class="context-menu">
${self.context_menu_items()}
</ul>
<div class="form">
<div>
% if previous_transaction or next_transaction:
% if previous_transaction:
${h.link_to("<< older version", url('{0}.version'.format(route_prefix), uuid=model_instance.uuid, transaction_id=previous_transaction.id))}
% else:
<span>(oldest version)</span>
% endif
&nbsp; | &nbsp;
% if next_transaction:
${h.link_to("newer version >>", url('{0}.version'.format(route_prefix), uuid=model_instance.uuid, transaction_id=next_transaction.id))}
% else:
<span>(newest version)</span>
% endif
% else:
<span>(only version)</span>
% endif
</div>
<div class="fieldset">
<div class="field-wrapper">
<label>When:</label>
<div class="field">${h.pretty_datetime(request.rattail_config, transaction.issued_at)}</div>
</div>
<div class="field-wrapper">
<label>Who:</label>
<div class="field">${transaction.user or "(unknown / system)"}</div>
</div>
<div class="field-wrapper">
<label>Where:</label>
<div class="field">${transaction.remote_addr}</div>
</div>
% for ver in versions:
<div class="field-wrapper">
<label>What:</label>
<div class="field" style="font-weight: bold;">${ver.version_parent.__class__.__name__}:&nbsp; ${ver.version_parent}</div>
</div>
<div class="field-wrapper">
<label>Changes:</label>
<div class="field">
<div class="grid">
<table>
<thead>
<tr>
<th>Field</th>
<th>Old Value</th>
<th>New Value</th>
</tr>
</thead>
<tbody>
% for key in sorted(ver.changeset):
<tr>
<td>${key}</td>
<td class="oldvalue">${ver.changeset[key][0]}</td>
<td class="newvalue">${ver.changeset[key][1]}</td>
</tr>
% endfor
</tbody>
</table>
</div>
</div>
</div>
% endfor
</div>
</div>
</div>

View file

@ -22,9 +22,6 @@
################################################################################
"""
Base views for maintaining "new-style" batches.
.. note::
This is all still somewhat experimental.
"""
from __future__ import unicode_literals, absolute_import
@ -50,7 +47,7 @@ from pyramid.response import FileResponse
from pyramid_simpleform import Form
from webhelpers2.html import HTML, tags
from tailbone import forms, newgrids as grids
from tailbone import forms, grids
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.forms.renderers.batch import FileFieldRenderer

View file

@ -30,7 +30,7 @@ import six
from rattail.db import model
from tailbone import grids3 as grids
from tailbone import grids
from tailbone.views import MasterView2
from tailbone.views.batch import BatchMasterView, FileBatchMasterView
from tailbone.views.batch.core import MobileBatchStatusFilter

View file

@ -37,7 +37,7 @@ import formalchemy
from pyramid.response import FileResponse
from webhelpers2.html import literal
from tailbone import grids3 as grids
from tailbone import grids
from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer

View file

@ -30,7 +30,7 @@ import six
from rattail.db import model
from tailbone import grids3 as grids
from tailbone import grids
from tailbone.views import MasterView2 as MasterView, AutocompleteView

View file

@ -32,7 +32,7 @@ from rattail.db import model
import formalchemy as fa
from tailbone import forms, grids3 as grids
from tailbone import forms, grids
from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView, AutocompleteView

View file

@ -41,9 +41,8 @@ from pyramid import httpexceptions
from pyramid.renderers import get_renderer, render_to_response, render
from webhelpers2.html import HTML, tags
from tailbone import forms, newgrids as grids
from tailbone import forms, grids
from tailbone.views import View
from tailbone.newgrids import filters, AlchemyGrid, GridAction, MobileGrid
class MasterView(View):
@ -161,24 +160,6 @@ class MasterView(View):
grid.load_settings()
return grid
@classmethod
def get_mobile_grid_factory(cls):
"""
Must return a callable to be used when creating new mobile grid
instances. Instead of overriding this, you can set
:attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`.
"""
return getattr(cls, 'mobile_grid_factory', MobileGrid)
@classmethod
def get_mobile_row_grid_factory(cls):
"""
Must return a callable to be used when creating new mobile grid
instances. Instead of overriding this, you can set
:attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`.
"""
return getattr(cls, 'mobile_row_grid_factory', MobileGrid)
@classmethod
def get_mobile_grid_key(cls):
"""
@ -422,14 +403,6 @@ class MasterView(View):
grid.load_settings()
return grid
@classmethod
def get_version_grid_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
version grid instances.
"""
return getattr(cls, 'version_grid_factory', AlchemyGrid)
@classmethod
def get_version_grid_key(cls):
"""
@ -1254,23 +1227,6 @@ class MasterView(View):
# Grid Stuff
##############################
@classmethod
def get_grid_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
grid instances.
"""
return getattr(cls, 'grid_factory', AlchemyGrid)
@classmethod
def get_row_grid_factory(cls):
"""
Must return a callable to be used when creating new row grid instances.
Instead of overriding this, you can set :attr:`row_grid_factory`.
Default factory is :class:`AlchemyGrid`.
"""
return getattr(cls, 'row_grid_factory', AlchemyGrid)
@classmethod
def get_grid_key(cls):
"""
@ -1385,7 +1341,7 @@ class MasterView(View):
if url is None:
route = '{}.{}'.format(self.get_route_prefix(), key)
url = lambda r, i: self.request.route_url(route, **self.get_action_route_kwargs(r))
return GridAction(key, url=url, **kwargs)
return grids.GridAction(key, url=url, **kwargs)
def get_action_route_kwargs(self, row):
"""
@ -1424,20 +1380,6 @@ class MasterView(View):
def _preconfigure_grid(self, grid):
pass
def configure_grid(self, grid):
"""
Configure the grid, customizing as necessary. Subclasses are
encouraged to override this method.
As a bare minimum, the logic for this method must at some point invoke
the ``configure()`` method on the grid instance. The default
implementation does exactly (and only) this, passing no arguments.
This requirement is a result of using FormAlchemy under the hood, and
it is in fact a call to :meth:`formalchemy:formalchemy.tables.Grid.configure()`.
"""
if hasattr(grid, 'configure'):
grid.configure()
def get_data(self, session=None):
"""
Generate the base data set for the grid. This typically will be a

View file

@ -26,7 +26,9 @@ Master View
from __future__ import unicode_literals, absolute_import
from tailbone import grids3 as grids
import sqlalchemy_continuum as continuum
from tailbone import grids
from tailbone.views import MasterView
@ -64,6 +66,14 @@ class MasterView2(MasterView):
"""
return getattr(cls, 'row_grid_factory', grids.Grid)
@classmethod
def get_version_grid_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
version grid instances.
"""
return getattr(cls, 'version_grid_factory', grids.Grid)
@classmethod
def get_mobile_grid_factory(cls):
"""
@ -82,6 +92,18 @@ class MasterView2(MasterView):
"""
return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid)
def get_effective_data(self, session=None, **kwargs):
"""
Convenience method which returns the "effective" data for the master
grid, filtered and sorted to match what would show on the UI, but not
paged etc.
"""
if session is None:
session = self.Session()
kwargs.setdefault('pageable', False)
grid = self.make_grid(session=session, **kwargs)
return grid.make_visible_data()
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Creates a new grid instance
@ -102,18 +124,6 @@ class MasterView2(MasterView):
grid.load_settings()
return grid
def get_effective_data(self, session=None, **kwargs):
"""
Convenience method which returns the "effective" data for the master
grid, filtered and sorted to match what would show on the UI, but not
paged etc.
"""
if session is None:
session = self.Session()
kwargs.setdefault('pageable', False)
grid = self.make_grid(session=session, **kwargs)
return grid.make_visible_data()
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Make and return a new (configured) rows grid instance.
@ -139,6 +149,30 @@ class MasterView2(MasterView):
grid.load_settings()
return grid
def make_version_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Creates a new version grid instance
"""
instance = kwargs.pop('instance', None)
if not instance:
instance = self.get_instance()
if factory is None:
factory = self.get_version_grid_factory()
if key is None:
key = self.get_version_grid_key()
if data is None:
data = self.get_version_data(instance)
if columns is None:
columns = self.get_version_grid_columns()
kwargs.setdefault('request', self.request)
kwargs = self.make_version_grid_kwargs(**kwargs)
grid = factory(key, data, columns, **kwargs)
self.configure_version_grid(grid)
grid.load_settings()
return grid
def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Creates a new mobile grid instance
@ -195,6 +229,17 @@ class MasterView2(MasterView):
# TODO
raise NotImplementedError
def get_version_grid_columns(self):
if hasattr(self, 'version_grid_columns'):
return self.version_grid_columns
# TODO
return [
'issued_at',
'user',
'remote_addr',
'comment',
]
def get_mobile_grid_columns(self):
if hasattr(self, 'mobile_grid_columns'):
return self.mobile_grid_columns
@ -269,6 +314,26 @@ class MasterView2(MasterView):
defaults.update(kwargs)
return defaults
def make_version_grid_kwargs(self, **kwargs):
"""
Return a dictionary of kwargs to be passed to the factory when
constructing a new version grid.
"""
defaults = {
'model_class': continuum.transaction_class(self.get_model_class()),
'width': 'full',
'pageable': True,
}
if 'main_actions' not in kwargs:
route = '{}.version'.format(self.get_route_prefix())
instance = kwargs.get('instance') or self.get_instance()
url = lambda txn, i: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id)
defaults['main_actions'] = [
self.make_action('view', icon='zoomin', url=url),
]
defaults.update(kwargs)
return defaults
def make_mobile_grid_kwargs(self, **kwargs):
"""
Must return a dictionary of kwargs to be passed to the factory when
@ -342,6 +407,9 @@ class MasterView2(MasterView):
def configure_row_grid(self, grid):
pass
def configure_version_grid(self, grid):
pass
def configure_mobile_grid(self, grid):
pass

View file

@ -46,7 +46,7 @@ from pyramid import httpexceptions
from pyramid.renderers import render_to_response
from webhelpers2.html import tags, HTML
from tailbone import forms, grids3 as grids
from tailbone import forms, grids
from tailbone.db import Session
from tailbone.views import MasterView2 as MasterView, AutocompleteView
from tailbone.progress import SessionProgress

View file

@ -39,7 +39,7 @@ import formalchemy as fa
import formencode as fe
from webhelpers2.html import tags
from tailbone import forms, grids3 as grids
from tailbone import forms, grids
from tailbone.views.purchasing import PurchasingBatchView

View file

@ -34,7 +34,7 @@ from rattail.db.auth import has_permission, administrator_role, guest_role, auth
import formalchemy as fa
from formalchemy.fields import IntegerFieldRenderer
from tailbone import forms, grids3 as grids
from tailbone import forms, grids
from tailbone.db import Session
from tailbone.views.principal import PrincipalMasterView