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:
parent
52c7f485ab
commit
c57e2e17cc
6
docs/api/grids.rst
Normal file
6
docs/api/grids.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``tailbone.grids``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.grids
|
||||
:members:
|
|
@ -1,10 +0,0 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
``tailbone.newgrids``
|
||||
=====================
|
||||
|
||||
.. automodule:: tailbone.newgrids
|
||||
:members:
|
||||
|
||||
.. automodule:: tailbone.newgrids.alchemy
|
||||
:members:
|
|
@ -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
|
||||
.. -------------------
|
||||
|
|
|
@ -22,7 +22,7 @@ Package API:
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api/newgrids
|
||||
api/grids
|
||||
api/subscribers
|
||||
api/views/batch
|
||||
api/views/master
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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:
|
||||
|
||||
% 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 -->
|
|
@ -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> </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>
|
||||
${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
|
||||
${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
|
||||
|
|
|
@ -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
|
||||
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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()}
|
||||
|
|
|
@ -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
|
||||
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
|
@ -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()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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)}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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>
|
||||
|
|
|
@ -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()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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()}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/index.mako" />
|
||||
${parent.body()}
|
|
@ -1,3 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/versions/view.mako" />
|
||||
${parent.body()}
|
|
@ -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}: ${model_instance}</h2>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -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
|
||||
|
|
||||
% 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__}: ${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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue