massive grid overhaul
This commit is contained in:
parent
c788601572
commit
75d680a8b0
21 changed files with 1243 additions and 782 deletions
|
@ -42,15 +42,13 @@ from edbob.pyramid import Session, helpers
|
||||||
from edbob.time import localize
|
from edbob.time import localize
|
||||||
|
|
||||||
from edbob.pyramid.forms.formalchemy.fieldset import *
|
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.fields import *
|
||||||
from edbob.pyramid.forms.formalchemy.renderers import *
|
from edbob.pyramid.forms.formalchemy.renderers import *
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField',
|
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
|
||||||
'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer',
|
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
|
||||||
'AutocompleteFieldRenderer', 'FieldSet',
|
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
|
||||||
'make_fieldset', 'required', 'pretty_datetime',
|
|
||||||
'AssociationProxyField']
|
'AssociationProxyField']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
``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 ``<div>``
|
|
||||||
# (wrapper) and ``<table>`` 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('<a href="#">') + label + literal('</a>')
|
|
||||||
if class_:
|
|
||||||
class_ = ' class="%s"' % class_
|
|
||||||
return literal('<th' + class_ + ' field="' + field.key + '">') + label + literal('</th>')
|
|
||||||
|
|
||||||
# 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 ``<td>`` 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(
|
|
||||||
'<td class="action %s"><a href="#">%s</a></td>' %
|
|
||||||
(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 ``<tr>`` 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 ''
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
``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('<a href="#">') + label + literal('</a>')
|
|
||||||
if class_:
|
|
||||||
class_ = ' class="%s"' % class_
|
|
||||||
return literal('<th' + class_ + ' field="' + field + '">') + label + literal('</th>')
|
|
||||||
|
|
||||||
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)())
|
|
32
edbob/pyramid/grids/__init__.py
Normal file
32
edbob/pyramid/grids/__init__.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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
|
103
edbob/pyramid/grids/alchemy.py
Normal file
103
edbob/pyramid/grids/alchemy.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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('<th%s field="%s">' % (cls, field.key)) + label + literal('</th>')
|
||||||
|
|
||||||
|
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
|
98
edbob/pyramid/grids/core.py
Normal file
98
edbob/pyramid/grids/core.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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('<th field="%s">%s</th>' % (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))
|
272
edbob/pyramid/grids/search.py
Normal file
272
edbob/pyramid/grids/search.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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
|
138
edbob/pyramid/grids/util.py
Normal file
138
edbob/pyramid/grids/util.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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)())
|
|
@ -181,6 +181,15 @@ div.dialog {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Filters
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
div.filters div.filter div.value {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************************
|
/******************************
|
||||||
* Grids
|
* Grids
|
||||||
******************************/
|
******************************/
|
||||||
|
@ -228,7 +237,7 @@ div.grid table tbody td {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.grid table tr.even {
|
div.grid table tr.odd {
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +296,6 @@ div.pager p.showing {
|
||||||
|
|
||||||
div.pager #grid-page-count {
|
div.pager #grid-page-count {
|
||||||
font-size: 8pt;
|
font-size: 8pt;
|
||||||
height: 21px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.pager p.page-links {
|
div.pager p.page-links {
|
||||||
|
|
|
@ -38,16 +38,16 @@ div.object-index table.header td.context-menu ul {
|
||||||
* Filters
|
* Filters
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
div.object-index div.filterset div.filter {
|
div.object-index div.filters div.filter {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.object-index div.filterset div.filter label,
|
div.object-index div.filters div.filter label,
|
||||||
div.object-index div.filterset div.filter select.filter-type {
|
div.object-index div.filters div.filter select.filter-type {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.object-index div.filterset div.buttons * {
|
div.object-index div.filters div.buttons * {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ function disable_filter_options() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* get_dialog(id, callback)
|
* get_dialog(id, callback)
|
||||||
*
|
*
|
||||||
|
@ -115,20 +114,13 @@ function loading(element) {
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* grid_navigate_page(link)
|
* Navigates to another page of results within a grid.
|
||||||
*
|
|
||||||
* Navigates to another page of results within the grid.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function grid_navigate_page(link) {
|
function grid_navigate_page(link, url) {
|
||||||
var page = link.attr('href').replace(/^.*page=/, '');
|
var div = $(link).parents('div.grid:first');
|
||||||
var div = link.parents('div.grid:first');
|
|
||||||
loading(div);
|
loading(div);
|
||||||
div.load(div.attr('url'), {
|
div.load(url);
|
||||||
'page': page,
|
|
||||||
'partial': true,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,7 +159,7 @@ $(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#add-filter').live('change', function() {
|
$('#add-filter').live('change', function() {
|
||||||
var div = $(this).parents('div.filterset:first');
|
var div = $(this).parents('div.filters:first');
|
||||||
var filter = div.find('#filter-'+$(this).val());
|
var filter = div.find('#filter-'+$(this).val());
|
||||||
filter.find(':first-child').attr('checked', true);
|
filter.find(':first-child').attr('checked', true);
|
||||||
filter.show();
|
filter.show();
|
||||||
|
@ -183,7 +175,7 @@ $(function() {
|
||||||
div.find('button[type=reset]').show();
|
div.find('button[type=reset]').show();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('div.filterset form').live('submit', function() {
|
$('div.filters form').live('submit', function() {
|
||||||
var div = $('div.grid:first');
|
var div = $('div.grid:first');
|
||||||
var data = $(this).serialize() + '&partial=true';
|
var data = $(this).serialize() + '&partial=true';
|
||||||
loading(div);
|
loading(div);
|
||||||
|
@ -193,6 +185,22 @@ $(function() {
|
||||||
return false;
|
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() {
|
$('div.grid table th.sortable a').live('click', function() {
|
||||||
var div = $(this).parents('div.grid:first');
|
var div = $(this).parents('div.grid:first');
|
||||||
var th = $(this).parents('th:first');
|
var th = $(this).parents('th:first');
|
||||||
|
@ -204,6 +212,7 @@ $(function() {
|
||||||
var url = div.attr('url');
|
var url = div.attr('url');
|
||||||
url += url.match(/\?/) ? '&' : '?';
|
url += url.match(/\?/) ? '&' : '?';
|
||||||
url += 'sort=' + th.attr('field') + '&dir=' + dir;
|
url += 'sort=' + th.attr('field') + '&dir=' + dir;
|
||||||
|
url += '&page=1';
|
||||||
url += '&partial=true';
|
url += '&partial=true';
|
||||||
div.load(url);
|
div.load(url);
|
||||||
return false;
|
return false;
|
||||||
|
@ -304,7 +313,11 @@ $(function() {
|
||||||
$('#grid-page-count').live('change', function() {
|
$('#grid-page-count').live('change', function() {
|
||||||
var div = $(this).parents('div.grid:first');
|
var div = $(this).parents('div.grid:first');
|
||||||
loading(div);
|
loading(div);
|
||||||
div.load(div.attr('url') + '&per_page=' + $(this).val());
|
url = div.attr('url');
|
||||||
|
url += url.match(/\?/) ? '&' : '?';
|
||||||
|
url += 'per_page=' + $(this).val();
|
||||||
|
url += '&partial=true';
|
||||||
|
div.load(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('button.autocomplete-change').live('click', function() {
|
$('button.autocomplete-change').live('click', function() {
|
||||||
|
|
|
@ -12,9 +12,13 @@
|
||||||
|
|
||||||
<table class="header">
|
<table class="header">
|
||||||
<tr>
|
<tr>
|
||||||
|
% if search:
|
||||||
<td rowspan="2" class="filters">
|
<td rowspan="2" class="filters">
|
||||||
${search.render()|n}
|
${search.render()}
|
||||||
</td>
|
</td>
|
||||||
|
% else:
|
||||||
|
<td rowspan="2"> </td>
|
||||||
|
% endif
|
||||||
<td class="context-menu">
|
<td class="context-menu">
|
||||||
<ul>
|
<ul>
|
||||||
${self.context_menu_items()}
|
${self.context_menu_items()}
|
||||||
|
@ -28,6 +32,6 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
${grid|n}
|
${grid}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<th class="checkbox">${h.checkbox('check-all')}</th>
|
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||||
% endif
|
% endif
|
||||||
% for field in grid.iter_fields():
|
% for field in grid.iter_fields():
|
||||||
${grid.th_sortable(field)|n}
|
${grid.column_header(field)}
|
||||||
% endfor
|
% endfor
|
||||||
% for col in grid.extra_columns:
|
% for col in grid.extra_columns:
|
||||||
<th>${col.label}</td>
|
<th>${col.label}</td>
|
||||||
|
|
50
edbob/pyramid/templates/grids/grid.mako
Normal file
50
edbob/pyramid/templates/grids/grid.mako
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<div ${grid.div_attrs()}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
% if checkboxes:
|
||||||
|
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
${grid.column_header(field)}
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<th>${col.label}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.deletable:
|
||||||
|
<th> </th>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for i, row in enumerate(grid.iter_rows(), 1):
|
||||||
|
<tr ${grid.row_attrs(row, i)}>
|
||||||
|
% if grid.checkboxes:
|
||||||
|
<td class="checkbox">${grid.checkbox(row)}</td>
|
||||||
|
% endif
|
||||||
|
% for field in grid.iter_fields():
|
||||||
|
<td class="${field.name}">${grid.render_field(field)}</td>
|
||||||
|
% endfor
|
||||||
|
% for col in grid.extra_columns:
|
||||||
|
<td class="${col.name}">${col.callback(row)}</td>
|
||||||
|
% endfor
|
||||||
|
% if grid.deletable:
|
||||||
|
<td class="delete"> </td>
|
||||||
|
% endif
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
% if grid.pager:
|
||||||
|
<div class="pager">
|
||||||
|
<p class="showing">
|
||||||
|
${grid.pager.pager('showing $first_item thru $last_item of $item_count (page $page of $page_count)')}
|
||||||
|
</p>
|
||||||
|
<p class="page-links">
|
||||||
|
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
|
||||||
|
per page
|
||||||
|
${grid.page_links()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
36
edbob/pyramid/templates/grids/search.mako
Normal file
36
edbob/pyramid/templates/grids/search.mako
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="filters" url="${search.request.current_route_url()}">
|
||||||
|
${search.begin()}
|
||||||
|
${search.hidden('filters', 'true')}
|
||||||
|
<% visible = [] %>
|
||||||
|
% for f in search.sorted_filters():
|
||||||
|
<div class="filter" id="filter-${f.name}"${' style="display: none;"' if not search.config.get('include_filter_'+f.name) else ''|n}>
|
||||||
|
${search.checkbox('include_filter_'+f.name)}
|
||||||
|
<label for="${f.name}">${f.label}</label>
|
||||||
|
${f.types_select()}
|
||||||
|
<div class="value">
|
||||||
|
${f.value_control()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if search.config.get('include_filter_'+f.name):
|
||||||
|
<% visible.append(f.name) %>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
<div class="buttons">
|
||||||
|
${search.add_filter(visible)}
|
||||||
|
${search.submit('submit', "Search", style='display: none;' if not visible else None)}
|
||||||
|
<button type="reset"${' style="display: none;"' if not visible else ''|n}>Reset</button>
|
||||||
|
</div>
|
||||||
|
${search.end()}
|
||||||
|
% if visible:
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
filters_to_disable = [
|
||||||
|
% for field in visible:
|
||||||
|
'${field}',
|
||||||
|
% endfor
|
||||||
|
];
|
||||||
|
$(function() {
|
||||||
|
disable_filter_options();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</div>
|
|
@ -32,9 +32,10 @@ from pyramid.security import authenticated_userid
|
||||||
from webhelpers.html import literal
|
from webhelpers.html import literal
|
||||||
from webhelpers.html.tags import link_to
|
from webhelpers.html.tags import link_to
|
||||||
|
|
||||||
|
from edbob.pyramid.views.core import *
|
||||||
from edbob.pyramid.views.autocomplete import *
|
from edbob.pyramid.views.autocomplete import *
|
||||||
from edbob.pyramid.views.form import *
|
from edbob.pyramid.views.form import *
|
||||||
from edbob.pyramid.views.grid import *
|
from edbob.pyramid.views.grids import *
|
||||||
|
|
||||||
|
|
||||||
def forbidden(request):
|
def forbidden(request):
|
||||||
|
|
36
edbob/pyramid/views/core.py
Normal file
36
edbob/pyramid/views/core.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``edbob.pyramid.views.core`` -- Core Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['View']
|
||||||
|
|
||||||
|
|
||||||
|
class View(object):
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
"""
|
|
||||||
``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)
|
|
30
edbob/pyramid/views/grids/__init__.py
Normal file
30
edbob/pyramid/views/grids/__init__.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``edbob.pyramid.views.grids`` -- Grid Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from edbob.pyramid.views.grids.core import *
|
||||||
|
from edbob.pyramid.views.grids.alchemy import *
|
166
edbob/pyramid/views/grids/alchemy.py
Normal file
166
edbob/pyramid/views/grids/alchemy.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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)
|
63
edbob/pyramid/views/grids/core.py
Normal file
63
edbob/pyramid/views/grids/core.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
``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)
|
Loading…
Add table
Add a link
Reference in a new issue