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
|
.. attribute:: MasterView.grid_factory
|
||||||
|
|
||||||
Factory callable to be used when creating new grid instances; defaults to
|
Factory callable to be used when creating new grid instances; defaults to
|
||||||
:class:`tailbone.newgrids.alchemy.AlchemyGrid`.
|
:class:`tailbone.grids.Grid`.
|
||||||
|
|
||||||
.. Methods to Override
|
.. Methods to Override
|
||||||
.. -------------------
|
.. -------------------
|
||||||
|
|
|
@ -22,7 +22,7 @@ Package API:
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
api/newgrids
|
api/grids
|
||||||
api/subscribers
|
api/subscribers
|
||||||
api/views/batch
|
api/views/batch
|
||||||
api/views/master
|
api/views/master
|
||||||
|
|
|
@ -26,8 +26,6 @@ Grids and Friends
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from . import filters
|
||||||
from .core import Grid, GridAction
|
from .core import Grid, GridAction
|
||||||
from .mobile import MobileGrid
|
from .mobile import MobileGrid
|
||||||
|
|
||||||
# TODO
|
|
||||||
from tailbone.newgrids import filters
|
|
|
@ -26,23 +26,23 @@ Core Grid Classes
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import api
|
from rattail.db import api
|
||||||
from rattail.db.types import GPCType
|
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
|
import webhelpers2_grid
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
from paginate_sqlalchemy import SqlalchemyOrmPage
|
from paginate_sqlalchemy import SqlalchemyOrmPage
|
||||||
|
|
||||||
|
from . import filters as gridfilters
|
||||||
from tailbone.db import Session
|
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
|
from tailbone.util import raw_datetime
|
||||||
|
|
||||||
|
|
||||||
|
@ -301,7 +301,7 @@ class Grid(object):
|
||||||
def actions_column_format(self, column_number, row_number, item):
|
def actions_column_format(self, column_number, row_number, item):
|
||||||
return HTML.td(self.render_actions(item, row_number), class_='actions')
|
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 = kwargs
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
grid_class = ''
|
grid_class = ''
|
||||||
|
@ -320,7 +320,7 @@ class Grid(object):
|
||||||
if callable(self.default_filters):
|
if callable(self.default_filters):
|
||||||
return self.default_filters()
|
return self.default_filters()
|
||||||
return self.default_filters
|
return self.default_filters
|
||||||
filters = newgrids.filters.GridFilterSet()
|
filters = gridfilters.GridFilterSet()
|
||||||
if self.model_class:
|
if self.model_class:
|
||||||
mapper = orm.class_mapper(self.model_class)
|
mapper = orm.class_mapper(self.model_class)
|
||||||
for prop in mapper.iterate_properties:
|
for prop in mapper.iterate_properties:
|
||||||
|
@ -344,22 +344,22 @@ class Grid(object):
|
||||||
"""
|
"""
|
||||||
factory = kwargs.pop('factory', None)
|
factory = kwargs.pop('factory', None)
|
||||||
if not factory:
|
if not factory:
|
||||||
factory = newgrids.filters.AlchemyGridFilter
|
factory = gridfilters.AlchemyGridFilter
|
||||||
if isinstance(column.type, sa.String):
|
if isinstance(column.type, sa.String):
|
||||||
factory = newgrids.filters.AlchemyStringFilter
|
factory = gridfilters.AlchemyStringFilter
|
||||||
elif isinstance(column.type, sa.Numeric):
|
elif isinstance(column.type, sa.Numeric):
|
||||||
factory = newgrids.filters.AlchemyNumericFilter
|
factory = gridfilters.AlchemyNumericFilter
|
||||||
elif isinstance(column.type, sa.Integer):
|
elif isinstance(column.type, sa.Integer):
|
||||||
factory = newgrids.filters.AlchemyNumericFilter
|
factory = gridfilters.AlchemyNumericFilter
|
||||||
elif isinstance(column.type, sa.Boolean):
|
elif isinstance(column.type, sa.Boolean):
|
||||||
# TODO: check column for nullable here?
|
# TODO: check column for nullable here?
|
||||||
factory = newgrids.filters.AlchemyNullableBooleanFilter
|
factory = gridfilters.AlchemyNullableBooleanFilter
|
||||||
elif isinstance(column.type, sa.Date):
|
elif isinstance(column.type, sa.Date):
|
||||||
factory = newgrids.filters.AlchemyDateFilter
|
factory = gridfilters.AlchemyDateFilter
|
||||||
elif isinstance(column.type, sa.DateTime):
|
elif isinstance(column.type, sa.DateTime):
|
||||||
factory = newgrids.filters.AlchemyDateTimeFilter
|
factory = gridfilters.AlchemyDateTimeFilter
|
||||||
elif isinstance(column.type, GPCType):
|
elif isinstance(column.type, GPCType):
|
||||||
factory = newgrids.filters.AlchemyGPCFilter
|
factory = gridfilters.AlchemyGPCFilter
|
||||||
return factory(key, column=column, config=self.request.rattail_config, **kwargs)
|
return factory(key, column=column, config=self.request.rattail_config, **kwargs)
|
||||||
|
|
||||||
def iter_filters(self):
|
def iter_filters(self):
|
||||||
|
@ -799,7 +799,7 @@ class Grid(object):
|
||||||
data = self.pager
|
data = self.pager
|
||||||
return data
|
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.
|
Render the complete grid, including filters.
|
||||||
"""
|
"""
|
||||||
|
@ -808,7 +808,7 @@ class Grid(object):
|
||||||
context.setdefault('allow_save_defaults', True)
|
context.setdefault('allow_save_defaults', True)
|
||||||
return render(template, context)
|
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.
|
Render the filters to a Unicode string, using the specified template.
|
||||||
Additional kwargs are passed along as context to the 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['{}.verb'.format(filtr.key)] = filtr.verb
|
||||||
data[filtr.key] = filtr.value
|
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['request'] = self.request
|
||||||
kwargs['grid'] = self
|
kwargs['grid'] = self
|
||||||
kwargs['form'] = newgrids.filters.GridFiltersFormRenderer(form)
|
kwargs['form'] = gridfilters.GridFiltersFormRenderer(form)
|
||||||
return render(template, kwargs)
|
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):
|
def render_actions(self, row, i):
|
||||||
"""
|
"""
|
||||||
Returns the rendered contents of the 'actions' column for a given row.
|
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)
|
value = tags.link_to(value, url)
|
||||||
class_name = 'c{} {}'.format(column_number, column_name)
|
class_name = 'c{} {}'.format(column_number, column_name)
|
||||||
return HTML.tag('td', value, class_=class_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 pyramid.renderers import render
|
||||||
|
|
||||||
from tailbone.grids3 import Grid
|
from .core import Grid
|
||||||
|
|
||||||
|
|
||||||
class MobileGrid(Grid):
|
class MobileGrid(Grid):
|
||||||
|
@ -36,18 +36,18 @@ class MobileGrid(Grid):
|
||||||
Base class for all mobile grids
|
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 = kwargs
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
return render(template, context)
|
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 = kwargs
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
return render(template, context)
|
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 = kwargs
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
return render(template, context)
|
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 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<div ${grid.div_attrs()}>
|
<div class="newgrid grid3 ${grid_class}">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
${grid.make_webhelpers_grid()}
|
||||||
<tr>
|
|
||||||
% if grid.checkboxes:
|
|
||||||
<th class="checkbox">${h.checkbox('check-all')}</th>
|
|
||||||
% endif
|
|
||||||
% for field in grid.iter_fields():
|
|
||||||
${grid.column_header(field)}
|
|
||||||
% endfor
|
|
||||||
% for col in grid.extra_columns:
|
|
||||||
<th>${col.label}</td>
|
|
||||||
% endfor
|
|
||||||
% if grid.viewable:
|
|
||||||
<th> </th>
|
|
||||||
% endif
|
|
||||||
% if grid.editable:
|
|
||||||
<th> </th>
|
|
||||||
% endif
|
|
||||||
% if grid.deletable:
|
|
||||||
<th> </th>
|
|
||||||
% endif
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
% for i, row in enumerate(grid.iter_rows(), 1):
|
|
||||||
<tr ${grid.get_row_attrs(row, i)}>
|
|
||||||
% if grid.checkboxes:
|
|
||||||
<td class="checkbox">${grid.checkbox(row)}</td>
|
|
||||||
% endif
|
|
||||||
% for field in grid.iter_fields():
|
|
||||||
<td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
|
|
||||||
% endfor
|
|
||||||
% for col in grid.extra_columns:
|
|
||||||
<td class="${col.name}">${col.callback(row)}</td>
|
|
||||||
% endfor
|
|
||||||
% if grid.viewable:
|
|
||||||
<td class="view" url="${grid.get_view_url(row)}"> </td>
|
|
||||||
% endif
|
|
||||||
% if grid.editable:
|
|
||||||
<td class="edit" url="${grid.get_edit_url(row)}"> </td>
|
|
||||||
% endif
|
|
||||||
% if grid.deletable:
|
|
||||||
<td class="delete" url="${grid.get_delete_url(row)}"> </td>
|
|
||||||
% endif
|
|
||||||
</tr>
|
|
||||||
% endfor
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
% if grid.pager:
|
% if grid.pageable and grid.pager:
|
||||||
<div class="pager">
|
<div class="pager">
|
||||||
<p class="showing">
|
<p class="showing">
|
||||||
showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
|
${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
|
||||||
(page ${grid.pager.page} of ${grid.pager.page_count})
|
% if grid.pager.page_count > 1:
|
||||||
|
${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
|
||||||
|
% endif
|
||||||
</p>
|
</p>
|
||||||
<p class="page-links">
|
<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
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% 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" />
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="head_tags()">
|
||||||
|
@ -22,9 +22,6 @@
|
||||||
<li>${h.link_to("Edit Printer Settings", url('labelprofiles.printer_settings', uuid=instance.uuid))}</li>
|
<li>${h.link_to("Edit Printer Settings", url('labelprofiles.printer_settings', uuid=instance.uuid))}</li>
|
||||||
% endif
|
% endif
|
||||||
% 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>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
${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
|
## 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)">
|
<%def name="render_main_fields(form)">
|
||||||
${render_field_readonly(form.fieldset.upc)}
|
${render_field_readonly(form.fieldset.upc)}
|
||||||
${render_field_readonly(form.fieldset.brand)}
|
${render_field_readonly(form.fieldset.brand)}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/edit.mako" />
|
<%inherit file="/master/edit.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_styles()">
|
||||||
${parent.head_tags()}
|
${parent.extra_styles()}
|
||||||
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
|
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
|
||||||
</%def>
|
</%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()}
|
${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'))}
|
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
|
||||||
</%def>
|
</%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()}
|
${parent.body()}
|
||||||
|
|
||||||
<h2>Users</h2>
|
<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; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%namespace name="base" file="tailbone:templates/base.mako" />
|
<%namespace name="base" file="tailbone:templates/base.mako" />
|
||||||
<%namespace file="/menu.mako" import="main_menu_items" />
|
<%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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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" />
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_styles()">
|
||||||
${parent.head_tags()}
|
${parent.extra_styles()}
|
||||||
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
|
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
|
||||||
</%def>
|
</%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()}
|
${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.
|
Base views for maintaining "new-style" batches.
|
||||||
|
|
||||||
.. note::
|
|
||||||
This is all still somewhat experimental.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
@ -50,7 +47,7 @@ from pyramid.response import FileResponse
|
||||||
from pyramid_simpleform import Form
|
from pyramid_simpleform import Form
|
||||||
from webhelpers2.html import HTML, tags
|
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.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
from tailbone.forms.renderers.batch import FileFieldRenderer
|
from tailbone.forms.renderers.batch import FileFieldRenderer
|
||||||
|
|
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone import grids3 as grids
|
from tailbone import grids
|
||||||
from tailbone.views import MasterView2
|
from tailbone.views import MasterView2
|
||||||
from tailbone.views.batch import BatchMasterView, FileBatchMasterView
|
from tailbone.views.batch import BatchMasterView, FileBatchMasterView
|
||||||
from tailbone.views.batch.core import MobileBatchStatusFilter
|
from tailbone.views.batch.core import MobileBatchStatusFilter
|
||||||
|
|
|
@ -37,7 +37,7 @@ import formalchemy
|
||||||
from pyramid.response import FileResponse
|
from pyramid.response import FileResponse
|
||||||
from webhelpers2.html import literal
|
from webhelpers2.html import literal
|
||||||
|
|
||||||
from tailbone import grids3 as grids
|
from tailbone import grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView2 as MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer
|
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer
|
||||||
|
|
|
@ -30,7 +30,7 @@ import six
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone import grids3 as grids
|
from tailbone import grids
|
||||||
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ from rattail.db import model
|
||||||
|
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
|
|
||||||
from tailbone import forms, grids3 as grids
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
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 pyramid.renderers import get_renderer, render_to_response, render
|
||||||
from webhelpers2.html import HTML, tags
|
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.views import View
|
||||||
from tailbone.newgrids import filters, AlchemyGrid, GridAction, MobileGrid
|
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View):
|
class MasterView(View):
|
||||||
|
@ -161,24 +160,6 @@ class MasterView(View):
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
return grid
|
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
|
@classmethod
|
||||||
def get_mobile_grid_key(cls):
|
def get_mobile_grid_key(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -422,14 +403,6 @@ class MasterView(View):
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
return grid
|
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
|
@classmethod
|
||||||
def get_version_grid_key(cls):
|
def get_version_grid_key(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1254,23 +1227,6 @@ class MasterView(View):
|
||||||
# Grid Stuff
|
# 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
|
@classmethod
|
||||||
def get_grid_key(cls):
|
def get_grid_key(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1385,7 +1341,7 @@ class MasterView(View):
|
||||||
if url is None:
|
if url is None:
|
||||||
route = '{}.{}'.format(self.get_route_prefix(), key)
|
route = '{}.{}'.format(self.get_route_prefix(), key)
|
||||||
url = lambda r, i: self.request.route_url(route, **self.get_action_route_kwargs(r))
|
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):
|
def get_action_route_kwargs(self, row):
|
||||||
"""
|
"""
|
||||||
|
@ -1424,20 +1380,6 @@ class MasterView(View):
|
||||||
def _preconfigure_grid(self, grid):
|
def _preconfigure_grid(self, grid):
|
||||||
pass
|
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):
|
def get_data(self, session=None):
|
||||||
"""
|
"""
|
||||||
Generate the base data set for the grid. This typically will be a
|
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 __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
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,6 +66,14 @@ class MasterView2(MasterView):
|
||||||
"""
|
"""
|
||||||
return getattr(cls, 'row_grid_factory', grids.Grid)
|
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
|
@classmethod
|
||||||
def get_mobile_grid_factory(cls):
|
def get_mobile_grid_factory(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -82,6 +92,18 @@ class MasterView2(MasterView):
|
||||||
"""
|
"""
|
||||||
return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid)
|
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):
|
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a new grid instance
|
Creates a new grid instance
|
||||||
|
@ -102,18 +124,6 @@ class MasterView2(MasterView):
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
return grid
|
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):
|
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make and return a new (configured) rows grid instance.
|
Make and return a new (configured) rows grid instance.
|
||||||
|
@ -139,6 +149,30 @@ class MasterView2(MasterView):
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
return grid
|
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):
|
def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a new mobile grid instance
|
Creates a new mobile grid instance
|
||||||
|
@ -195,6 +229,17 @@ class MasterView2(MasterView):
|
||||||
# TODO
|
# TODO
|
||||||
raise NotImplementedError
|
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):
|
def get_mobile_grid_columns(self):
|
||||||
if hasattr(self, 'mobile_grid_columns'):
|
if hasattr(self, 'mobile_grid_columns'):
|
||||||
return self.mobile_grid_columns
|
return self.mobile_grid_columns
|
||||||
|
@ -269,6 +314,26 @@ class MasterView2(MasterView):
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return defaults
|
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):
|
def make_mobile_grid_kwargs(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Must return a dictionary of kwargs to be passed to the factory when
|
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):
|
def configure_row_grid(self, grid):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def configure_version_grid(self, grid):
|
||||||
|
pass
|
||||||
|
|
||||||
def configure_mobile_grid(self, grid):
|
def configure_mobile_grid(self, grid):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ from pyramid import httpexceptions
|
||||||
from pyramid.renderers import render_to_response
|
from pyramid.renderers import render_to_response
|
||||||
from webhelpers2.html import tags, HTML
|
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.db import Session
|
||||||
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
from tailbone.progress import SessionProgress
|
from tailbone.progress import SessionProgress
|
||||||
|
|
|
@ -39,7 +39,7 @@ import formalchemy as fa
|
||||||
import formencode as fe
|
import formencode as fe
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from tailbone import forms, grids3 as grids
|
from tailbone import forms, grids
|
||||||
from tailbone.views.purchasing import PurchasingBatchView
|
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
|
import formalchemy as fa
|
||||||
from formalchemy.fields import IntegerFieldRenderer
|
from formalchemy.fields import IntegerFieldRenderer
|
||||||
|
|
||||||
from tailbone import forms, grids3 as grids
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.principal import PrincipalMasterView
|
from tailbone.views.principal import PrincipalMasterView
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue