Remove all "old-style" (aka. version 1) grids

This commit is contained in:
Lance Edgar 2017-07-06 00:28:01 -05:00
parent 0befc46070
commit 62fa0f9fcb
19 changed files with 32 additions and 1604 deletions

View file

@ -5,24 +5,12 @@
.. automodule:: tailbone.views.batch
.. autoclass:: BatchGrid
:members:
.. autoclass:: FileBatchGrid
:members:
.. autoclass:: BatchCrud
:members:
.. autoclass:: FileBatchCrud
:members:
.. autoclass:: BatchRowGrid
:members:
.. autoclass:: ProductBatchRowGrid
:members:
.. autoclass:: BatchRowCrud
:members:

View file

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grids
"""
from __future__ import unicode_literals, absolute_import
from .core import *
from .alchemy import AlchemyGrid
from . import util
from . import search

View file

@ -1,123 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Grid Classes
"""
from __future__ import unicode_literals, absolute_import
from sqlalchemy.orm import object_session
try:
from sqlalchemy.inspection import inspect
except ImportError:
inspect = None
from sqlalchemy.orm import class_mapper
from rattail.util import prettify
import formalchemy as fa
from webhelpers.html import tags
from webhelpers.html import HTML
from tailbone.db import Session
from tailbone.grids.core import Grid
class AlchemyGrid(Grid):
sort_map = {}
pager = None
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
def __init__(self, request, cls, instances, **kwargs):
super(AlchemyGrid, self).__init__(request, **kwargs)
self._formalchemy_grid = fa.Grid(cls, instances, session=Session(),
request=request)
self._formalchemy_grid.prettify = prettify
def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr)
def __getattr__(self, attr):
return getattr(self._formalchemy_grid, attr)
def cell_class(self, field):
classes = [field.name]
return ' '.join(classes)
def checkbox(self, row):
return tags.checkbox('check-'+row.uuid)
def column_header(self, field):
class_ = None
label = field.label()
if field.key in self.sort_map:
class_ = 'sortable'
if field.key == self.config['sort']:
class_ += ' sorted ' + self.config['dir']
label = tags.link_to(label, '#')
return HTML.tag('th', class_=class_, field=field.key,
title=self.column_titles.get(field.key), c=label)
def crud_route_kwargs(self, row):
if inspect:
mapper = inspect(row.__class__)
else:
mapper = class_mapper(row.__class__)
keys = [k.key for k in mapper.primary_key]
values = [getattr(row, k) for k in keys]
return dict(zip(keys, values))
view_route_kwargs = crud_route_kwargs
edit_route_kwargs = crud_route_kwargs
delete_route_kwargs = crud_route_kwargs
def iter_fields(self):
return self._formalchemy_grid.render_fields.itervalues()
def iter_rows(self):
for row in self._formalchemy_grid.rows:
self._formalchemy_grid._set_active(row, object_session(row))
yield row
def page_count_options(self):
return [5, 10, 20, 50, 100]
def page_links(self):
return self.pager.pager(self.pager_format,
symbol_next='next',
symbol_previous='prev',
onclick="grid_navigate_page(this, '$partial_url'); return false;")
def render_field(self, field):
if self._formalchemy_grid.readonly:
return field.render_readonly()
return field.render()
def row_attrs(self, row, i):
attrs = super(AlchemyGrid, self).row_attrs(row, i)
if hasattr(row, 'uuid'):
attrs['uuid'] = row.uuid
return attrs

View file

