diff --git a/rattail/pyramid/forms/renderers.py b/rattail/pyramid/forms/renderers/__init__.py
similarity index 93%
rename from rattail/pyramid/forms/renderers.py
rename to rattail/pyramid/forms/renderers/__init__.py
index c444e0a9..b1cd38dc 100644
--- a/rattail/pyramid/forms/renderers.py
+++ b/rattail/pyramid/forms/renderers/__init__.py
@@ -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):
diff --git a/rattail/pyramid/forms/renderers/common.py b/rattail/pyramid/forms/renderers/common.py
new file mode 100644
index 00000000..da945b51
--- /dev/null
+++ b/rattail/pyramid/forms/renderers/common.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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
diff --git a/rattail/pyramid/forms/renderers/products.py b/rattail/pyramid/forms/renderers/products.py
new file mode 100644
index 00000000..d17f5177
--- /dev/null
+++ b/rattail/pyramid/forms/renderers/products.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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
diff --git a/rattail/pyramid/forms/renderers/users.py b/rattail/pyramid/forms/renderers/users.py
new file mode 100644
index 00000000..da4603e9
--- /dev/null
+++ b/rattail/pyramid/forms/renderers/users.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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)
diff --git a/rattail/pyramid/grids/__init__.py b/rattail/pyramid/grids/__init__.py
index 38bdc8e2..9c936f2b 100644
--- a/rattail/pyramid/grids/__init__.py
+++ b/rattail/pyramid/grids/__init__.py
@@ -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
diff --git a/rattail/pyramid/grids/alchemy.py b/rattail/pyramid/grids/alchemy.py
new file mode 100644
index 00000000..d6d69bd3
--- /dev/null
+++ b/rattail/pyramid/grids/alchemy.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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
diff --git a/rattail/pyramid/grids/core.py b/rattail/pyramid/grids/core.py
new file mode 100644
index 00000000..d1e05a49
--- /dev/null
+++ b/rattail/pyramid/grids/core.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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
diff --git a/rattail/pyramid/grids/search.py b/rattail/pyramid/grids/search.py
index bb0513ff..359096f5 100644
--- a/rattail/pyramid/grids/search.py
+++ b/rattail/pyramid/grids/search.py
@@ -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
diff --git a/rattail/pyramid/grids/util.py b/rattail/pyramid/grids/util.py
new file mode 100644
index 00000000..8970fdc5
--- /dev/null
+++ b/rattail/pyramid/grids/util.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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)())
diff --git a/rattail/pyramid/views/__init__.py b/rattail/pyramid/views/__init__.py
index e5820a78..2cfdfc5b 100644
--- a/rattail/pyramid/views/__init__.py
+++ b/rattail/pyramid/views/__init__.py
@@ -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')
diff --git a/rattail/pyramid/views/employees.py b/rattail/pyramid/views/employees.py
index 56fad799..a173ac4e 100644
--- a/rattail/pyramid/views/employees.py
+++ b/rattail/pyramid/views/employees.py
@@ -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)
diff --git a/rattail/pyramid/views/grids/__init__.py b/rattail/pyramid/views/grids/__init__.py
new file mode 100644
index 00000000..a6f513fa
--- /dev/null
+++ b/rattail/pyramid/views/grids/__init__.py
@@ -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 .
+#
+################################################################################
+
+"""
+``rattail.pyramid.views.grids`` -- Grid Views
+"""
+
+from rattail.pyramid.views.grids.core import *
+from rattail.pyramid.views.grids.alchemy import *
diff --git a/rattail/pyramid/views/grids/alchemy.py b/rattail/pyramid/views/grids/alchemy.py
new file mode 100644
index 00000000..726e4d69
--- /dev/null
+++ b/rattail/pyramid/views/grids/alchemy.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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)
diff --git a/rattail/pyramid/views/grids/core.py b/rattail/pyramid/views/grids/core.py
new file mode 100644
index 00000000..c119b258
--- /dev/null
+++ b/rattail/pyramid/views/grids/core.py
@@ -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 .
+#
+################################################################################
+
+"""
+``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)