Stole grids and grid-based views from edbob.

This commit is contained in:
Lance Edgar 2013-07-19 15:04:03 -07:00
parent aad5e6e2bd
commit 578e4bde2a
14 changed files with 1093 additions and 6 deletions

View file

@ -33,15 +33,19 @@ import formalchemy
from edbob.pyramid.forms import pretty_datetime
from edbob.pyramid.forms.formalchemy.renderers import (
AutocompleteFieldRenderer, EnumFieldRenderer, YesNoFieldRenderer)
AutocompleteFieldRenderer, YesNoFieldRenderer)
import rattail
from rattail.gpc import GPC
from .common import EnumFieldRenderer
from .products import ProductFieldRenderer
from .users import UserFieldRenderer
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', 'YesNoFieldRenderer',
'GPCFieldRenderer', 'PersonFieldRenderer', 'PriceFieldRenderer',
'PriceWithExpirationFieldRenderer']
'PriceWithExpirationFieldRenderer', 'ProductFieldRenderer', 'UserFieldRenderer']
class GPCFieldRenderer(formalchemy.TextFieldRenderer):

View file

@ -0,0 +1,54 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.forms.renderers.common`` -- Common Field Renderers
"""
from formalchemy.fields import SelectFieldRenderer
__all__ = ['EnumFieldRenderer']
def EnumFieldRenderer(enum):
"""
Adds support for enumeration fields.
"""
class Renderer(SelectFieldRenderer):
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
if value in enum:
return enum[value]
return str(value)
def render(self, **kwargs):
opts = [(enum[x], x) for x in sorted(enum)]
return SelectFieldRenderer.render(self, opts, **kwargs)
return Renderer

View file

@ -0,0 +1,44 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.forms.renderers.products`` -- Product Field Renderers
"""
from formalchemy.fields import TextFieldRenderer
__all__ = ['ProductFieldRenderer']
class ProductFieldRenderer(TextFieldRenderer):
"""
Renderer for fields which represent ``Product`` instances.
"""
def render_readonly(self, **kwargs):
product = self.raw_value
if product is None:
return ''
return product.full_description

View file

@ -0,0 +1,44 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.forms.renderers.users`` -- User Field Renderers
"""
from formalchemy.fields import TextFieldRenderer
__all__ = ['UserFieldRenderer']
class UserFieldRenderer(TextFieldRenderer):
"""
Renderer for fields which represent ``User`` instances.
"""
def render_readonly(self, **kwargs):
user = self.raw_value
if user is None:
return u''
return unicode(user.display_name)

View file

@ -26,4 +26,7 @@
``rattail.pyramid.grids`` -- Grids
"""
from rattail.pyramid.grids.search import *
from rattail.pyramid.grids.core import *
from rattail.pyramid.grids.alchemy import *
from rattail.pyramid.grids import util
from rattail.pyramid.grids import search

View file