@ -1,155 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Grid Classes
"""
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from webhelpers.html import HTML
from webhelpers.html.builder import format_attrs
from pyramid.renderers import render
from rattail.core import Object
__all__ = ['Grid']
class Grid(Object):
full = False
hoverable = True
checkboxes = False
partial_only = False
viewable = False
view_route_name = None
view_route_kwargs = None
editable = False
edit_route_name = None
edit_route_kwargs = None
deletable = False
delete_route_name = None
delete_route_kwargs = None
# Set this to a callable to allow ad-hoc row class additions.
extra_row_class = None
def __init__(self, request, **kwargs):
kwargs.setdefault('fields', OrderedDict())
kwargs.setdefault('column_titles', {})
kwargs.setdefault('extra_columns', [])
super(Grid, self).__init__(**kwargs)
self.request = request
def add_column(self, name, label, callback):
self.extra_columns.append(
Object(name=name, label=label, callback=callback))
def column_header(self, field):
return HTML.tag('th', field=field.name,
title=self.column_titles.get(field.name),
c=field.label)
def div_attrs(self):
classes = ['grid']
if self.full:
classes.append('full')
if self.hoverable:
classes.append('hoverable')
return format_attrs(
class_=' '.join(classes),
url=self.request.current_route_url(_query=None))
def get_view_url(self, row):
kwargs = {}
if self.view_route_kwargs:
if callable(self.view_route_kwargs):
kwargs = self.view_route_kwargs(row)
else:
kwargs = self.view_route_kwargs
return self.request.route_url(self.view_route_name, **kwargs)
def get_edit_url(self, row):
kwargs = {}
if self.edit_route_kwargs:
if callable(self.edit_route_kwargs):
kwargs = self.edit_route_kwargs(row)
else:
kwargs = self.edit_route_kwargs
return self.request.route_url(self.edit_route_name, **kwargs)
def get_delete_url(self, row):
kwargs = {}
if self.delete_route_kwargs:
if callable(self.delete_route_kwargs):
kwargs = self.delete_route_kwargs(row)
else:
kwargs = self.delete_route_kwargs
return self.request.route_url(self.delete_route_name, **kwargs)
def get_row_attrs(self, row, i):
attrs = self.row_attrs(row, i)
return format_attrs(**attrs)
def row_attrs(self, row, i):
return {'class_': self.get_row_class(row, i)}
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 default_row_class(self, row, i):
return 'odd' if i % 2 else 'even'
def iter_fields(self):
return self.fields.itervalues()
def iter_rows(self):
"""
Iterate over the grid rows. The default implementation simply returns
an iterator over ``self.rows``; note however that by default there is
no such attribute. You must either populate that, or overrirde this
method.
"""
return iter(self.rows)
def render(self, template='/grids/grid.mako', **kwargs):
kwargs.setdefault('grid', self)
return render(template, kwargs)
def render_field(self, field):
raise NotImplementedError

View file

@ -1,382 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Search Filters
"""
from __future__ import unicode_literals, absolute_import
import re
from sqlalchemy import func, or_
from webhelpers.html import tags
from webhelpers.html import literal
from pyramid.renderers import render
from pyramid_simpleform import Form
from pyramid_simpleform.renderers import FormRenderer
from rattail.core import Object
from rattail.gpc import GPC
from rattail.util import prettify
class SearchFilter(Object):
"""
Base class and default implementation for search filters.
"""
def __init__(self, name, label=None, **kwargs):
Object.__init__(self, **kwargs)
self.name = name
self.label = label or prettify(name)
def types_select(self):
types = [
('is', "is"),
('nt', "is not"),
('lk', "contains"),
('nl', "doesn't contain"),
(u'sx', u"sounds like"),
(u'nx', u"doesn't sound like"),
]
options = []
filter_map = self.search.filter_map[self.name]
for value, label in types:
if value in filter_map:
options.append((value, label))
return tags.select('filter_type_'+self.name,
self.search.config.get('filter_type_'+self.name),
options, class_='filter-type')
def value_control(self):
return tags.text(self.name, self.search.config.get(self.name))
class BooleanSearchFilter(SearchFilter):
"""
Boolean search filter.
"""
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name),
["True", "False"])
class ChoiceSearchFilter(SearchFilter):
"""
Generic search filter where the user may only select among a specific set
of choices.
"""
def __init__(self, choices):
self.choices = choices
def __call__(self, name, label=None, **kwargs):
super(ChoiceSearchFilter, self).__init__(name, label=label, **kwargs)
return self
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name), self.choices)
def EnumSearchFilter(enum):
options = enum.items()
class EnumSearchFilter(SearchFilter):
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name), options)
return EnumSearchFilter
class SearchForm(Form):
"""
Generic form class which aggregates :class:`SearchFilter` instances.
"""
def __init__(self, request, filter_map, config, *args, **kwargs):
super(SearchForm, self).__init__(request, *args, **kwargs)
self.filter_map = filter_map
self.config = config
self.filters = {}
def add_filter(self, filter_):
filter_.search = self
self.filters[filter_.name] = filter_
class SearchFormRenderer(FormRenderer):
"""
Renderer for :class:`SearchForm` instances.
"""
def __init__(self, form, *args, **kwargs):
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
self.request = form.request
self.filters = form.filters
self.config = form.config
def add_filter(self, visible):
options = ['add a filter']
for f in self.sorted_filters():
options.append((f.name, f.label))
return self.select('add-filter', options,
style='display: none;' if len(visible) == len(self.filters) else None)
def checkbox(self, name, checked=None, **kwargs):
if name.startswith('include_filter_'):
if checked is None:
checked = self.config[name]
return tags.checkbox(name, checked=checked, **kwargs)
if checked is None:
checked = False
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
def render(self, **kwargs):
kwargs['search'] = self
return literal(render('/grids/search.mako', kwargs))
def sorted_filters(self):
return sorted(self.filters.values(), key=lambda x: x.label)
def text(self, name, **kwargs):
return tags.text(name, value=self.config.get(name), **kwargs)
def filter_exact(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "exact match" queries applied to ``field``.
"""
return {
'is':
lambda q, v: q.filter(field == v) if v else q,
'nt':
lambda q, v: q.filter(field != v) if v else q,
}
def filter_ilike(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "ILIKE" queries applied to ``field``.
"""
def ilike(query, value):
if value:
query = query.filter(field.ilike('%%%s%%' % value))
return query
def not_ilike(query, value):
if value:
query = query.filter(or_(
field == None,
~field.ilike('%%%s%%' % value),
))
return query
return {'lk': ilike, 'nl': not_ilike}
def filter_int(field):
"""
Returns a filter map entry for an integer field. This provides exact
matching but also strips out non-numeric characters to avoid type errors.
"""
def filter_is(q, v):
v = re.sub(r'\D', '', v or '')
return q.filter(field == int(v)) if v else q
def filter_nt(q, v):
v = re.sub(r'\D', '', v or '')
return q.filter(field != int(v)) if v else q
return {'is': filter_is, 'nt': filter_nt}
def filter_soundex(field):
"""
Returns a filter map entry which leverages the `soundex()` SQL function.
"""
def soundex(query, value):
if value:
query = query.filter(func.soundex(field) == func.soundex(value))
return query
def not_soundex(query, value):
if value:
query = query.filter(func.soundex(field) != func.soundex(value))
return query
return {u'sx': soundex, u'nx': not_soundex}
def filter_ilike_and_soundex(field):
"""
Returns a filter map which provides both the `ilike` and `soundex`
features.
"""
filters = filter_ilike(field)
filters.update(filter_soundex(field))
return filters
def filter_gpc(field):
"""
Returns a filter suitable for a GPC field.
"""
def filter_is(q, v):
if not v:
return q
try:
return q.filter(field.in_((
GPC(v), GPC(v, calc_check_digit='upc'))))
except ValueError:
return q
def filter_not(q, v):
if not v:
return q
try:
return q.filter(~field.in_((
GPC(v), GPC(v, calc_check_digit='upc'))))
except ValueError:
return q
return {'is': filter_is, 'nt': filter_not}
def get_filter_config(prefix, request, filter_map, **kwargs):
"""
Returns a configuration dictionary for a search form.
"""
config = {}
def update_config(dict_, prefix='', exclude_by_default=False):
"""
Updates the ``config`` dictionary based on the contents of ``dict_``.
"""
for field in filter_map:
if prefix+'include_filter_'+field in dict_:
include = dict_[prefix+'include_filter_'+field]
include = bool(include) and include != '0'
config['include_filter_'+field] = include
elif exclude_by_default:
config['include_filter_'+field] = False
if prefix+'filter_type_'+field in dict_:
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
if prefix+field in dict_:
config[field] = dict_[prefix+field]
# Update config to exclude all filters by default.
for field in filter_map:
config['include_filter_'+field] = False
# Update config to honor default settings.
config.update(kwargs)
# Update config with data cached in session.
update_config(request.session, prefix=prefix+'.')
# Update config with data from GET/POST request.
if request.params.get('filters') == 'true':
update_config(request.params, exclude_by_default=True)
# Cache filter data in session.
for key in config:
if (not key.startswith('filter_factory_')
and not key.startswith('filter_label_')):
request.session[prefix+'.'+key] = config[key]
return config
def get_filter_map(cls, exact=[], ilike=[], int_=[], **kwargs):
"""
Convenience function which returns a "filter map" for ``cls``.
``exact``, if provided, should be a list of field names for which "exact"
filtering is to be allowed.
``ilike``, if provided, should be a list of field names for which "ILIKE"
filtering is to be allowed.
``int_``, if provided, should be a list of field names for which "integer"
filtering is to be allowed.
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
and are added directly to the map.
"""
fmap = {}
for name in exact:
fmap[name] = filter_exact(getattr(cls, name))
for name in ilike:
fmap[name] = filter_ilike(getattr(cls, name))
for name in int_:
fmap[name] = filter_int(getattr(cls, name))
fmap.update(kwargs)
return fmap
def get_search_form(request, filter_map, config):
"""
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
each filter in ``filter_map``, using configuration from ``config``.
"""
search = SearchForm(request, filter_map, config)
for field in filter_map:
factory = config.get('filter_factory_%s' % field, SearchFilter)
label = config.get('filter_label_%s' % field)
search.add_filter(factory(field, label=label))
return search
def filter_query(query, config, filter_map, join_map):
"""
Filters the given query according to filter and sorting hints found within
the config dictionary, using the filter and join maps as needed. The
filtered query is returned.
"""
joins = config.setdefault('joins', [])
for key in config:
if key.startswith('include_filter_') and config[key]:
field = key[15:]
value = config.get(field)
if value != '':
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
fmap = filter_map[field]
filt = fmap[config['filter_type_'+field]]
query = filt(query, value)
return query

View file

@ -1,138 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Utilities
"""
from sqlalchemy.orm.attributes import InstrumentedAttribute
from webhelpers.html import literal
from pyramid.response import Response
from .search import SearchFormRenderer
def get_sort_config(name, request, **kwargs):
"""
Returns a configuration dictionary for grid sorting.
"""
# Initial config uses some default values.
config = {
'dir': 'asc',
'per_page': 20,
'page': 1,
}
# Override with defaults provided by caller.
config.update(kwargs)
# Override with values from GET/POST request and/or session.
for key in config:
full_key = name+'_'+key
if request.params.get(key):
value = request.params[key]
config[key] = value
request.session[full_key] = value
elif request.session.get(full_key):
value = request.session[full_key]
config[key] = value
return config
def get_sort_map(cls, names=None, **kwargs):
"""
Convenience function which returns a sort map for ``cls``.
If ``names`` is not specified, the map will include all "standard" fields
present on the mapped class. Otherwise, the map will be limited to only
the fields which are named.
All remaining ``kwargs`` are assumed to be sort map entries, and will be
added to the map directly.
"""
smap = {}
if names is None:
names = []
for attr in cls.__dict__:
obj = getattr(cls, attr)
if isinstance(obj, InstrumentedAttribute):
if obj.key != 'uuid':
names.append(obj.key)
for name in names:
smap[name] = sorter(getattr(cls, name))
smap.update(kwargs)
return smap
def render_grid(grid, search_form=None, **kwargs):
"""
Convenience function to render ``grid`` (which should be a
:class:`tailbone.grids.Grid` instance).
This "usually" will return a dictionary to be used as context for rendering
the final view template.
However, if a partial grid is requested (or mandated), then the grid body
will be rendered and a :class:`pyramid.response.Response` object will be
returned instead.
"""
if grid.partial_only or grid.request.params.get('partial'):
return Response(body=grid.render(), content_type='text/html')
kwargs['grid'] = literal(grid.render())
if search_form:
kwargs['search'] = SearchFormRenderer(search_form)
return kwargs
def sort_query(query, config, sort_map, join_map={}):
"""
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
used, if necessary, to join additional tables to the base query. The
sorted query is returned.
"""
field = config.get('sort')
if not field:
return query
joins = config.setdefault('joins', [])
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
sort = sort_map[field]
return sort(query, config['dir'])
def sorter(field):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
return lambda q, d: q.order_by(getattr(field, d)())

View file

@ -32,9 +32,6 @@ from .master import MasterView
# TODO: deprecate / remove some of this
from .autocomplete import AutocompleteView
from .crud import CrudView
from .grids import (
GridView, AlchemyGridView, SortableAlchemyGridView,
PagedAlchemyGridView, SearchableAlchemyGridView)
def includeme(config):

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
@ -29,5 +29,5 @@ from __future__ import unicode_literals, absolute_import
from .core import BatchMasterView, FileBatchMasterView
# TODO: deprecate / remove this
from .core import (BaseGrid, BatchGrid, FileBatchGrid, BaseCrud, BatchCrud, FileBatchCrud,
StatusRenderer, BatchRowGrid, ProductBatchRowGrid, BatchRowCrud, defaults)
from .core import (BaseCrud, BatchCrud, FileBatchCrud,
StatusRenderer, BatchRowCrud, defaults)

View file

@ -51,9 +51,8 @@ from webhelpers.html import HTML, tags
from tailbone import forms, newgrids as grids
from tailbone.db import Session
from tailbone.views import MasterView, SearchableAlchemyGridView, CrudView
from tailbone.views import MasterView, CrudView
from tailbone.forms.renderers.batch import FileFieldRenderer
from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter
from tailbone.progress import SessionProgress
@ -1137,264 +1136,6 @@ class FileBatchMasterView(BatchMasterView):
"Download existing {} data file".format(model_title))
class BaseGrid(SearchableAlchemyGridView):
"""
Base view for batch and batch row grid views. You should not derive from
this class, but :class:`BatchGrid` or :class:`BatchRowGrid` instead.
"""
@property
def config_prefix(self):
"""
Config prefix for the grid view. This is used to keep track of current
filtering and sorting, within the user's session. Derived classes may
override this.
"""
return self.mapped_class.__name__.lower()
@property
def permission_prefix(self):
"""
Permission prefix for the grid view. This is used to automatically
protect certain views common to all batches. Derived classes can
override this.
"""
return self.route_prefix
def join_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default join map.
"""
return {}
def filter_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default filter map.
"""
return {}
def make_filter_map(self, **kwargs):
"""
Make a filter map by combining kwargs from the base class, with extras
supplied by a derived class.
"""
extras = self.filter_map_extras()
exact = extras.pop('exact', None)
if exact:
kwargs.setdefault('exact', []).extend(exact)
ilike = extras.pop('ilike', None)
if ilike:
kwargs.setdefault('ilike', []).extend(ilike)
kwargs.update(extras)
return super(BaseGrid, self).make_filter_map(**kwargs)
def filter_config_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default filter config.
"""
return {}
def sort_map_extras(self):
"""
Derived classes can override this. The value returned will be used to
supplement the default sort map.
"""
return {}
def _configure_grid(self, grid):
"""
Internal method for configuring the grid. This is meant only for base
classes; derived classes should not need to override it.
"""
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
class BatchGrid(BaseGrid):
"""
Base grid view for batches, which can be filtered and sorted.
"""
@property
def batch_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.batch_class
@property
def batch_display(self):
"""
Singular display text for the batch type, e.g. "Vendor Invoice".
Override this as necessary.
"""
return self.batch_class.__name__
@property
def batch_display_plural(self):
"""
Plural display text for the batch type, e.g. "Vendor Invoices".
Override this as necessary.
"""
return "{0}s".format(self.batch_display)
def join_map(self):
"""
Provides the default join map for batch grid views. Derived classes
should *not* override this, but :meth:`join_map_extras()` instead.
"""
map_ = {
'created_by':
lambda q: q.join(model.User, model.User.uuid == self.batch_class.created_by_uuid),
'executed_by':
lambda q: q.outerjoin(model.User, model.User.uuid == self.batch_class.executed_by_uuid),
}
map_.update(self.join_map_extras())
return map_
def filter_map(self):
"""
Provides the default filter map for batch grid views. Derived classes
should *not* override this, but :meth:`filter_map_extras()` instead.
"""
def executed_is(q, v):
if v == 'True':
return q.filter(self.batch_class.executed != None)
else:
return q.filter(self.batch_class.executed == None)
def executed_nt(q, v):
if v == 'True':
return q.filter(self.batch_class.executed == None)
else:
return q.filter(self.batch_class.executed != None)
return self.make_filter_map(
executed={'is': executed_is, 'nt': executed_nt})
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
defaults = self.filter_config_extras()
config = self.make_filter_config(
filter_factory_executed=BooleanSearchFilter,
filter_type_executed='is',
executed=False,
include_filter_executed=True)
defaults.update(config)
return defaults
def sort_map(self):
"""
Provides the default sort map for batch grid views. Derived classes
should *not* override this, but :meth:`sort_map_extras()` instead.
"""
map_ = self.make_sort_map(
created_by=self.sorter(model.User.username),
executed_by=self.sorter(model.User.username))
map_.update(self.sort_map_extras())
return map_
def sort_config(self):
"""
Provides the default sort config for batch grid views. Derived classes
may override this.
"""
return self.make_sort_config(sort='created', dir='desc')
def grid(self):
"""
Creates the grid for the view. Derived classes should *not* override
this, but :meth:`configure_grid()` instead.
"""
g = self.make_grid()
g.created_by.set(renderer=forms.renderers.UserFieldRenderer)
g.cognized_by.set(renderer=forms.renderers.UserFieldRenderer)
g.executed_by.set(renderer=forms.renderers.UserFieldRenderer)
self._configure_grid(g)
self.configure_grid(g)
if self.request.has_perm('{0}.view'.format(self.permission_prefix)):
g.viewable = True
g.view_route_name = '{0}.view'.format(self.route_prefix)
if self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
g.editable = True
g.edit_route_name = '{0}.edit'.format(self.route_prefix)
if self.request.has_perm('{0}.delete'.format(self.permission_prefix)):
g.deletable = True
g.delete_route_name = '{0}.delete'.format(self.route_prefix)
return g
def _configure_grid(self, grid):
grid.created_by.set(label="Created by")
grid.executed_by.set(label="Executed by")
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
g = grid
g.configure(
include=[
g.created,
g.created_by,
g.executed,
g.executed_by,
],
readonly=True)
def render_kwargs(self):
"""
Add some things to the template context: batch type display name, route
and permission prefixes.
"""
return {
'batch_display': self.batch_display,
'batch_display_plural': self.batch_display_plural,
'route_prefix': self.route_prefix,
'permission_prefix': self.permission_prefix,
}
class FileBatchGrid(BatchGrid):
"""
Base grid view for batches, which involve primarily a file upload.
"""
def _configure_grid(self, g):
super(FileBatchGrid, self)._configure_grid(g)
g.created.set(label="Uploaded")
g.created_by.set(label="Uploaded by")
def configure_grid(self, grid):
"""
Derived classes can override this. Customizes a grid which has already
been created with defaults by the base class.
"""
g = grid
g.configure(
include=[
g.created,
g.created_by,
g.filename,
g.executed,
g.executed_by,
],
readonly=True)
class BaseCrud(CrudView):
"""
Base CRUD view for batches and batch rows.
@ -2000,173 +1741,6 @@ class StatusRenderer(forms.renderers.EnumFieldRenderer):
return status_code_text
class BatchRowGrid(BaseGrid):
"""
Base grid view for batch rows, which can be filtered and sorted. Also it
can delete all rows matching the current list view query.
"""
@property
def row_class(self):
raise NotImplementedError
@property
def mapped_class(self):
return self.row_class
@property
def config_prefix(self):
"""
Config prefix for the grid view. This is used to keep track of current
filtering and sorting, within the user's session. Derived classes may
override this.
"""
return '{0}.{1}'.format(self.mapped_class.__name__.lower(),
self.request.matchdict['uuid'])
@property
def batch_class(self):
"""
Model class of the batch to which the rows belong.
"""
return self.row_class.__batch_class__
@property
def batch_display(self):
"""
Singular display text for the batch type, e.g. "Vendor Invoice".
Override this as necessary.
"""
return self.batch_class.__name__
def current_batch(self):
"""
Return the current batch, based on the UUID within the URL.
"""
return Session.query(self.batch_class).get(self.request.matchdict['uuid'])
def modify_query(self, q):
q = super(BatchRowGrid, self).modify_query(q)
q = q.filter(self.row_class.batch == self.current_batch())
q = q.filter(self.row_class.removed == False)
return q
def join_map(self):
"""
Provides the default join map for batch row grid views. Derived
classes should *not* override this, but :meth:`join_map_extras()`
instead.
"""
return self.join_map_extras()
def filter_map(self):
"""
Provides the default filter map for batch row grid views. Derived
classes should *not* override this, but :meth:`filter_map_extras()`
instead.
"""
return self.make_filter_map(exact=['status_code'])
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
kwargs = {'filter_label_status_code': "Status",
'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS)}
kwargs.update(self.filter_config_extras())
return self.make_filter_config(**kwargs)
def sort_map(self):
"""
Provides the default sort map for batch grid views. Derived classes
should *not* override this, but :meth:`sort_map_extras()` instead.
"""
map_ = self.make_sort_map()
map_.update(self.sort_map_extras())
return map_
def sort_config(self):
"""
Provides the default sort config for batch grid views. Derived classes
may override this.
"""
return self.make_sort_config(sort='sequence', dir='asc')
def grid(self):
"""
Creates the grid for the view. Derived classes should *not* override
this, but :meth:`configure_grid()` instead.
"""
g = self.make_grid()
g.extra_row_class = self.tr_class
g.sequence.set(label="Seq.")
g.status_code.set(label="Status", renderer=StatusRenderer(self.row_class.STATUS))
self._configure_grid(g)
self.configure_grid(g)
batch = self.current_batch()
g.viewable = True
g.view_route_name = '{0}.row.view'.format(self.route_prefix)
# TODO: Fix this check for edit mode.
edit_mode = self.request.referrer.endswith('/edit')
if edit_mode and not batch.executed and self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
# g.editable = True
# g.edit_route_name = '{0}.rows.edit'.format(self.route_prefix)
g.deletable = True
g.delete_route_name = '{0}.rows.delete'.format(self.route_prefix)
return g
def tr_class(self, row, i):
pass
def render_kwargs(self):
"""
Add the current batch and route prefix to the template context.
"""
return {'batch': self.current_batch(),
'route_prefix': self.route_prefix}
def bulk_delete(self):
"""
"Delete" all rows matching the current row grid view query. This sets
the ``removed`` flag on the rows but does not truly delete them.
"""
self.query().update({'removed': True}, synchronize_session=False)
return httpexceptions.HTTPFound(location=self.request.route_url('{}.view'.format(self.route_prefix),
uuid=self.request.matchdict['uuid']))
class ProductBatchRowGrid(BatchRowGrid):
"""
Base grid view for batch rows which deal directly with products.
"""
def filter_map(self):
"""
Provides the default filter map for batch row grid views. Derived
classes should *not* override this, but :meth:`filter_map_extras()`
instead.
"""
return self.make_filter_map(exact=['status_code'],
ilike=['brand_name', 'description', 'size'],
upc=self.filter_gpc(self.row_class.upc))
def filter_config(self):
"""
Provides the default filter config for batch grid views. Derived
classes should *not* override this, but :meth:`filter_config_extras()`
instead.
"""
kwargs = {'filter_label_status_code': "Status",
'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS),
'filter_label_upc': "UPC",
'filter_label_brand_name': "Brand"}
kwargs.update(self.filter_config_extras())
return self.make_filter_config(**kwargs)
class BatchRowCrud(BaseCrud):
"""
Base CRUD view for batch rows.

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -33,7 +33,6 @@ from rattail.db import model
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.newgrids.filters import ChoiceValueRenderer
class CustomerOrdersView(MasterView):

View file

@ -28,8 +28,8 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone import newgrids as grids
from tailbone.views import MasterView, AutocompleteView
from tailbone.newgrids import AlchemyGrid, GridAction
class DepartmentsView(MasterView):
@ -67,11 +67,11 @@ class DepartmentsView(MasterView):
# shouldn't need a key for this one, for instance (no settings
# required), but there is plenty of room for improvement here.
employees = sorted(department.employees, key=unicode)
employees = AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
main_actions=[
GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),
])
employees = grids.AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
main_actions=[
grids.GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),
])
employees.configure(include=[employees.display_name], readonly=True)
kwargs['employees'] = employees

