massive grid overhaul

This commit is contained in:
Lance Edgar 2012-08-05 14:19:15 -07:00
parent c788601572
commit 75d680a8b0
21 changed files with 1243 additions and 782 deletions

View file

@ -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']

View file

@ -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 ''

View file

@ -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)())

View 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

View 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

View 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))

View 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
View 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)())

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 = $('<div class="dialog" id="'+id+'-dialog"></div>');
dialog = $('<div class="dialog" id="'+id+'-dialog"></div>');
}
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');
});
});

View file

@ -12,22 +12,26 @@
<table class="header">
<tr>
<td rowspan="2" class="filters">
${search.render()|n}
</td>
% if search:
<td rowspan="2" class="filters">
${search.render()}
</td>
% else:
<td rowspan="2">&nbsp;</td>
% endif
<td class="context-menu">
<ul>
${self.context_menu_items()}
</ul>
<ul>
${self.context_menu_items()}
</ul>
</td>
</tr>
<tr>
<td class="tools">
${self.tools()}
${self.tools()}
</td>
</tr>
</table>
${grid|n}
${grid}
</div>

View file

@ -7,7 +7,7 @@
<th class="checkbox">${h.checkbox('check-all')}</th>
% endif
% for field in grid.iter_fields():
${grid.th_sortable(field)|n}
${grid.column_header(field)}
% endfor
% for col in grid.extra_columns:
<th>${col.label}</td>

View 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>&nbsp;</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">&nbsp;</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&nbsp;
${grid.page_links()}
</p>
</div>
% endif
</div>

View 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>

View file

@ -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):

View 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

View file

@ -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)

View 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 *

View 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)

View 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)