@ -0,0 +1,125 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes
"""
from webhelpers.html import tags
from webhelpers.html import HTML
import formalchemy
import edbob
from edbob.util import prettify
from rattail.pyramid.grids.core import Grid
from rattail.pyramid import Session
__all__ = ['AlchemyGrid']
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 = formalchemy.Grid(
cls, instances, session=Session(), request=request)
self._formalchemy_grid.prettify = prettify
self.noclick_fields = []
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]
if field.name in self.noclick_fields:
classes.append('noclick')
return ' '.join(classes)
def checkbox(self, row):
return tags.checkbox('check-'+row.uuid)
def click_route_kwargs(self, row):
return {'uuid': 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 edit_route_kwargs(self, row):
return {'uuid': row.uuid}
def delete_route_kwargs(self, row):
return {'uuid': row.uuid}
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)
yield row
def page_count_options(self):
options = edbob.config.get('edbob.pyramid', 'grid.page_count_options')
if options:
options = options.split(',')
options = [int(x.strip()) for x in options]
else:
options = [5, 10, 20, 50, 100]
return options
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

@ -0,0 +1,138 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.grids.core`` -- 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 edbob.core import Object
__all__ = ['Grid']
class Grid(Object):
full = False
hoverable = True
clickable = False
checkboxes = False
editable = False
deletable = False
partial_only = False
click_route_name = None
click_route_kwargs = None
edit_route_name = None
edit_route_kwargs = None
delete_route_name = None
delete_route_kwargs = 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.clickable:
classes.append('clickable')
if self.hoverable:
classes.append('hoverable')
return format_attrs(
class_=' '.join(classes),
url=self.request.current_route_url())
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_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_row_attrs(self, row, i):
attrs = self.row_attrs(row, i)
if self.clickable:
kwargs = {}
if self.click_route_kwargs:
if callable(self.click_route_kwargs):
kwargs = self.click_route_kwargs(row)
else:
kwargs = self.click_route_kwargs
attrs['url'] = self.request.route_url(self.click_route_name, **kwargs)
return format_attrs(**attrs)
def iter_fields(self):
return self.fields.itervalues()
def iter_rows(self):
raise NotImplementedError
def render(self, template='/grids/grid.mako', **kwargs):
kwargs.setdefault('grid', self)
return render(template, kwargs)
def render_field(self, field):
raise NotImplementedError
def row_attrs(self, row, i):
attrs = {'class_': 'odd' if i % 2 else 'even'}
return attrs

View file

@ -26,9 +26,57 @@
``rattail.pyramid.grids.search`` -- Grid Search Filters
"""
from webhelpers.html import tags
from sqlalchemy import or_
from edbob.pyramid.grids.search import SearchFilter
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 edbob.core import Object
from edbob.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"),
]
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"])
def EnumSearchFilter(enum):
@ -39,3 +87,199 @@ def EnumSearchFilter(enum):
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 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=[], **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.
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))
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 ``query`` according to ``config`` and ``filter_map``. ``join_map``
is used, if necessary, to join additional tables to the base query. The
filtered query is returned.
"""
joins = config.setdefault('joins', [])
for key in config:
if key.startswith('include_filter_') and config[key]:
field = key[15:]
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
value = config.get(field)
if value:
fmap = filter_map[field]
filt = fmap[config['filter_type_'+field]]
query = filt(query, value)
return query

View file

@ -0,0 +1,138 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.grids.util`` -- Grid Utilities
"""
from sqlalchemy.orm.attributes import InstrumentedAttribute
from webhelpers.html import literal
from pyramid.response import Response
from rattail.pyramid.grids.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:`edbob.pyramid.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

@ -30,6 +30,15 @@ from rattail.pyramid.views.crud import *
from rattail.pyramid.views.autocomplete import *
class View(object):
"""
Base for all class-based views.
"""
def __init__(self, request):
self.request = request
def includeme(config):
config.include('rattail.pyramid.views.batches')
# config.include('rattail.pyramid.views.categories')

View file

@ -31,7 +31,7 @@ from sqlalchemy import and_
from edbob.pyramid.views import SearchableAlchemyGridView
from rattail.pyramid.views import CrudView
from rattail.pyramid.grids import EnumSearchFilter
from rattail.pyramid.grids.search import EnumSearchFilter
from rattail.pyramid.forms import AssociationProxyField, EnumFieldRenderer
from rattail.db.model import (
Employee, EmployeePhoneNumber, EmployeeEmailAddress, Person)

View file

@ -0,0 +1,30 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.views.grids`` -- Grid Views
"""
from rattail.pyramid.views.grids.core import *
from rattail.pyramid.views.grids.alchemy import *

View file

@ -0,0 +1,184 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.views.grids.alchemy`` -- FormAlchemy Grid Views
"""
from webhelpers import paginate
from rattail.pyramid.views.grids.core import GridView
from rattail.pyramid import grids
from rattail.pyramid import Session
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
'PagedAlchemyGridView', 'SearchableAlchemyGridView']
class AlchemyGridView(GridView):
def make_query(self):
q = Session.query(self.mapped_class)
return q
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 grids.util.render_grid(grid)
class SortableAlchemyGridView(AlchemyGridView):
sort = None
@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 make_query(self):
query = Session.query(self.mapped_class)
query = grids.util.sort_query(
query, self._sort_config, self.sort_map(), self.join_map())
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,
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 grids.util.render_grid(grid)
class PagedAlchemyGridView(SortableAlchemyGridView):
full = True
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 grids.util.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 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 make_query(self, session=Session):
join_map = self.join_map()
query = session.query(self.mapped_class)
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
kwargs = self.render_kwargs()
return grids.util.render_grid(grid, search, **kwargs)

View file

@ -0,0 +1,70 @@
#!/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/>.
#
################################################################################
"""
``rattail.pyramid.views.grids.core`` -- Core Grid View
"""
from rattail.pyramid.views import View
from rattail.pyramid import grids
__all__ = ['GridView']
class GridView(View):
route_name = None
route_url = None
renderer = None
permission = None
full = False
checkboxes = False
clickable = False
deletable = False
partial_only = False
def update_grid_kwargs(self, kwargs):
kwargs.setdefault('full', self.full)
kwargs.setdefault('checkboxes', self.checkboxes)
kwargs.setdefault('clickable', self.clickable)
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 __call__(self):
grid = self.grid()
kwargs = self.render_kwargs()
return grids.util.render_grid(grid, **kwargs)