View file

@ -34,13 +34,12 @@ from rattail import mail
from rattail.db import api
from rattail.config import parse_list
from tailbone import forms
from tailbone import forms, newgrids as grids
from tailbone.db import Session
from tailbone.views import MasterView, View
from tailbone.newgrids import Grid, GridColumn
class BoolGridColumn(GridColumn):
class BoolGridColumn(grids.GridColumn):
def render(self, value):
if value is None:
@ -48,7 +47,7 @@ class BoolGridColumn(GridColumn):
return 'Yes' if value else 'No'
class EmailListGridColumn(GridColumn):
class EmailListGridColumn(grids.GridColumn):
def render(self, value):
if not value:
@ -77,7 +76,7 @@ class ProfilesView(MasterView):
model_key = 'key'
url_prefix = '/email/profiles'
grid_factory = Grid
grid_factory = grids.Grid
filterable = False
pageable = False
@ -115,9 +114,9 @@ class ProfilesView(MasterView):
def configure_grid(self, g):
g.columns = [
GridColumn('key'),
GridColumn('prefix'),
GridColumn('subject'),
grids.GridColumn('key'),
grids.GridColumn('prefix'),
grids.GridColumn('subject'),
EmailListGridColumn('to'),
BoolGridColumn('enabled'),
]

