Remove all "old-style" (aka. version 1) grids
This commit is contained in:
parent
0befc46070
commit
62fa0f9fcb
|
@ -5,24 +5,12 @@
|
||||||
|
|
||||||
.. automodule:: tailbone.views.batch
|
.. automodule:: tailbone.views.batch
|
||||||
|
|
||||||
.. autoclass:: BatchGrid
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: FileBatchGrid
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: BatchCrud
|
.. autoclass:: BatchCrud
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: FileBatchCrud
|
.. autoclass:: FileBatchCrud
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: BatchRowGrid
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: ProductBatchRowGrid
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: BatchRowCrud
|
.. autoclass:: BatchRowCrud
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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)())
|
|
|
@ -32,9 +32,6 @@ from .master import MasterView
|
||||||
# TODO: deprecate / remove some of this
|
# TODO: deprecate / remove some of this
|
||||||
from .autocomplete import AutocompleteView
|
from .autocomplete import AutocompleteView
|
||||||
from .crud import CrudView
|
from .crud import CrudView
|
||||||
from .grids import (
|
|
||||||
GridView, AlchemyGridView, SortableAlchemyGridView,
|
|
||||||
PagedAlchemyGridView, SearchableAlchemyGridView)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2016 Lance Edgar
|
# Copyright © 2010-2017 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -29,5 +29,5 @@ from __future__ import unicode_literals, absolute_import
|
||||||
from .core import BatchMasterView, FileBatchMasterView
|
from .core import BatchMasterView, FileBatchMasterView
|
||||||
|
|
||||||
# TODO: deprecate / remove this
|
# TODO: deprecate / remove this
|
||||||
from .core import (BaseGrid, BatchGrid, FileBatchGrid, BaseCrud, BatchCrud, FileBatchCrud,
|
from .core import (BaseCrud, BatchCrud, FileBatchCrud,
|
||||||
StatusRenderer, BatchRowGrid, ProductBatchRowGrid, BatchRowCrud, defaults)
|
StatusRenderer, BatchRowCrud, defaults)
|
||||||
|
|
|
@ -51,9 +51,8 @@ from webhelpers.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import forms, newgrids as grids
|
from tailbone import forms, newgrids as grids
|
||||||
from tailbone.db import Session
|
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.forms.renderers.batch import FileFieldRenderer
|
||||||
from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter
|
|
||||||
from tailbone.progress import SessionProgress
|
from tailbone.progress import SessionProgress
|
||||||
|
|
||||||
|
|
||||||
|
@ -1137,264 +1136,6 @@ class FileBatchMasterView(BatchMasterView):
|
||||||
"Download existing {} data file".format(model_title))
|
"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):
|
class BaseCrud(CrudView):
|
||||||
"""
|
"""
|
||||||
Base CRUD view for batches and batch rows.
|
Base CRUD view for batches and batch rows.
|
||||||
|
@ -2000,173 +1741,6 @@ class StatusRenderer(forms.renderers.EnumFieldRenderer):
|
||||||
return status_code_text
|
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):
|
class BatchRowCrud(BaseCrud):
|
||||||
"""
|
"""
|
||||||
Base CRUD view for batch rows.
|
Base CRUD view for batch rows.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -33,7 +33,6 @@ from rattail.db import model
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
from tailbone.newgrids.filters import ChoiceValueRenderer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerOrdersView(MasterView):
|
class CustomerOrdersView(MasterView):
|
||||||
|
|
|
@ -28,8 +28,8 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
from tailbone import newgrids as grids
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView, AutocompleteView
|
||||||
from tailbone.newgrids import AlchemyGrid, GridAction
|
|
||||||
|
|
||||||
|
|
||||||
class DepartmentsView(MasterView):
|
class DepartmentsView(MasterView):
|
||||||
|
@ -67,9 +67,9 @@ class DepartmentsView(MasterView):
|
||||||
# shouldn't need a key for this one, for instance (no settings
|
# shouldn't need a key for this one, for instance (no settings
|
||||||
# required), but there is plenty of room for improvement here.
|
# required), but there is plenty of room for improvement here.
|
||||||
employees = sorted(department.employees, key=unicode)
|
employees = sorted(department.employees, key=unicode)
|
||||||
employees = AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
|
employees = grids.AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
|
||||||
main_actions=[
|
main_actions=[
|
||||||
GridAction('view', icon='zoomin',
|
grids.GridAction('view', icon='zoomin',
|
||||||
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),
|
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),
|
||||||
])
|
])
|
||||||
employees.configure(include=[employees.display_name], readonly=True)
|
employees.configure(include=[employees.display_name], readonly=True)
|
||||||
|
|
|
@ -34,13 +34,12 @@ from rattail import mail
|
||||||
from rattail.db import api
|
from rattail.db import api
|
||||||
from rattail.config import parse_list
|
from rattail.config import parse_list
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms, newgrids as grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, View
|
from tailbone.views import MasterView, View
|
||||||
from tailbone.newgrids import Grid, GridColumn
|
|
||||||
|
|
||||||
|
|
||||||
class BoolGridColumn(GridColumn):
|
class BoolGridColumn(grids.GridColumn):
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -48,7 +47,7 @@ class BoolGridColumn(GridColumn):
|
||||||
return 'Yes' if value else 'No'
|
return 'Yes' if value else 'No'
|
||||||
|
|
||||||
|
|
||||||
class EmailListGridColumn(GridColumn):
|
class EmailListGridColumn(grids.GridColumn):
|
||||||
|
|
||||||
def render(self, value):
|
def render(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
|
@ -77,7 +76,7 @@ class ProfilesView(MasterView):
|
||||||
model_key = 'key'
|
model_key = 'key'
|
||||||
url_prefix = '/email/profiles'
|
url_prefix = '/email/profiles'
|
||||||
|
|
||||||
grid_factory = Grid
|
grid_factory = grids.Grid
|
||||||
filterable = False
|
filterable = False
|
||||||
pageable = False
|
pageable = False
|
||||||
|
|
||||||
|
@ -115,9 +114,9 @@ class ProfilesView(MasterView):
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.columns = [
|
g.columns = [
|
||||||
GridColumn('key'),
|
grids.GridColumn('key'),
|
||||||
GridColumn('prefix'),
|
grids.GridColumn('prefix'),
|
||||||
GridColumn('subject'),
|
grids.GridColumn('subject'),
|
||||||
EmailListGridColumn('to'),
|
EmailListGridColumn('to'),
|
||||||
BoolGridColumn('enabled'),
|
BoolGridColumn('enabled'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -32,11 +32,9 @@ from rattail.db import model
|
||||||
|
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms, newgrids as grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView, AutocompleteView
|
||||||
from tailbone.newgrids import AlchemyGrid, GridAction
|
|
||||||
from tailbone.newgrids.filters import EnumValueRenderer
|
|
||||||
|
|
||||||
|
|
||||||
class EmployeesView(MasterView):
|
class EmployeesView(MasterView):
|
||||||
|
@ -67,7 +65,7 @@ class EmployeesView(MasterView):
|
||||||
g.filters['status'].default_active = True
|
g.filters['status'].default_active = True
|
||||||
g.filters['status'].default_verb = 'equal'
|
g.filters['status'].default_verb = 'equal'
|
||||||
g.filters['status'].default_value = self.enum.EMPLOYEE_STATUS_CURRENT
|
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:
|
else:
|
||||||
del g.filters['id']
|
del g.filters['id']
|
||||||
del g.filters['status']
|
del g.filters['status']
|
||||||
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -35,7 +35,7 @@ from rattail.time import localtime
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
from pyramid import httpexceptions
|
from pyramid import httpexceptions
|
||||||
|
|
||||||
from tailbone import forms, newgrids as grids
|
from tailbone import forms
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,9 @@ import formalchemy as fa
|
||||||
from formalchemy.fields import IntegerFieldRenderer
|
from formalchemy.fields import IntegerFieldRenderer
|
||||||
from webhelpers.html import HTML, tags
|
from webhelpers.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms, newgrids as grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.principal import PrincipalMasterView
|
from tailbone.views.principal import PrincipalMasterView
|
||||||
from tailbone.newgrids import AlchemyGrid, GridAction
|
|
||||||
|
|
||||||
|
|
||||||
class RolesView(PrincipalMasterView):
|
class RolesView(PrincipalMasterView):
|
||||||
|
@ -86,9 +85,9 @@ class RolesView(PrincipalMasterView):
|
||||||
# for this one, for instance (no settings required), but there is
|
# for this one, for instance (no settings required), but there is
|
||||||
# plenty of room for improvement here.
|
# plenty of room for improvement here.
|
||||||
users = sorted(role.users, key=lambda u: u.username)
|
users = sorted(role.users, key=lambda u: u.username)
|
||||||
users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
|
users = grids.AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
|
||||||
main_actions=[
|
main_actions=[
|
||||||
GridAction('view', icon='zoomin',
|
grids.GridAction('view', icon='zoomin',
|
||||||
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),
|
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),
|
||||||
])
|
])
|
||||||
users.configure(include=[users.username], readonly=True)
|
users.configure(include=[users.username], readonly=True)
|
||||||
|
|
|
@ -26,8 +26,8 @@ Views with info about the underlying Rattail tables
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
from tailbone import newgrids as grids
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
from tailbone.newgrids import Grid, GridColumn
|
|
||||||
|
|
||||||
|
|
||||||
class TablesView(MasterView):
|
class TablesView(MasterView):
|
||||||
|
@ -41,7 +41,7 @@ class TablesView(MasterView):
|
||||||
editable = False
|
editable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
viewable = False
|
viewable = False
|
||||||
grid_factory = Grid
|
grid_factory = grids.Grid
|
||||||
filterable = False
|
filterable = False
|
||||||
pageable = False
|
pageable = False
|
||||||
|
|
||||||
|
@ -59,8 +59,8 @@ class TablesView(MasterView):
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.columns = [
|
g.columns = [
|
||||||
GridColumn('name'),
|
grids.GridColumn('name'),
|
||||||
GridColumn('row_count'),
|
grids.GridColumn('row_count'),
|
||||||
]
|
]
|
||||||
|
|
||||||
g.sorters['name'] = g.make_sorter('name', foldcase=True)
|
g.sorters['name'] = g.make_sorter('name', foldcase=True)
|
||||||
|
|
Loading…
Reference in a new issue