diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py
index 9b02cdc..c0a35ef 100644
--- a/edbob/pyramid/forms/formalchemy/__init__.py
+++ b/edbob/pyramid/forms/formalchemy/__init__.py
@@ -42,15 +42,13 @@ from edbob.pyramid import Session, helpers
from edbob.time import localize
from edbob.pyramid.forms.formalchemy.fieldset import *
-from edbob.pyramid.forms.formalchemy.grid import *
from edbob.pyramid.forms.formalchemy.fields import *
from edbob.pyramid.forms.formalchemy.renderers import *
-__all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField',
- 'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer',
- 'AutocompleteFieldRenderer', 'FieldSet',
- 'make_fieldset', 'required', 'pretty_datetime',
+__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
+ 'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
+ 'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
'AssociationProxyField']
diff --git a/edbob/pyramid/forms/formalchemy/grid.py b/edbob/pyramid/forms/formalchemy/grid.py
deleted file mode 100644
index b217bc3..0000000
--- a/edbob/pyramid/forms/formalchemy/grid.py
+++ /dev/null
@@ -1,227 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-################################################################################
-#
-# edbob -- Pythonic Software Framework
-# Copyright © 2010-2012 Lance Edgar
-#
-# This file is part of edbob.
-#
-# edbob 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.
-#
-# edbob 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 edbob. If not, see .
-#
-################################################################################
-
-"""
-``edbob.pyramid.forms.formalchemy.grid`` -- FormAlchemy Grid
-"""
-
-from webhelpers import paginate
-from webhelpers.html.builder import format_attrs
-from webhelpers.html.tags import literal
-
-import formalchemy
-
-import edbob
-from edbob.util import prettify
-
-
-__all__ = ['AlchemyGrid']
-
-
-class AlchemyGrid(formalchemy.Grid):
- """
- Provides an "enhanced" version of the :class:`formalchemy.Grid` class.
- """
-
- prettify = staticmethod(prettify)
-
- # uuid_key = None
-
- # def __init__(self, cls, instances, config, url_kwargs={}, *args, **kwargs):
- # formalchemy.Grid.__init__(self, cls, instances, *args, **kwargs)
- # self.pager = instances if isinstance(instances, paginate.Page) else None
- # self.config = config
- # self.url_kwargs = url_kwargs
- # self.sortable = config.get('sortable', False)
-
- def __init__(self, cls, instances, config, gridurl=None, objurl=None,
- delurl=None, **kwargs):
- """
- Grid constructor.
-
- ``url`` must be the URL used to access the grid itself. This url/view
- must accept a GET query string parameter of "partial=True", which will
- indicate that the grid *only* is being requested, as opposed to the
- full page in which the grid normally resides.
- """
-
- formalchemy.Grid.__init__(self, cls, instances, **kwargs)
- self.config = config
- self.request = config['request']
- self.gridurl = gridurl
- self.objurl = objurl
- self.delurl = delurl
- self.sortable = config.get('sortable', False)
- self.clickable = config.get('clickable', False)
- self.deletable = config.get('deletable', False)
- self.pager = instances if isinstance(instances, paginate.Page) else None
- self.extra_columns = []
-
- def add_column(self, name, label, callback):
- self.extra_columns.append(
- edbob.Object(name=name, label=label, callback=callback))
-
- def field_name(self, field):
- return field.name
-
- def iter_fields(self):
- for field in self.render_fields.itervalues():
- yield field
-
- def render_field(self, field, readonly):
- if readonly:
- return field.render_readonly()
- return field.render()
-
- def row_attrs(self, i):
- attrs = dict(class_='even' if i % 2 else 'odd')
- if hasattr(self.model, 'uuid'):
- attrs['uuid'] = self.model.uuid
- return format_attrs(**attrs)
-
- def url_attrs(self):
- attrs = {}
- url = self.request.route_url
- if self.gridurl:
- attrs['url'] = self.gridurl
- if self.objurl:
- attrs['objurl'] = url(self.objurl, uuid='{uuid}')
- if self.delurl:
- attrs['delurl'] = url(self.delurl, uuid='{uuid}')
- return format_attrs(**attrs)
-
- # def render(self, class_=None, **kwargs):
- # """
- # Renders the grid into HTML, and returns the result.
-
- # ``class_`` (if provided) is used to define the class of the ``
``
- # (wrapper) and ``
`` elements of the grid.
-
- # Any remaining ``kwargs`` are passed directly to the underlying
- # ``formalchemy.Grid.render()`` method.
- # """
-
- # kwargs['class_'] = class_
- # # kwargs.setdefault('get_uuid', self.get_uuid)
- # kwargs.setdefault('checkboxes', False)
- # return formalchemy.Grid.render(self, **kwargs)
-
- def render(self, **kwargs):
- engine = self.engine or formalchemy.config.engine
- if self.readonly:
- return engine('grid_readonly', grid=self, **kwargs)
- kwargs.setdefault('request', self._request)
- return engine('grid', grid=self, **kwargs)
-
- def th_sortable(self, field):
- class_ = ''
- label = field.label()
- if self.sortable and field.key in self.config.get('sort_map', {}):
- class_ = 'sortable'
- if field.key == self.config['sort']:
- class_ += ' sorted ' + self.config['dir']
- label = literal('') + label + literal(' ')
- if class_:
- class_ = ' class="%s"' % class_
- return literal('') + label + literal(' ')
-
- # def url(self):
- # # TODO: Probably clean this up somehow...
- # if self.pager is not None:
- # u = self.pager._url_generator(self.pager.page, partial=True)
- # else:
- # u = self._url or ''
- # qs = self.query_string()
- # if qs:
- # if '?' not in u:
- # u += '?'
- # u += qs
- # elif '?' not in u:
- # u += '?partial=True'
- # return u
-
- # def query_string(self):
- # # TODO: Probably clean this up somehow...
- # qs = ''
- # if self.url_kwargs:
- # for k, v in self.url_kwargs.items():
- # qs += '&%s=%s' % (urllib.quote_plus(k), urllib.quote_plus(v))
- # return qs
-
- def get_actions(self):
- """
- Returns an HTML snippet containing ```` elements for each "action"
- defined in the grid.
- """
-
- def get_class(text):
- return text.lower().replace(' ', '-')
-
- res = ''
- for action in self.config['actions']:
- if isinstance(action, basestring):
- text = action
- cls = get_class(text)
- else:
- text = action[0]
- if len(action) == 2:
- cls = action[1]
- else:
- cls = get_class(text)
- res += literal(
- ' %s ' %
- (cls, text))
- return res
-
- # def get_uuid(self):
- # """
- # .. highlight:: none
-
- # Returns a unique identifier for a given record, in the form of an HTML
- # attribute for direct inclusion in a ```` element within a template.
- # An example of what this function might return would be the string::
-
- # 'uuid="420"'
-
- # Rattail itself will tend to use *universally-unique* IDs (true UUIDs),
- # but this method may be overridden to support legacy databases with
- # auto-increment IDs, etc. Really the only important thing is that the
- # value returned be unique across the relevant data set.
-
- # If the concept is unsupported, the method should return an empty
- # string.
- # """
-
- # def uuid():
- # if self.uuid_key and hasattr(self.model, self.uuid_key):
- # return getattr(self.model, self.uuid_key)
- # if hasattr(self.model, 'uuid'):
- # return getattr(self.model, 'uuid')
- # if hasattr(self.model, 'id'):
- # return getattr(self.model, 'id')
-
- # uuid = uuid()
- # if uuid:
- # return literal('uuid="%s"' % uuid)
- # return ''
diff --git a/edbob/pyramid/grids.py b/edbob/pyramid/grids.py
deleted file mode 100644
index 824fde2..0000000
--- a/edbob/pyramid/grids.py
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-################################################################################
-#
-# edbob -- Pythonic Software Framework
-# Copyright © 2010-2012 Lance Edgar
-#
-# This file is part of edbob.
-#
-# edbob 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.
-#
-# edbob 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 edbob. If not, see .
-#
-################################################################################
-
-"""
-``edbob.pyramid.grids`` -- Grid Tables
-"""
-
-try:
- from collections import OrderedDict
-except ImportError:
- from ordereddict import OrderedDict
-
-from sqlalchemy.orm import Query
-from sqlalchemy.orm.attributes import InstrumentedAttribute
-
-from pyramid.renderers import render
-from pyramid.response import Response
-from webhelpers import paginate
-from webhelpers.html import literal
-from webhelpers.html.builder import format_attrs
-
-import edbob
-from edbob.pyramid.filters import SearchFormRenderer
-from edbob.util import prettify
-
-
-class BasicGrid(edbob.Object):
- """
- Basic grid class for those times when SQLAlchemy is not needed.
- """
-
- def __init__(self, columns, rows, config, url, sortable=True, deletable=False, **kwargs):
- edbob.Object.__init__(self, **kwargs)
- self.rows = rows
- self.config = config
- self.url = url
- self.sortable = sortable
- self.deletable = deletable
- self.columns = OrderedDict()
- for col in columns:
- if isinstance(col, (tuple, list)):
- if len(col) == 2:
- self.columns[col[0]] = col[1]
- continue
- elif isinstance(col, basestring):
- self.columns[col] = prettify(col)
- continue
- raise ValueError("Column element must be either a string or 2-tuple")
-
- def _set_active(self, row):
- self.model = {}
- for i, col in enumerate(self.columns.keys()):
- if i >= len(row):
- break
- self.model[col] = row[i]
-
- def field_label(self, name):
- return self.columns[name]
-
- def field_name(self, field):
- return field
-
- def iter_fields(self):
- for col in self.columns.keys():
- yield col
-
- def render(self, **kwargs):
- kwargs['grid'] = self
- return render('forms/grid_readonly.mako', kwargs)
-
- def render_field(self, field, readonly):
- return self.model[field]
-
- def row_attrs(self, i):
- return format_attrs(class_='even' if i % 2 else 'odd')
-
- def th_sortable(self, field):
- class_ = ''
- label = self.field_label(field)
- if self.sortable and field in self.config.get('sort_map', {}):
- class_ = 'sortable'
- if field == self.config['sort']:
- class_ += ' sorted ' + self.config['dir']
- label = literal('') + label + literal(' ')
- if class_:
- class_ = ' class="%s"' % class_
- return literal('') + label + literal(' ')
-
- def url_attrs(self):
- return format_attrs(url=self.url)
-
-
-def get_grid_config(name, request, search=None, url=None, **kwargs):
- config = {
- 'actions': [],
- 'per_page': 20,
- 'page': 1,
- 'sortable': True,
- 'dir': 'asc',
- 'object_url': '',
- 'deletable': False,
- 'delete_url': '',
- 'use_dialog': False,
- }
- config.update(kwargs)
- # words = name.split('.')
- # if len(words) == 2:
- # config.setdefault('object_url', request.route_url(words[0], action='crud'))
- # config.setdefault('delete_url', config['object_url'])
- 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
- config['request'] = request
- config['search'] = search
- config['url'] = url
- return config
-
-
-def get_pager(query, config):
- query = query(config)
- count = None
- if isinstance(query, Query):
- count = query.count()
- return paginate.Page(
- query, item_count=count,
- items_per_page=int(config['per_page']),
- page=int(config['page']),
- url=paginate.PageURL(config['url'], {}),
- )
-
-
-def get_sort_map(cls, names=None, **kwargs):
- """
- Convenience function which returns a sort map.
- """
-
- 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(request, grid, search=None, **kwargs):
- if request.params.get('partial'):
- return Response(body=grid, content_type='text/html')
- kwargs['grid'] = grid
- if search:
- kwargs['search'] = SearchFormRenderer(search)
- return kwargs
-
-
-def sort_query(query, config, sort_map, join_map={}):
- 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)
- config['sort_map'] = sort_map
- return sort_map[field](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/edbob/pyramid/grids/__init__.py b/edbob/pyramid/grids/__init__.py
new file mode 100644
index 0000000..8593f3b
--- /dev/null
+++ b/edbob/pyramid/grids/__init__.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.grids`` -- Grids
+"""
+
+from edbob.pyramid.grids.core import *
+from edbob.pyramid.grids.alchemy import *
+from edbob.pyramid.grids import util
+from edbob.pyramid.grids import search
diff --git a/edbob/pyramid/grids/alchemy.py b/edbob/pyramid/grids/alchemy.py
new file mode 100644
index 0000000..9b586f8
--- /dev/null
+++ b/edbob/pyramid/grids/alchemy.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes
+"""
+
+from webhelpers.html import literal
+from webhelpers.html import tags
+
+import formalchemy
+
+import edbob
+from edbob.pyramid import Session
+from edbob.pyramid.grids.core import Grid
+from edbob.util import prettify
+
+
+__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
+
+ def __getattr__(self, attr):
+ return getattr(self._formalchemy_grid, attr)
+
+ def column_header(self, field):
+ cls = ''
+ label = field.label()
+ if field.key in self.sort_map:
+ cls = 'sortable'
+ if field.key == self.config['sort']:
+ cls += ' sorted ' + self.config['dir']
+ label = tags.link_to(label, '#')
+ if cls:
+ cls = ' class="%s"' % cls
+ return literal('' % (cls, field.key)) + label + literal(' ')
+
+ 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/edbob/pyramid/grids/core.py b/edbob/pyramid/grids/core.py
new file mode 100644
index 0000000..651cc59
--- /dev/null
+++ b/edbob/pyramid/grids/core.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.grids.core`` -- Core Grid Classes
+"""
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ from ordereddict import OrderedDict
+
+from webhelpers.html import literal
+from webhelpers.html.builder import format_attrs
+
+from pyramid.renderers import render
+
+import edbob
+
+
+__all__ = ['Grid']
+
+
+class Grid(edbob.Object):
+
+ hoverable = True
+ clickable = False
+ checkboxes = False
+ deletable = False
+ partial_only = False
+
+ def __init__(self, request, **kwargs):
+ kwargs.setdefault('fields', OrderedDict())
+ kwargs.setdefault('extra_columns', [])
+ super(Grid, self).__init__(**kwargs)
+ self.request = request
+
+ def column_header(self, field):
+ return literal('%s ' % (field.name, field.label))
+
+ def div_class(self):
+ if self.clickable:
+ return 'grid clickable'
+ if self.hoverable:
+ return 'grid hoverable'
+ return 'grid'
+
+ def _div_attrs(self):
+ attrs = {'class_':'grid', 'url':self.request.current_route_url()}
+ if self.clickable:
+ attrs['class_'] = 'grid clickable'
+ elif self.hoverable:
+ attrs['class_'] = 'grid hoverable'
+ return attrs
+
+ def div_attrs(self):
+ return format_attrs(**self._div_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
+
+ def row_attrs(self, row, i):
+ return format_attrs(**self._row_attrs(row, i))
diff --git a/edbob/pyramid/grids/search.py b/edbob/pyramid/grids/search.py
new file mode 100644
index 0000000..9477a6d
--- /dev/null
+++ b/edbob/pyramid/grids/search.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.grids.search`` -- Grid Search Filters
+"""
+
+import re
+
+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
+
+import edbob
+from edbob.util import prettify
+
+
+class SearchFilter(edbob.Object):
+ """
+ Base class and default implementation for search filters.
+ """
+
+ def __init__(self, name, label=None, **kwargs):
+ edbob.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 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``.
+ """
+
+ return {
+ 'lk':
+ lambda q, v: q.filter(field.ilike('%%%s%%' % v)) if v else q,
+ 'nl':
+ lambda q, v: q.filter(~field.ilike('%%%s%%' % v)) if v else q,
+ }
+
+
+def get_filter_config(name, 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=name+'.')
+
+ # 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[name+'.'+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, join_map={}):
+# filter_map = config['filter_map']
+# if config.get('search'):
+# search = config['search'].config
+# joins = config.setdefault('joins', [])
+# include_filter = re.compile(r'^include_filter_(.*)$')
+# for key in search:
+# m = include_filter.match(key)
+# if m and search[key]:
+# field = m.group(1)
+# if field in join_map and field not in joins:
+# query = join_map[field](query)
+# joins.append(field)
+# value = search.get(field)
+# if value:
+# f = filter_map[field][search['filter_type_'+field]]
+# query = f(query, value)
+# return query
+
+
+def filter_query(query, config, filter_map, join_map):
+ joins = config.setdefault('joins', [])
+ include_filter = re.compile(r'^include_filter_(.*)$')
+ for key in config:
+ m = include_filter.match(key)
+ if m and config[key]:
+ field = m.group(1)
+ if field in join_map and field not in joins:
+ query = join_map[field](query)
+ joins.append(field)
+ value = config.get(field)
+ if value:
+ f = filter_map[field][config['filter_type_'+field]]
+ query = f(query, value)
+ return query
diff --git a/edbob/pyramid/grids/util.py b/edbob/pyramid/grids/util.py
new file mode 100644
index 0000000..1911135
--- /dev/null
+++ b/edbob/pyramid/grids/util.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.grids.util`` -- Grid Utilities
+"""
+
+from sqlalchemy.orm.attributes import InstrumentedAttribute
+
+from webhelpers.html import literal
+
+from pyramid.response import Response
+
+from edbob.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/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/edbob.css
index 1efb683..c17d99a 100644
--- a/edbob/pyramid/static/css/edbob.css
+++ b/edbob/pyramid/static/css/edbob.css
@@ -181,6 +181,15 @@ div.dialog {
}
+/******************************
+ * Filters
+ ******************************/
+
+div.filters div.filter div.value {
+ display: inline;
+}
+
+
/******************************
* Grids
******************************/
@@ -228,7 +237,7 @@ div.grid table tbody td {
text-align: left;
}
-div.grid table tr.even {
+div.grid table tr.odd {
background-color: #e0e0e0;
}
@@ -287,7 +296,6 @@ div.pager p.showing {
div.pager #grid-page-count {
font-size: 8pt;
- height: 21px;
}
div.pager p.page-links {
diff --git a/edbob/pyramid/static/css/index.css b/edbob/pyramid/static/css/index.css
index 0092b5e..8beaa9b 100644
--- a/edbob/pyramid/static/css/index.css
+++ b/edbob/pyramid/static/css/index.css
@@ -38,16 +38,16 @@ div.object-index table.header td.context-menu ul {
* Filters
******************************/
-div.object-index div.filterset div.filter {
+div.object-index div.filters div.filter {
margin-bottom: 10px;
}
-div.object-index div.filterset div.filter label,
-div.object-index div.filterset div.filter select.filter-type {
+div.object-index div.filters div.filter label,
+div.object-index div.filters div.filter select.filter-type {
margin-right: 8px;
}
-div.object-index div.filterset div.buttons * {
+div.object-index div.filters div.buttons * {
margin-right: 8px;
}
diff --git a/edbob/pyramid/static/js/edbob.js b/edbob/pyramid/static/js/edbob.js
index d05d9e7..e9add59 100644
--- a/edbob/pyramid/static/js/edbob.js
+++ b/edbob/pyramid/static/js/edbob.js
@@ -1,15 +1,15 @@
/************************************************************
-*
-* edbob.js
-*
-* This library contains all of Javascript functionality
-* provided directly by edbob.
-*
-* It also attaches some jQuery event handlers for certain
-* design patterns.
-*
-************************************************************/
+ *
+ * edbob.js
+ *
+ * This library contains all of Javascript functionality
+ * provided directly by edbob.
+ *
+ * It also attaches some jQuery event handlers for certain
+ * design patterns.
+ *
+ ************************************************************/
var filters_to_disable = [];
@@ -23,12 +23,11 @@ function disable_button(button, text) {
function disable_filter_options() {
for (var i = 0; i <= filters_to_disable.length; ++i) {
- var filter = filters_to_disable.pop();
- var option = $('#add-filter option[value='+filter+']').attr('disabled', true);
+ var filter = filters_to_disable.pop();
+ var option = $('#add-filter option[value='+filter+']').attr('disabled', true);
}
}
-
/*
* get_dialog(id, callback)
*
@@ -46,10 +45,10 @@ function disable_filter_options() {
function get_dialog(id, callback) {
var dialog = $('#'+id+'-dialog');
if (! dialog.length) {
- dialog = $('
');
+ dialog = $('
');
}
if (callback) {
- dialog.attr('callback', callback);
+ dialog.attr('callback', callback);
}
return dialog;
}
@@ -80,11 +79,11 @@ function get_uuid(obj) {
obj = $(obj);
if (obj.attr('uuid')) {
- return obj.attr('uuid');
+ return obj.attr('uuid');
}
var tr = obj.parents('tr:first');
if (tr.attr('uuid')) {
- return tr.attr('uuid');
+ return tr.attr('uuid');
}
return undefined;
}
@@ -115,20 +114,13 @@ function loading(element) {
/*
- * grid_navigate_page(link)
- *
- * Navigates to another page of results within the grid.
+ * Navigates to another page of results within a grid.
*/
-function grid_navigate_page(link) {
- var page = link.attr('href').replace(/^.*page=/, '');
- var div = link.parents('div.grid:first');
+function grid_navigate_page(link, url) {
+ var div = $(link).parents('div.grid:first');
loading(div);
- div.load(div.attr('url'), {
- 'page': page,
- 'partial': true,
- });
- return false;
+ div.load(url);
}
@@ -141,13 +133,13 @@ function grid_navigate_page(link) {
function reload_grid_div(div) {
if (! div) {
- div = $('div.grid');
+ div = $('div.grid');
} else if (! div.hasClass('grid')) {
- div = div.find('div.grid');
+ div = div.find('div.grid');
}
if (! div.length) {
- alert('assert: div should have length');
- return;
+ alert('assert: div should have length');
+ return;
}
loading(div);
div.load(div.attr('url'));
@@ -157,213 +149,234 @@ function reload_grid_div(div) {
$(function() {
$('div.filter label').live('click', function() {
- var checkbox = $(this).prev();
- if (checkbox.attr('checked')) {
- checkbox.attr('checked', false);
- return false;
- }
- checkbox.attr('checked', true);
- return true;
+ var checkbox = $(this).prev();
+ if (checkbox.attr('checked')) {
+ checkbox.attr('checked', false);
+ return false;
+ }
+ checkbox.attr('checked', true);
+ return true;
});
$('#add-filter').live('change', function() {
- var div = $(this).parents('div.filterset:first');
- var filter = div.find('#filter-'+$(this).val());
- filter.find(':first-child').attr('checked', true);
- filter.show();
- var field = filter.find(':last-child');
- field.select();
- field.focus();
- $(this).find('option:selected').attr('disabled', true);
- $(this).val('add a filter');
- if ($(this).find('option[disabled=false]').length == 1) {
- $(this).hide();
- }
- div.find('input[type=submit]').show();
- div.find('button[type=reset]').show();
+ var div = $(this).parents('div.filters:first');
+ var filter = div.find('#filter-'+$(this).val());
+ filter.find(':first-child').attr('checked', true);
+ filter.show();
+ var field = filter.find(':last-child');
+ field.select();
+ field.focus();
+ $(this).find('option:selected').attr('disabled', true);
+ $(this).val('add a filter');
+ if ($(this).find('option[disabled=false]').length == 1) {
+ $(this).hide();
+ }
+ div.find('input[type=submit]').show();
+ div.find('button[type=reset]').show();
});
- $('div.filterset form').live('submit', function() {
- var div = $('div.grid:first');
- var data = $(this).serialize() + '&partial=true';
- loading(div);
- $.post(div.attr('url'), data, function(data) {
- div.replaceWith(data);
- });
- return false;
+ $('div.filters form').live('submit', function() {
+ var div = $('div.grid:first');
+ var data = $(this).serialize() + '&partial=true';
+ loading(div);
+ $.post(div.attr('url'), data, function(data) {
+ div.replaceWith(data);
+ });
+ return false;
+ });
+
+ $('div.filters form div.buttons button[type=reset]').click(function() {
+ var filters = $(this).parents('div.filters:first');
+ filters.find('div.filter').each(function() {
+ $(this).find('div.value input').val('');
+ });
+ var url = filters.attr('url');
+ var grid = $('div.grid[url='+url+']');
+ loading(grid);
+ var form = filters.find('form');
+ var data = form.serialize() + '&partial=true';
+ $.post(url, data, function(data) {
+ grid.replaceWith(data);
+ });
+ return false;
});
$('div.grid table th.sortable a').live('click', function() {
- var div = $(this).parents('div.grid:first');
- var th = $(this).parents('th:first');
- var dir = 'asc';
- if (th.hasClass('sorted') && th.hasClass('asc')) {
- dir = 'desc';
- }
- loading(div);
- var url = div.attr('url');
- url += url.match(/\?/) ? '&' : '?';
- url += 'sort=' + th.attr('field') + '&dir=' + dir;
- url += '&partial=true';
- div.load(url);
- return false;
+ var div = $(this).parents('div.grid:first');
+ var th = $(this).parents('th:first');
+ var dir = 'asc';
+ if (th.hasClass('sorted') && th.hasClass('asc')) {
+ dir = 'desc';
+ }
+ loading(div);
+ var url = div.attr('url');
+ url += url.match(/\?/) ? '&' : '?';
+ url += 'sort=' + th.attr('field') + '&dir=' + dir;
+ url += '&page=1';
+ url += '&partial=true';
+ div.load(url);
+ return false;
});
$('div.grid.hoverable table tbody tr').live('mouseenter', function() {
- $(this).addClass('hovering');
+ $(this).addClass('hovering');
});
$('div.grid.hoverable table tbody tr').live('mouseleave', function() {
- $(this).removeClass('hovering');
+ $(this).removeClass('hovering');
});
$('div.grid.clickable table tbody tr').live('mouseenter', function() {
- $(this).addClass('hovering');
+ $(this).addClass('hovering');
});
$('div.grid.clickable table tbody tr').live('mouseleave', function() {
- $(this).removeClass('hovering');
+ $(this).removeClass('hovering');
});
$('div.grid.selectable table tbody tr').live('mouseenter', function() {
- $(this).addClass('hovering');
+ $(this).addClass('hovering');
});
$('div.grid.selectable table tbody tr').live('mouseleave', function() {
- $(this).removeClass('hovering');
+ $(this).removeClass('hovering');
});
$('div.grid.checkable table tbody tr').live('mouseenter', function() {
- $(this).addClass('hovering');
+ $(this).addClass('hovering');
});
$('div.grid.checkable table tbody tr').live('mouseleave', function() {
- $(this).removeClass('hovering');
+ $(this).removeClass('hovering');
});
$('div.grid.clickable table tbody tr').live('click', function() {
- var div = $(this).parents('div.grid:first');
- if (div.attr('usedlg') == 'True') {
- var dlg = get_dialog('grid-object');
- var data = {
- 'uuid': get_uuid(this),
- 'partial': true,
- };
- dlg.load(div.attr('objurl'), data, function() {
- dlg.dialog({
- width: 500,
- height: 450,
- });
- });
- } else {
- location.href = div.attr('objurl').replace(/%7Buuid%7D/, get_uuid(this));
- }
+ var div = $(this).parents('div.grid:first');
+ if (div.attr('usedlg') == 'True') {
+ var dlg = get_dialog('grid-object');
+ var data = {
+ 'uuid': get_uuid(this),
+ 'partial': true,
+ };
+ dlg.load(div.attr('objurl'), data, function() {
+ dlg.dialog({
+ width: 500,
+ height: 450,
+ });
+ });
+ } else {
+ location.href = div.attr('objurl').replace(/%7Buuid%7D/, get_uuid(this));
+ }
});
$('div.grid.checkable table thead th.checkbox input[type=checkbox]').live('click', function() {
- var checked = $(this).is(':checked');
- var table = $(this).parents('table:first');
- table.find('tbody tr').each(function() {
- $(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
- if (checked) {
- $(this).addClass('selected');
- } else {
- $(this).removeClass('selected');
- }
- });
+ var checked = $(this).is(':checked');
+ var table = $(this).parents('table:first');
+ table.find('tbody tr').each(function() {
+ $(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
+ if (checked) {
+ $(this).addClass('selected');
+ } else {
+ $(this).removeClass('selected');
+ }
+ });
});
$('div.grid.selectable table tbody tr').live('click', function() {
- var table = $(this).parents('table:first');
- if (! table.hasClass('multiple')) {
- table.find('tbody tr').removeClass('selected');
- }
- $(this).addClass('selected');
+ var table = $(this).parents('table:first');
+ if (! table.hasClass('multiple')) {
+ table.find('tbody tr').removeClass('selected');
+ }
+ $(this).addClass('selected');
});
$('div.grid.checkable table tbody tr').live('click', function() {
- var checkbox = $(this).find('td:first input[type=checkbox]');
- checkbox.attr('checked', !checkbox.is(':checked'));
- $(this).toggleClass('selected');
+ var checkbox = $(this).find('td:first input[type=checkbox]');
+ checkbox.attr('checked', !checkbox.is(':checked'));
+ $(this).toggleClass('selected');
});
$('div.grid td.delete').live('click', function() {
- var grid = $(this).parents('div.grid:first');
- var url = grid.attr('delurl');
- if (url) {
- if (confirm("Do you really wish to delete this object?")) {
- location.href = url.replace(/%7Buuid%7D/, get_uuid(this));
- }
- } else {
- alert("Hm, I don't know how to delete that..\n\n"
- + "(Add a 'delurl' parameter to the AlchemyGrid instance.)");
- }
- return false;
+ var grid = $(this).parents('div.grid:first');
+ var url = grid.attr('delurl');
+ if (url) {
+ if (confirm("Do you really wish to delete this object?")) {
+ location.href = url.replace(/%7Buuid%7D/, get_uuid(this));
+ }
+ } else {
+ alert("Hm, I don't know how to delete that..\n\n"
+ + "(Add a 'delurl' parameter to the AlchemyGrid instance.)");
+ }
+ return false;
});
$('#grid-page-count').live('change', function() {
- var div = $(this).parents('div.grid:first');
- loading(div);
- div.load(div.attr('url') + '&per_page=' + $(this).val());
+ var div = $(this).parents('div.grid:first');
+ loading(div);
+ url = div.attr('url');
+ url += url.match(/\?/) ? '&' : '?';
+ url += 'per_page=' + $(this).val();
+ url += '&partial=true';
+ div.load(url);
});
$('button.autocomplete-change').live('click', function() {
- var container = $(this).parents('div.autocomplete-container:first');
- container.find('div.autocomplete-display').hide();
- var textbox = container.find('input.autocomplete-textbox');
- textbox.show();
- textbox.select();
- textbox.focus();
+ var container = $(this).parents('div.autocomplete-container:first');
+ container.find('div.autocomplete-display').hide();
+ var textbox = container.find('input.autocomplete-textbox');
+ textbox.show();
+ textbox.select();
+ textbox.focus();
});
$('div.dialog form').live('submit', function() {
- var form = $(this);
- var dialog = form.parents('div.dialog:first');
- $.ajax({
- type: 'POST',
- url: form.attr('action'),
- data: form.serialize(),
- success: function(data) {
- if (json_success(data)) {
- if (dialog.attr('callback')) {
- eval(dialog.attr('callback'))(data);
- }
- dialog.dialog('close');
- } else if (typeof(data) == 'object') {
- alert(data.message);
- } else {
- dialog.html(data);
- }
- },
- error: function() {
- alert("Sorry, something went wrong...try again?");
- },
- });
- return false;
+ var form = $(this);
+ var dialog = form.parents('div.dialog:first');
+ $.ajax({
+ type: 'POST',
+ url: form.attr('action'),
+ data: form.serialize(),
+ success: function(data) {
+ if (json_success(data)) {
+ if (dialog.attr('callback')) {
+ eval(dialog.attr('callback'))(data);
+ }
+ dialog.dialog('close');
+ } else if (typeof(data) == 'object') {
+ alert(data.message);
+ } else {
+ dialog.html(data);
+ }
+ },
+ error: function() {
+ alert("Sorry, something went wrong...try again?");
+ },
+ });
+ return false;
});
$('div.dialog button.close').live('click', function() {
- var dialog = $(this).parents('div.dialog:first');
- dialog.dialog('close');
+ var dialog = $(this).parents('div.dialog:first');
+ dialog.dialog('close');
});
$('div.dialog button.cancel').live('click', function() {
- var dialog = $(this).parents('div.dialog:first');
- dialog.dialog('close');
+ var dialog = $(this).parents('div.dialog:first');
+ dialog.dialog('close');
});
$('div.dialog.lookup button.ok').live('click', function() {
- var dialog = $(this).parents('div.dialog.lookup:first');
- var tr = dialog.find('div.grid table tbody tr.selected');
- if (! tr.length) {
- alert("You haven't selected anything.");
- return false;
- }
- var uuid = get_uuid(tr);
- var col = parseInt(dialog.attr('textcol'));
- var text = tr.find('td:eq('+col+')').html();
- eval(dialog.attr('callback'))(uuid, text);
- dialog.dialog('close');
+ var dialog = $(this).parents('div.dialog.lookup:first');
+ var tr = dialog.find('div.grid table tbody tr.selected');
+ if (! tr.length) {
+ alert("You haven't selected anything.");
+ return false;
+ }
+ var uuid = get_uuid(tr);
+ var col = parseInt(dialog.attr('textcol'));
+ var text = tr.find('td:eq('+col+')').html();
+ eval(dialog.attr('callback'))(uuid, text);
+ dialog.dialog('close');
});
});
diff --git a/edbob/pyramid/templates/edbob/index.mako b/edbob/pyramid/templates/edbob/index.mako
index 44d1af7..2aeae65 100644
--- a/edbob/pyramid/templates/edbob/index.mako
+++ b/edbob/pyramid/templates/edbob/index.mako
@@ -12,22 +12,26 @@
- ${grid|n}
+ ${grid}
diff --git a/edbob/pyramid/templates/forms/grid_readonly.mako b/edbob/pyramid/templates/forms/grid_readonly.mako
index 72920ca..fab23d9 100644
--- a/edbob/pyramid/templates/forms/grid_readonly.mako
+++ b/edbob/pyramid/templates/forms/grid_readonly.mako
@@ -7,7 +7,7 @@
${h.checkbox('check-all')}
% endif
% for field in grid.iter_fields():
- ${grid.th_sortable(field)|n}
+ ${grid.column_header(field)}
% endfor
% for col in grid.extra_columns:
${col.label}
diff --git a/edbob/pyramid/templates/grids/grid.mako b/edbob/pyramid/templates/grids/grid.mako
new file mode 100644
index 0000000..44ee62e
--- /dev/null
+++ b/edbob/pyramid/templates/grids/grid.mako
@@ -0,0 +1,50 @@
+
+
+
+
+ % if checkboxes:
+ ${h.checkbox('check-all')}
+ % endif
+ % for field in grid.iter_fields():
+ ${grid.column_header(field)}
+ % endfor
+ % for col in grid.extra_columns:
+ ${col.label}
+ % endfor
+ % if grid.deletable:
+
+ % endif
+
+
+
+ % for i, row in enumerate(grid.iter_rows(), 1):
+
+ % if grid.checkboxes:
+ ${grid.checkbox(row)}
+ % endif
+ % for field in grid.iter_fields():
+ ${grid.render_field(field)}
+ % endfor
+ % for col in grid.extra_columns:
+ ${col.callback(row)}
+ % endfor
+ % if grid.deletable:
+
+ % endif
+
+ % endfor
+
+
+ % if grid.pager:
+
+ % endif
+
diff --git a/edbob/pyramid/templates/grids/search.mako b/edbob/pyramid/templates/grids/search.mako
new file mode 100644
index 0000000..17fbabd
--- /dev/null
+++ b/edbob/pyramid/templates/grids/search.mako
@@ -0,0 +1,36 @@
+
+ ${search.begin()}
+ ${search.hidden('filters', 'true')}
+ <% visible = [] %>
+ % for f in search.sorted_filters():
+
+ ${search.checkbox('include_filter_'+f.name)}
+
${f.label}
+ ${f.types_select()}
+
+ ${f.value_control()}
+
+
+ % if search.config.get('include_filter_'+f.name):
+ <% visible.append(f.name) %>
+ % endif
+ % endfor
+
+ ${search.add_filter(visible)}
+ ${search.submit('submit', "Search", style='display: none;' if not visible else None)}
+ Reset
+
+ ${search.end()}
+ % if visible:
+
+ % endif
+
diff --git a/edbob/pyramid/views/__init__.py b/edbob/pyramid/views/__init__.py
index 1fed843..f51f74c 100644
--- a/edbob/pyramid/views/__init__.py
+++ b/edbob/pyramid/views/__init__.py
@@ -32,9 +32,10 @@ from pyramid.security import authenticated_userid
from webhelpers.html import literal
from webhelpers.html.tags import link_to
+from edbob.pyramid.views.core import *
from edbob.pyramid.views.autocomplete import *
from edbob.pyramid.views.form import *
-from edbob.pyramid.views.grid import *
+from edbob.pyramid.views.grids import *
def forbidden(request):
diff --git a/edbob/pyramid/views/core.py b/edbob/pyramid/views/core.py
new file mode 100644
index 0000000..8171679
--- /dev/null
+++ b/edbob/pyramid/views/core.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.views.core`` -- Core Views
+"""
+
+
+__all__ = ['View']
+
+
+class View(object):
+
+ def __init__(self, request):
+ self.request = request
diff --git a/edbob/pyramid/views/grid.py b/edbob/pyramid/views/grid.py
deleted file mode 100644
index 2dabe18..0000000
--- a/edbob/pyramid/views/grid.py
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-################################################################################
-#
-# edbob -- Pythonic Software Framework
-# Copyright © 2010-2012 Lance Edgar
-#
-# This file is part of edbob.
-#
-# edbob 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.
-#
-# edbob 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 edbob. If not, see .
-#
-################################################################################
-
-"""
-``edbob.pyramid.views.grid`` -- Base Grid View
-"""
-
-from edbob.pyramid import filters
-from edbob.pyramid import forms
-from edbob.pyramid import grids
-from edbob.pyramid import Session
-from edbob.util import requires_impl
-
-
-__all__ = ['GridView']
-
-
-class GridView(object):
-
- @property
- @requires_impl(is_property=True)
- def mapped_class(self):
- raise NotImplementedError
-
- @property
- @requires_impl(is_property=True)
- def route_name(self):
- raise NotImplementedError
-
- @property
- @requires_impl(is_property=True)
- def route_prefix(self):
- raise NotImplementedError
-
- def __init__(self, request):
- self.request = request
-
- def join_map(self):
- return {}
-
- def make_filter_map(self, **kwargs):
- return filters.get_filter_map(self.mapped_class, **kwargs)
-
- def filter_map(self):
- return self.make_filter_map()
-
- def make_search_config(self, fmap, **kwargs):
- return filters.get_search_config(self.route_name, self.request, fmap,
- **kwargs)
-
- def search_config(self, fmap):
- return self.make_search_config(fmap)
-
- def make_search_form(self, config, **labels):
- return filters.get_search_form(config, **labels)
-
- def search_form(self, config):
- return self.make_search_form(config)
-
- def make_sort_map(self, *args, **kwargs):
- return grids.get_sort_map(self.mapped_class, names=args or None, **kwargs)
-
- def sort_map(self):
- return self.make_sort_map()
-
- def make_grid_config(self, search, fmap, **kwargs):
- return grids.get_grid_config(
- self.route_name, self.request,
- search, filter_map=fmap, **kwargs)
-
- def grid_config(self, search, fmap):
- return self.make_grid_config(search, fmap)
-
- def filter_query(self, q):
- return q
-
- def make_query(self, config, jmap=None):
- if jmap is None:
- jmap = self.join_map()
- smap = self.sort_map()
- q = Session.query(self.mapped_class)
- q = self.filter_query(q)
- q = filters.filter_query(q, config, jmap)
- q = grids.sort_query(q, config, smap, jmap)
- return q
-
- def query(self, config):
- return self.make_query(config)
-
- def make_grid(self, data, config, gridurl=None, objurl=None, delurl=None):
- if not gridurl:
- gridurl = self.request.route_url(self.route_name)
- if not objurl:
- objurl = '%s.edit' % self.route_prefix
- if not delurl:
- delurl = '%s.delete' % self.route_prefix
- g = forms.AlchemyGrid(
- self.mapped_class, data, config,
- gridurl=gridurl, objurl=objurl, delurl=delurl)
- return g
-
- def grid(self, data, config):
- g = self.make_grid(data, config)
- g.configure(readonly=True)
- return g
-
- def __call__(self):
- """
- View callable method.
- """
-
- fmap = self.filter_map()
- config = self.search_config(fmap)
- search = self.search_form(config)
- config = self.grid_config(search, fmap)
- grid = grids.get_pager(self.query, config)
-
- g = self.grid(grid, config)
- cls = self.mapped_class.__name__
- if g.clickable:
- cls = 'clickable %s' % cls
- else:
- cls = 'hoverable %s' % cls
- grid = g.render(class_=cls)
- return grids.render_grid(self.request, grid, search)
-
- @classmethod
- def add_route(cls, config, route_name, url_prefix, template_prefix=None, permission=None):
- if not template_prefix:
- template_prefix = url_prefix
- if not permission:
- permission = route_name
- config.add_route(route_name, url_prefix)
- config.add_view(cls, route_name=route_name, renderer='%s/index.mako' % template_prefix,
- permission=permission, http_cache=0)
diff --git a/edbob/pyramid/views/grids/__init__.py b/edbob/pyramid/views/grids/__init__.py
new file mode 100644
index 0000000..5e98506
--- /dev/null
+++ b/edbob/pyramid/views/grids/__init__.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.views.grids`` -- Grid Views
+"""
+
+from edbob.pyramid.views.grids.core import *
+from edbob.pyramid.views.grids.alchemy import *
diff --git a/edbob/pyramid/views/grids/alchemy.py b/edbob/pyramid/views/grids/alchemy.py
new file mode 100644
index 0000000..8365842
--- /dev/null
+++ b/edbob/pyramid/views/grids/alchemy.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.views.grids`` -- Grid Views
+"""
+
+from webhelpers import paginate
+
+from edbob.pyramid import grids
+from edbob.pyramid import Session
+from edbob.pyramid.views.grids.core import GridView
+
+
+__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, data, **kwargs):
+ kwargs.setdefault('partial_only', self.partial_only)
+ return grids.AlchemyGrid(
+ self.request, self.mapped_class, data, **kwargs)
+
+ def grid(self, data):
+ return self.make_grid(data)
+
+ def __call__(self):
+ query = self.query()
+ grid = self.grid(query)
+ return grids.util.render_grid(grid)
+
+
+class SortableAlchemyGridView(AlchemyGridView):
+
+ sort = None
+
+ 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 sort_map(self):
+ return self.make_sort_map()
+
+ def make_sort_config(self, **kwargs):
+ return grids.util.get_sort_config(
+ self.route_name, 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):
+ kwargs.setdefault('partial_only', self.partial_only)
+ 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):
+
+ 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 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.route_name, 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):
+ 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)
+ 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 grids.util.render_grid(grid, search)
diff --git a/edbob/pyramid/views/grids/core.py b/edbob/pyramid/views/grids/core.py
new file mode 100644
index 0000000..3a47232
--- /dev/null
+++ b/edbob/pyramid/views/grids/core.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# edbob -- Pythonic Software Framework
+# Copyright © 2010-2012 Lance Edgar
+#
+# This file is part of edbob.
+#
+# edbob 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.
+#
+# edbob 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 edbob. If not, see .
+#
+################################################################################
+
+"""
+``edbob.pyramid.views.grids.core`` -- Core Grid View
+"""
+
+from edbob.pyramid import grids
+from edbob.pyramid.views.core import View
+
+
+__all__ = ['GridView']
+
+
+class GridView(View):
+
+ route_name = None
+ route_url = None
+ renderer = None
+ permission = None
+ partial_only = False
+
+ def make_grid(self, **kwargs):
+ kwargs.setdefault('partial_only', self.partial_only)
+ return grids.Grid(self.request, **kwargs)
+
+ def grid(self):
+ return self.make_grid()
+
+ def __call__(self):
+ grid = self.grid()
+ return grids.util.render_grid(grid)
+
+ @classmethod
+ def add_route(cls, config, route_name=None, route_url=None, renderer=None, permission=None):
+ route_name = route_name or cls.route_name
+ route_url = route_url or cls.route_url
+ renderer = renderer or cls.renderer
+ permission = permission or cls.permission
+ config.add_route(route_name, route_url)
+ config.add_view(cls, route_name=route_name, renderer=renderer,
+ permission=permission, http_cache=0)