View file

@ -32,11 +32,9 @@ from rattail.db import model
import formalchemy as fa
from tailbone import forms
from tailbone import forms, newgrids as grids
from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView
from tailbone.newgrids import AlchemyGrid, GridAction
from tailbone.newgrids.filters import EnumValueRenderer
class EmployeesView(MasterView):
@ -67,7 +65,7 @@ class EmployeesView(MasterView):
g.filters['status'].default_active = True
g.filters['status'].default_verb = 'equal'
g.filters['status'].default_value = self.enum.EMPLOYEE_STATUS_CURRENT
g.filters['status'].set_value_renderer(EnumValueRenderer(self.enum.EMPLOYEE_STATUS))
g.filters['status'].set_value_renderer(grids.filters.EnumValueRenderer(self.enum.EMPLOYEE_STATUS))
else:
del g.filters['id']
del g.filters['status']

View file

@ -1,32 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Views
"""
from tailbone.views.grids.core import GridView
from tailbone.views.grids.alchemy import (
AlchemyGridView, SortableAlchemyGridView,
PagedAlchemyGridView, SearchableAlchemyGridView)

View file

@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2014 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Grid Views
"""
from webhelpers import paginate
from .core import GridView
from ... import grids
from ...db import Session
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
'PagedAlchemyGridView', 'SearchableAlchemyGridView']
class AlchemyGridView(GridView):
def make_query(self, session=Session):
query = session.query(self.mapped_class)
return self.modify_query(query)
def modify_query(self, query):
return query
def query(self):
return self.make_query()
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.AlchemyGrid(
self.request, self.mapped_class, self._data, **kwargs)
def grid(self):
return self.make_grid()
def __call__(self):
self._data = self.query()
grid = self.grid()
return self.render_grid(grid)
class SortableAlchemyGridView(AlchemyGridView):
sort = None
full = True
@property
def config_prefix(self):
raise NotImplementedError
def join_map(self):
return {}
def make_sort_map(self, *args, **kwargs):
return grids.util.get_sort_map(
self.mapped_class, names=args or None, **kwargs)
def sorter(self, field):
return grids.util.sorter(field)
def sort_map(self):
return self.make_sort_map()
def make_sort_config(self, **kwargs):
return grids.util.get_sort_config(
self.config_prefix, self.request, **kwargs)
def sort_config(self):
return self.make_sort_config(sort=self.sort)
def modify_query(self, query):
return grids.util.sort_query(
query, self._sort_config, self.sort_map(), self.join_map())
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.AlchemyGrid(
self.request, self.mapped_class, self._data,
sort_map=self.sort_map(), config=self._sort_config, **kwargs)
def grid(self):
return self.make_grid()
def __call__(self):
self._sort_config = self.sort_config()
self._data = self.query()
grid = self.grid()
return self.render_grid(grid)
class PagedAlchemyGridView(SortableAlchemyGridView):
def make_pager(self):
config = self._sort_config
query = self.query()
return paginate.Page(
query, item_count=query.count(),
items_per_page=int(config['per_page']),
page=int(config['page']),
url=paginate.PageURL_WebOb(self.request))
def __call__(self):
self._sort_config = self.sort_config()
self._data = self.make_pager()
grid = self.grid()
grid.pager = self._data
return self.render_grid(grid)
class SearchableAlchemyGridView(PagedAlchemyGridView):
def filter_exact(self, field):
return grids.search.filter_exact(field)
def filter_ilike(self, field):
return grids.search.filter_ilike(field)
def filter_int(self, field):
return grids.search.filter_int(field)
def filter_soundex(self, field):
return grids.search.filter_soundex(field)
def filter_ilike_and_soundex(self, field):
return grids.search.filter_ilike_and_soundex(field)
def filter_gpc(self, field):
return grids.search.filter_gpc(field)
def make_filter_map(self, **kwargs):
return grids.search.get_filter_map(self.mapped_class, **kwargs)
def filter_map(self):
return self.make_filter_map()
def make_filter_config(self, **kwargs):
return grids.search.get_filter_config(
self.config_prefix, self.request, self.filter_map(), **kwargs)
def filter_config(self):
return self.make_filter_config()
def make_search_form(self):
return grids.search.get_search_form(
self.request, self.filter_map(), self._filter_config)
def search_form(self):
return self.make_search_form()
def modify_query(self, query):
join_map = self.join_map()
if not hasattr(self, '_filter_config'):
self._filter_config = self.filter_config()
query = grids.search.filter_query(
query, self._filter_config, self.filter_map(), join_map)
if hasattr(self, '_sort_config'):
self._sort_config['joins'] = self._filter_config['joins']
query = grids.util.sort_query(
query, self._sort_config, self.sort_map(), join_map)
return query
def __call__(self):
self._filter_config = self.filter_config()
search = self.search_form()
self._sort_config = self.sort_config()
self._data = self.make_pager()
grid = self.grid()
grid.pager = self._data
return self.render_grid(grid, search)

View file

@ -1,72 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Grid View
"""
from .. import View
from ... import grids
__all__ = ['GridView']
class GridView(View):
route_name = None
route_url = None
renderer = None
permission = None
full = False
checkboxes = False
deletable = False
partial_only = False
def update_grid_kwargs(self, kwargs):
kwargs.setdefault('full', self.full)
kwargs.setdefault('checkboxes', self.checkboxes)
kwargs.setdefault('deletable', self.deletable)
kwargs.setdefault('partial_only', self.partial_only)
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.Grid(self.request, **kwargs)
def grid(self):
return self.make_grid()
def render_kwargs(self):
return {}
def render_grid(self, grid, search=None, **kwargs):
kwargs = self.render_kwargs()
kwargs['search_form'] = search
return grids.util.render_grid(grid, **kwargs)
def __call__(self):
grid = self.grid()
return self.render_grid(grid)

View file

@ -35,7 +35,7 @@ from rattail.time import localtime
import formalchemy as fa
from pyramid import httpexceptions
from tailbone import forms, newgrids as grids
from tailbone import forms
from tailbone.views.batch import BatchMasterView

View file

@ -35,10 +35,9 @@ import formalchemy as fa
from formalchemy.fields import IntegerFieldRenderer
from webhelpers.html import HTML, tags
from tailbone import forms
from tailbone import forms, newgrids as grids
from tailbone.db import Session
from tailbone.views.principal import PrincipalMasterView
from tailbone.newgrids import AlchemyGrid, GridAction
class RolesView(PrincipalMasterView):
@ -86,11 +85,11 @@ class RolesView(PrincipalMasterView):
# for this one, for instance (no settings required), but there is
# plenty of room for improvement here.
users = sorted(role.users, key=lambda u: u.username)
users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
main_actions=[
GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),
])
users = grids.AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
main_actions=[
grids.GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),
])
users.configure(include=[users.username], readonly=True)
kwargs['users'] = users

View file

@ -26,8 +26,8 @@ Views with info about the underlying Rattail tables
from __future__ import unicode_literals, absolute_import
from tailbone import newgrids as grids
from tailbone.views import MasterView
from tailbone.newgrids import Grid, GridColumn
class TablesView(MasterView):
@ -41,7 +41,7 @@ class TablesView(MasterView):
editable = False
deletable = False
viewable = False
grid_factory = Grid
grid_factory = grids.Grid
filterable = False
pageable = False
@ -59,8 +59,8 @@ class TablesView(MasterView):
def configure_grid(self, g):
g.columns = [
GridColumn('name'),
GridColumn('row_count'),
grids.GridColumn('name'),
grids.GridColumn('row_count'),
]
g.sorters['name'] = g.make_sorter('name', foldcase=True)