diff --git a/edbob/pyramid/forms/formalchemy/__init__.py b/edbob/pyramid/forms/formalchemy/__init__.py index 9b02cdc..c0a35ef 100644 --- a/edbob/pyramid/forms/formalchemy/__init__.py +++ b/edbob/pyramid/forms/formalchemy/__init__.py @@ -42,15 +42,13 @@ from edbob.pyramid import Session, helpers from edbob.time import localize from edbob.pyramid.forms.formalchemy.fieldset import * -from edbob.pyramid.forms.formalchemy.grid import * from edbob.pyramid.forms.formalchemy.fields import * from edbob.pyramid.forms.formalchemy.renderers import * -__all__ = ['AlchemyGrid', 'ChildGridField', 'PropertyField', - 'EnumFieldRenderer', 'PrettyDateTimeFieldRenderer', - 'AutocompleteFieldRenderer', 'FieldSet', - 'make_fieldset', 'required', 'pretty_datetime', +__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer', + 'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer', + 'FieldSet', 'make_fieldset', 'required', 'pretty_datetime', 'AssociationProxyField'] diff --git a/edbob/pyramid/forms/formalchemy/grid.py b/edbob/pyramid/forms/formalchemy/grid.py deleted file mode 100644 index b217bc3..0000000 --- a/edbob/pyramid/forms/formalchemy/grid.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.pyramid.forms.formalchemy.grid`` -- FormAlchemy Grid -""" - -from webhelpers import paginate -from webhelpers.html.builder import format_attrs -from webhelpers.html.tags import literal - -import formalchemy - -import edbob -from edbob.util import prettify - - -__all__ = ['AlchemyGrid'] - - -class AlchemyGrid(formalchemy.Grid): - """ - Provides an "enhanced" version of the :class:`formalchemy.Grid` class. - """ - - prettify = staticmethod(prettify) - - # uuid_key = None - - # def __init__(self, cls, instances, config, url_kwargs={}, *args, **kwargs): - # formalchemy.Grid.__init__(self, cls, instances, *args, **kwargs) - # self.pager = instances if isinstance(instances, paginate.Page) else None - # self.config = config - # self.url_kwargs = url_kwargs - # self.sortable = config.get('sortable', False) - - def __init__(self, cls, instances, config, gridurl=None, objurl=None, - delurl=None, **kwargs): - """ - Grid constructor. - - ``url`` must be the URL used to access the grid itself. This url/view - must accept a GET query string parameter of "partial=True", which will - indicate that the grid *only* is being requested, as opposed to the - full page in which the grid normally resides. - """ - - formalchemy.Grid.__init__(self, cls, instances, **kwargs) - self.config = config - self.request = config['request'] - self.gridurl = gridurl - self.objurl = objurl - self.delurl = delurl - self.sortable = config.get('sortable', False) - self.clickable = config.get('clickable', False) - self.deletable = config.get('deletable', False) - self.pager = instances if isinstance(instances, paginate.Page) else None - self.extra_columns = [] - - def add_column(self, name, label, callback): - self.extra_columns.append( - edbob.Object(name=name, label=label, callback=callback)) - - def field_name(self, field): - return field.name - - def iter_fields(self): - for field in self.render_fields.itervalues(): - yield field - - def render_field(self, field, readonly): - if readonly: - return field.render_readonly() - return field.render() - - def row_attrs(self, i): - attrs = dict(class_='even' if i % 2 else 'odd') - if hasattr(self.model, 'uuid'): - attrs['uuid'] = self.model.uuid - return format_attrs(**attrs) - - def url_attrs(self): - attrs = {} - url = self.request.route_url - if self.gridurl: - attrs['url'] = self.gridurl - if self.objurl: - attrs['objurl'] = url(self.objurl, uuid='{uuid}') - if self.delurl: - attrs['delurl'] = url(self.delurl, uuid='{uuid}') - return format_attrs(**attrs) - - # def render(self, class_=None, **kwargs): - # """ - # Renders the grid into HTML, and returns the result. - - # ``class_`` (if provided) is used to define the class of the ``
`` - # (wrapper) and ```` elements of the grid. - - # Any remaining ``kwargs`` are passed directly to the underlying - # ``formalchemy.Grid.render()`` method. - # """ - - # kwargs['class_'] = class_ - # # kwargs.setdefault('get_uuid', self.get_uuid) - # kwargs.setdefault('checkboxes', False) - # return formalchemy.Grid.render(self, **kwargs) - - def render(self, **kwargs): - engine = self.engine or formalchemy.config.engine - if self.readonly: - return engine('grid_readonly', grid=self, **kwargs) - kwargs.setdefault('request', self._request) - return engine('grid', grid=self, **kwargs) - - def th_sortable(self, field): - class_ = '' - label = field.label() - if self.sortable and field.key in self.config.get('sort_map', {}): - class_ = 'sortable' - if field.key == self.config['sort']: - class_ += ' sorted ' + self.config['dir'] - label = literal('') + label + literal('') - if class_: - class_ = ' class="%s"' % class_ - return literal('') + label + literal('') - - # def url(self): - # # TODO: Probably clean this up somehow... - # if self.pager is not None: - # u = self.pager._url_generator(self.pager.page, partial=True) - # else: - # u = self._url or '' - # qs = self.query_string() - # if qs: - # if '?' not in u: - # u += '?' - # u += qs - # elif '?' not in u: - # u += '?partial=True' - # return u - - # def query_string(self): - # # TODO: Probably clean this up somehow... - # qs = '' - # if self.url_kwargs: - # for k, v in self.url_kwargs.items(): - # qs += '&%s=%s' % (urllib.quote_plus(k), urllib.quote_plus(v)) - # return qs - - def get_actions(self): - """ - Returns an HTML snippet containing ``' % - (cls, text)) - return res - - # def get_uuid(self): - # """ - # .. highlight:: none - - # Returns a unique identifier for a given record, in the form of an HTML - # attribute for direct inclusion in a ```` element within a template. - # An example of what this function might return would be the string:: - - # 'uuid="420"' - - # Rattail itself will tend to use *universally-unique* IDs (true UUIDs), - # but this method may be overridden to support legacy databases with - # auto-increment IDs, etc. Really the only important thing is that the - # value returned be unique across the relevant data set. - - # If the concept is unsupported, the method should return an empty - # string. - # """ - - # def uuid(): - # if self.uuid_key and hasattr(self.model, self.uuid_key): - # return getattr(self.model, self.uuid_key) - # if hasattr(self.model, 'uuid'): - # return getattr(self.model, 'uuid') - # if hasattr(self.model, 'id'): - # return getattr(self.model, 'id') - - # uuid = uuid() - # if uuid: - # return literal('uuid="%s"' % uuid) - # return '' diff --git a/edbob/pyramid/grids.py b/edbob/pyramid/grids.py deleted file mode 100644 index 824fde2..0000000 --- a/edbob/pyramid/grids.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.pyramid.grids`` -- Grid Tables -""" - -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict - -from sqlalchemy.orm import Query -from sqlalchemy.orm.attributes import InstrumentedAttribute - -from pyramid.renderers import render -from pyramid.response import Response -from webhelpers import paginate -from webhelpers.html import literal -from webhelpers.html.builder import format_attrs - -import edbob -from edbob.pyramid.filters import SearchFormRenderer -from edbob.util import prettify - - -class BasicGrid(edbob.Object): - """ - Basic grid class for those times when SQLAlchemy is not needed. - """ - - def __init__(self, columns, rows, config, url, sortable=True, deletable=False, **kwargs): - edbob.Object.__init__(self, **kwargs) - self.rows = rows - self.config = config - self.url = url - self.sortable = sortable - self.deletable = deletable - self.columns = OrderedDict() - for col in columns: - if isinstance(col, (tuple, list)): - if len(col) == 2: - self.columns[col[0]] = col[1] - continue - elif isinstance(col, basestring): - self.columns[col] = prettify(col) - continue - raise ValueError("Column element must be either a string or 2-tuple") - - def _set_active(self, row): - self.model = {} - for i, col in enumerate(self.columns.keys()): - if i >= len(row): - break - self.model[col] = row[i] - - def field_label(self, name): - return self.columns[name] - - def field_name(self, field): - return field - - def iter_fields(self): - for col in self.columns.keys(): - yield col - - def render(self, **kwargs): - kwargs['grid'] = self - return render('forms/grid_readonly.mako', kwargs) - - def render_field(self, field, readonly): - return self.model[field] - - def row_attrs(self, i): - return format_attrs(class_='even' if i % 2 else 'odd') - - def th_sortable(self, field): - class_ = '' - label = self.field_label(field) - if self.sortable and field in self.config.get('sort_map', {}): - class_ = 'sortable' - if field == self.config['sort']: - class_ += ' sorted ' + self.config['dir'] - label = literal('') + label + literal('') - if class_: - class_ = ' class="%s"' % class_ - return literal('') + label + literal('') - - def url_attrs(self): - return format_attrs(url=self.url) - - -def get_grid_config(name, request, search=None, url=None, **kwargs): - config = { - 'actions': [], - 'per_page': 20, - 'page': 1, - 'sortable': True, - 'dir': 'asc', - 'object_url': '', - 'deletable': False, - 'delete_url': '', - 'use_dialog': False, - } - config.update(kwargs) - # words = name.split('.') - # if len(words) == 2: - # config.setdefault('object_url', request.route_url(words[0], action='crud')) - # config.setdefault('delete_url', config['object_url']) - for key in config: - full_key = name+'_'+key - if request.params.get(key): - value = request.params[key] - config[key] = value - request.session[full_key] = value - elif request.session.get(full_key): - value = request.session[full_key] - config[key] = value - config['request'] = request - config['search'] = search - config['url'] = url - return config - - -def get_pager(query, config): - query = query(config) - count = None - if isinstance(query, Query): - count = query.count() - return paginate.Page( - query, item_count=count, - items_per_page=int(config['per_page']), - page=int(config['page']), - url=paginate.PageURL(config['url'], {}), - ) - - -def get_sort_map(cls, names=None, **kwargs): - """ - Convenience function which returns a sort map. - """ - - smap = {} - if names is None: - names = [] - for attr in cls.__dict__: - obj = getattr(cls, attr) - if isinstance(obj, InstrumentedAttribute): - if obj.key != 'uuid': - names.append(obj.key) - for name in names: - smap[name] = sorter(getattr(cls, name)) - smap.update(kwargs) - return smap - - -def render_grid(request, grid, search=None, **kwargs): - if request.params.get('partial'): - return Response(body=grid, content_type='text/html') - kwargs['grid'] = grid - if search: - kwargs['search'] = SearchFormRenderer(search) - return kwargs - - -def sort_query(query, config, sort_map, join_map={}): - field = config.get('sort') - if not field: - return query - joins = config.setdefault('joins', []) - if field in join_map and field not in joins: - query = join_map[field](query) - joins.append(field) - config['sort_map'] = sort_map - return sort_map[field](query, config['dir']) - - -def sorter(field): - """ - Returns a function suitable for a sort map callable, with typical - logic built in for sorting applied to ``field``. - """ - return lambda q, d: q.order_by(getattr(field, d)()) diff --git a/edbob/pyramid/grids/__init__.py b/edbob/pyramid/grids/__init__.py new file mode 100644 index 0000000..8593f3b --- /dev/null +++ b/edbob/pyramid/grids/__init__.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.grids`` -- Grids +""" + +from edbob.pyramid.grids.core import * +from edbob.pyramid.grids.alchemy import * +from edbob.pyramid.grids import util +from edbob.pyramid.grids import search diff --git a/edbob/pyramid/grids/alchemy.py b/edbob/pyramid/grids/alchemy.py new file mode 100644 index 0000000..9b586f8 --- /dev/null +++ b/edbob/pyramid/grids/alchemy.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes +""" + +from webhelpers.html import literal +from webhelpers.html import tags + +import formalchemy + +import edbob +from edbob.pyramid import Session +from edbob.pyramid.grids.core import Grid +from edbob.util import prettify + + +__all__ = ['AlchemyGrid'] + + +class AlchemyGrid(Grid): + + sort_map = {} + + pager = None + pager_format = '$link_first $link_previous ~1~ $link_next $link_last' + + def __init__(self, request, cls, instances, **kwargs): + super(AlchemyGrid, self).__init__(request, **kwargs) + self._formalchemy_grid = formalchemy.Grid( + cls, instances, session=Session(), request=request) + self._formalchemy_grid.prettify = prettify + + def __getattr__(self, attr): + return getattr(self._formalchemy_grid, attr) + + def column_header(self, field): + cls = '' + label = field.label() + if field.key in self.sort_map: + cls = 'sortable' + if field.key == self.config['sort']: + cls += ' sorted ' + self.config['dir'] + label = tags.link_to(label, '#') + if cls: + cls = ' class="%s"' % cls + return literal('' % (cls, field.key)) + label + literal('') + + def iter_fields(self): + return self._formalchemy_grid.render_fields.itervalues() + + def iter_rows(self): + for row in self._formalchemy_grid.rows: + self._formalchemy_grid._set_active(row) + yield row + + def page_count_options(self): + options = edbob.config.get('edbob.pyramid', 'grid.page_count_options') + if options: + options = options.split(',') + options = [int(x.strip()) for x in options] + else: + options = [5, 10, 20, 50, 100] + return options + + def page_links(self): + return self.pager.pager(self.pager_format, + symbol_next='next', + symbol_previous='prev', + onclick="grid_navigate_page(this, '$partial_url'); return false;") + + def render_field(self, field): + if self._formalchemy_grid.readonly: + return field.render_readonly() + return field.render() + + def _row_attrs(self, row, i): + attrs = super(AlchemyGrid, self)._row_attrs(row, i) + if hasattr(row, 'uuid'): + attrs['uuid'] = row.uuid + return attrs diff --git a/edbob/pyramid/grids/core.py b/edbob/pyramid/grids/core.py new file mode 100644 index 0000000..651cc59 --- /dev/null +++ b/edbob/pyramid/grids/core.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.grids.core`` -- Core Grid Classes +""" + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + +from webhelpers.html import literal +from webhelpers.html.builder import format_attrs + +from pyramid.renderers import render + +import edbob + + +__all__ = ['Grid'] + + +class Grid(edbob.Object): + + hoverable = True + clickable = False + checkboxes = False + deletable = False + partial_only = False + + def __init__(self, request, **kwargs): + kwargs.setdefault('fields', OrderedDict()) + kwargs.setdefault('extra_columns', []) + super(Grid, self).__init__(**kwargs) + self.request = request + + def column_header(self, field): + return literal('' % (field.name, field.label)) + + def div_class(self): + if self.clickable: + return 'grid clickable' + if self.hoverable: + return 'grid hoverable' + return 'grid' + + def _div_attrs(self): + attrs = {'class_':'grid', 'url':self.request.current_route_url()} + if self.clickable: + attrs['class_'] = 'grid clickable' + elif self.hoverable: + attrs['class_'] = 'grid hoverable' + return attrs + + def div_attrs(self): + return format_attrs(**self._div_attrs()) + + def iter_fields(self): + return self.fields.itervalues() + + def iter_rows(self): + raise NotImplementedError + + def render(self, template='/grids/grid.mako', **kwargs): + kwargs.setdefault('grid', self) + return render(template, kwargs) + + def render_field(self, field): + raise NotImplementedError + + def _row_attrs(self, row, i): + attrs = {'class_': 'odd' if i % 2 else 'even'} + return attrs + + def row_attrs(self, row, i): + return format_attrs(**self._row_attrs(row, i)) diff --git a/edbob/pyramid/grids/search.py b/edbob/pyramid/grids/search.py new file mode 100644 index 0000000..9477a6d --- /dev/null +++ b/edbob/pyramid/grids/search.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.grids.search`` -- Grid Search Filters +""" + +import re + +from webhelpers.html import tags +from webhelpers.html import literal + +from pyramid.renderers import render +from pyramid_simpleform import Form +from pyramid_simpleform.renderers import FormRenderer + +import edbob +from edbob.util import prettify + + +class SearchFilter(edbob.Object): + """ + Base class and default implementation for search filters. + """ + + def __init__(self, name, label=None, **kwargs): + edbob.Object.__init__(self, **kwargs) + self.name = name + self.label = label or prettify(name) + + def types_select(self): + types = [ + ('is', "is"), + ('nt', "is not"), + ('lk', "contains"), + ('nl', "doesn't contain"), + ] + options = [] + filter_map = self.search.filter_map[self.name] + for value, label in types: + if value in filter_map: + options.append((value, label)) + return tags.select('filter_type_'+self.name, + self.search.config.get('filter_type_'+self.name), + options, class_='filter-type') + + def value_control(self): + return tags.text(self.name, self.search.config.get(self.name)) + + +class SearchForm(Form): + """ + Generic form class which aggregates :class:`SearchFilter` instances. + """ + + def __init__(self, request, filter_map, config, *args, **kwargs): + super(SearchForm, self).__init__(request, *args, **kwargs) + self.filter_map = filter_map + self.config = config + self.filters = {} + + def add_filter(self, filter_): + filter_.search = self + self.filters[filter_.name] = filter_ + + +class SearchFormRenderer(FormRenderer): + """ + Renderer for :class:`SearchForm` instances. + """ + + def __init__(self, form, *args, **kwargs): + super(SearchFormRenderer, self).__init__(form, *args, **kwargs) + self.request = form.request + self.filters = form.filters + self.config = form.config + + def add_filter(self, visible): + options = ['add a filter'] + for f in self.sorted_filters(): + options.append((f.name, f.label)) + return self.select('add-filter', options, + style='display: none;' if len(visible) == len(self.filters) else None) + + def checkbox(self, name, checked=None, **kwargs): + if name.startswith('include_filter_'): + if checked is None: + checked = self.config[name] + return tags.checkbox(name, checked=checked, **kwargs) + if checked is None: + checked = False + return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs) + + def render(self, **kwargs): + kwargs['search'] = self + return literal(render('/grids/search.mako', kwargs)) + + def sorted_filters(self): + return sorted(self.filters.values(), key=lambda x: x.label) + + def text(self, name, **kwargs): + return tags.text(name, value=self.config.get(name), **kwargs) + + +def filter_exact(field): + """ + Convenience function which returns a filter map entry, with typical logic + built in for "exact match" queries applied to ``field``. + """ + + return { + 'is': + lambda q, v: q.filter(field == v) if v else q, + 'nt': + lambda q, v: q.filter(field != v) if v else q, + } + + +def filter_ilike(field): + """ + Convenience function which returns a filter map entry, with typical logic + built in for "ILIKE" queries applied to ``field``. + """ + + return { + 'lk': + lambda q, v: q.filter(field.ilike('%%%s%%' % v)) if v else q, + 'nl': + lambda q, v: q.filter(~field.ilike('%%%s%%' % v)) if v else q, + } + + +def get_filter_config(name, request, filter_map, **kwargs): + """ + Returns a configuration dictionary for a search form. + """ + + config = {} + + def update_config(dict_, prefix='', exclude_by_default=False): + """ + Updates the ``config`` dictionary based on the contents of ``dict_``. + """ + + for field in filter_map: + if prefix+'include_filter_'+field in dict_: + include = dict_[prefix+'include_filter_'+field] + include = bool(include) and include != '0' + config['include_filter_'+field] = include + elif exclude_by_default: + config['include_filter_'+field] = False + if prefix+'filter_type_'+field in dict_: + config['filter_type_'+field] = dict_[prefix+'filter_type_'+field] + if prefix+field in dict_: + config[field] = dict_[prefix+field] + + # Update config to exclude all filters by default. + for field in filter_map: + config['include_filter_'+field] = False + + # Update config to honor default settings. + config.update(kwargs) + + # Update config with data cached in session. + update_config(request.session, prefix=name+'.') + + # Update config with data from GET/POST request. + if request.params.get('filters') == 'true': + update_config(request.params, exclude_by_default=True) + + # Cache filter data in session. + for key in config: + if (not key.startswith('filter_factory_') + and not key.startswith('filter_label_')): + request.session[name+'.'+key] = config[key] + + return config + + +def get_filter_map(cls, exact=[], ilike=[], **kwargs): + """ + Convenience function which returns a "filter map" for ``cls``. + + ``exact``, if provided, should be a list of field names for which "exact" + filtering is to be allowed. + + ``ilike``, if provided, should be a list of field names for which "ILIKE" + filtering is to be allowed. + + Any remaining ``kwargs`` are assumed to be filter map entries themselves, + and are added directly to the map. + """ + + fmap = {} + for name in exact: + fmap[name] = filter_exact(getattr(cls, name)) + for name in ilike: + fmap[name] = filter_ilike(getattr(cls, name)) + fmap.update(kwargs) + return fmap + + +def get_search_form(request, filter_map, config): + """ + Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for + each filter in ``filter_map``, using configuration from ``config``. + """ + + search = SearchForm(request, filter_map, config) + for field in filter_map: + factory = config.get('filter_factory_%s' % field, SearchFilter) + label = config.get('filter_label_%s' % field) + search.add_filter(factory(field, label=label)) + return search + + +# def filter_query(query, config, join_map={}): +# filter_map = config['filter_map'] +# if config.get('search'): +# search = config['search'].config +# joins = config.setdefault('joins', []) +# include_filter = re.compile(r'^include_filter_(.*)$') +# for key in search: +# m = include_filter.match(key) +# if m and search[key]: +# field = m.group(1) +# if field in join_map and field not in joins: +# query = join_map[field](query) +# joins.append(field) +# value = search.get(field) +# if value: +# f = filter_map[field][search['filter_type_'+field]] +# query = f(query, value) +# return query + + +def filter_query(query, config, filter_map, join_map): + joins = config.setdefault('joins', []) + include_filter = re.compile(r'^include_filter_(.*)$') + for key in config: + m = include_filter.match(key) + if m and config[key]: + field = m.group(1) + if field in join_map and field not in joins: + query = join_map[field](query) + joins.append(field) + value = config.get(field) + if value: + f = filter_map[field][config['filter_type_'+field]] + query = f(query, value) + return query diff --git a/edbob/pyramid/grids/util.py b/edbob/pyramid/grids/util.py new file mode 100644 index 0000000..1911135 --- /dev/null +++ b/edbob/pyramid/grids/util.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.grids.util`` -- Grid Utilities +""" + +from sqlalchemy.orm.attributes import InstrumentedAttribute + +from webhelpers.html import literal + +from pyramid.response import Response + +from edbob.pyramid.grids.search import SearchFormRenderer + + +def get_sort_config(name, request, **kwargs): + """ + Returns a configuration dictionary for grid sorting. + """ + + # Initial config uses some default values. + config = { + 'dir': 'asc', + 'per_page': 20, + 'page': 1, + } + + # Override with defaults provided by caller. + config.update(kwargs) + + # Override with values from GET/POST request and/or session. + for key in config: + full_key = name+'_'+key + if request.params.get(key): + value = request.params[key] + config[key] = value + request.session[full_key] = value + elif request.session.get(full_key): + value = request.session[full_key] + config[key] = value + + return config + + +def get_sort_map(cls, names=None, **kwargs): + """ + Convenience function which returns a sort map for ``cls``. + + If ``names`` is not specified, the map will include all "standard" fields + present on the mapped class. Otherwise, the map will be limited to only + the fields which are named. + + All remaining ``kwargs`` are assumed to be sort map entries, and will be + added to the map directly. + """ + + smap = {} + if names is None: + names = [] + for attr in cls.__dict__: + obj = getattr(cls, attr) + if isinstance(obj, InstrumentedAttribute): + if obj.key != 'uuid': + names.append(obj.key) + for name in names: + smap[name] = sorter(getattr(cls, name)) + smap.update(kwargs) + return smap + + +def render_grid(grid, search_form=None, **kwargs): + """ + Convenience function to render ``grid`` (which should be a + :class:`edbob.pyramid.grids.Grid` instance). + + This "usually" will return a dictionary to be used as context for rendering + the final view template. + + However, if a partial grid is requested (or mandated), then the grid body + will be rendered and a :class:`pyramid.response.Response` object will be + returned instead. + """ + + if grid.partial_only or grid.request.params.get('partial'): + return Response(body=grid.render(), content_type='text/html') + kwargs['grid'] = literal(grid.render()) + if search_form: + kwargs['search'] = SearchFormRenderer(search_form) + return kwargs + + +def sort_query(query, config, sort_map, join_map={}): + """ + Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is + used, if necessary, to join additional tables to the base query. The + sorted query is returned. + """ + + field = config.get('sort') + if not field: + return query + joins = config.setdefault('joins', []) + if field in join_map and field not in joins: + query = join_map[field](query) + joins.append(field) + sort = sort_map[field] + return sort(query, config['dir']) + + +def sorter(field): + """ + Returns a function suitable for a sort map callable, with typical logic + built in for sorting applied to ``field``. + """ + + return lambda q, d: q.order_by(getattr(field, d)()) diff --git a/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/edbob.css index 1efb683..c17d99a 100644 --- a/edbob/pyramid/static/css/edbob.css +++ b/edbob/pyramid/static/css/edbob.css @@ -181,6 +181,15 @@ div.dialog { } +/****************************** + * Filters + ******************************/ + +div.filters div.filter div.value { + display: inline; +} + + /****************************** * Grids ******************************/ @@ -228,7 +237,7 @@ div.grid table tbody td { text-align: left; } -div.grid table tr.even { +div.grid table tr.odd { background-color: #e0e0e0; } @@ -287,7 +296,6 @@ div.pager p.showing { div.pager #grid-page-count { font-size: 8pt; - height: 21px; } div.pager p.page-links { diff --git a/edbob/pyramid/static/css/index.css b/edbob/pyramid/static/css/index.css index 0092b5e..8beaa9b 100644 --- a/edbob/pyramid/static/css/index.css +++ b/edbob/pyramid/static/css/index.css @@ -38,16 +38,16 @@ div.object-index table.header td.context-menu ul { * Filters ******************************/ -div.object-index div.filterset div.filter { +div.object-index div.filters div.filter { margin-bottom: 10px; } -div.object-index div.filterset div.filter label, -div.object-index div.filterset div.filter select.filter-type { +div.object-index div.filters div.filter label, +div.object-index div.filters div.filter select.filter-type { margin-right: 8px; } -div.object-index div.filterset div.buttons * { +div.object-index div.filters div.buttons * { margin-right: 8px; } diff --git a/edbob/pyramid/static/js/edbob.js b/edbob/pyramid/static/js/edbob.js index d05d9e7..e9add59 100644 --- a/edbob/pyramid/static/js/edbob.js +++ b/edbob/pyramid/static/js/edbob.js @@ -1,15 +1,15 @@ /************************************************************ -* -* edbob.js -* -* This library contains all of Javascript functionality -* provided directly by edbob. -* -* It also attaches some jQuery event handlers for certain -* design patterns. -* -************************************************************/ + * + * edbob.js + * + * This library contains all of Javascript functionality + * provided directly by edbob. + * + * It also attaches some jQuery event handlers for certain + * design patterns. + * + ************************************************************/ var filters_to_disable = []; @@ -23,12 +23,11 @@ function disable_button(button, text) { function disable_filter_options() { for (var i = 0; i <= filters_to_disable.length; ++i) { - var filter = filters_to_disable.pop(); - var option = $('#add-filter option[value='+filter+']').attr('disabled', true); + var filter = filters_to_disable.pop(); + var option = $('#add-filter option[value='+filter+']').attr('disabled', true); } } - /* * get_dialog(id, callback) * @@ -46,10 +45,10 @@ function disable_filter_options() { function get_dialog(id, callback) { var dialog = $('#'+id+'-dialog'); if (! dialog.length) { - dialog = $('
'); + dialog = $('
'); } if (callback) { - dialog.attr('callback', callback); + dialog.attr('callback', callback); } return dialog; } @@ -80,11 +79,11 @@ function get_uuid(obj) { obj = $(obj); if (obj.attr('uuid')) { - return obj.attr('uuid'); + return obj.attr('uuid'); } var tr = obj.parents('tr:first'); if (tr.attr('uuid')) { - return tr.attr('uuid'); + return tr.attr('uuid'); } return undefined; } @@ -115,20 +114,13 @@ function loading(element) { /* - * grid_navigate_page(link) - * - * Navigates to another page of results within the grid. + * Navigates to another page of results within a grid. */ -function grid_navigate_page(link) { - var page = link.attr('href').replace(/^.*page=/, ''); - var div = link.parents('div.grid:first'); +function grid_navigate_page(link, url) { + var div = $(link).parents('div.grid:first'); loading(div); - div.load(div.attr('url'), { - 'page': page, - 'partial': true, - }); - return false; + div.load(url); } @@ -141,13 +133,13 @@ function grid_navigate_page(link) { function reload_grid_div(div) { if (! div) { - div = $('div.grid'); + div = $('div.grid'); } else if (! div.hasClass('grid')) { - div = div.find('div.grid'); + div = div.find('div.grid'); } if (! div.length) { - alert('assert: div should have length'); - return; + alert('assert: div should have length'); + return; } loading(div); div.load(div.attr('url')); @@ -157,213 +149,234 @@ function reload_grid_div(div) { $(function() { $('div.filter label').live('click', function() { - var checkbox = $(this).prev(); - if (checkbox.attr('checked')) { - checkbox.attr('checked', false); - return false; - } - checkbox.attr('checked', true); - return true; + var checkbox = $(this).prev(); + if (checkbox.attr('checked')) { + checkbox.attr('checked', false); + return false; + } + checkbox.attr('checked', true); + return true; }); $('#add-filter').live('change', function() { - var div = $(this).parents('div.filterset:first'); - var filter = div.find('#filter-'+$(this).val()); - filter.find(':first-child').attr('checked', true); - filter.show(); - var field = filter.find(':last-child'); - field.select(); - field.focus(); - $(this).find('option:selected').attr('disabled', true); - $(this).val('add a filter'); - if ($(this).find('option[disabled=false]').length == 1) { - $(this).hide(); - } - div.find('input[type=submit]').show(); - div.find('button[type=reset]').show(); + var div = $(this).parents('div.filters:first'); + var filter = div.find('#filter-'+$(this).val()); + filter.find(':first-child').attr('checked', true); + filter.show(); + var field = filter.find(':last-child'); + field.select(); + field.focus(); + $(this).find('option:selected').attr('disabled', true); + $(this).val('add a filter'); + if ($(this).find('option[disabled=false]').length == 1) { + $(this).hide(); + } + div.find('input[type=submit]').show(); + div.find('button[type=reset]').show(); }); - $('div.filterset form').live('submit', function() { - var div = $('div.grid:first'); - var data = $(this).serialize() + '&partial=true'; - loading(div); - $.post(div.attr('url'), data, function(data) { - div.replaceWith(data); - }); - return false; + $('div.filters form').live('submit', function() { + var div = $('div.grid:first'); + var data = $(this).serialize() + '&partial=true'; + loading(div); + $.post(div.attr('url'), data, function(data) { + div.replaceWith(data); + }); + return false; + }); + + $('div.filters form div.buttons button[type=reset]').click(function() { + var filters = $(this).parents('div.filters:first'); + filters.find('div.filter').each(function() { + $(this).find('div.value input').val(''); + }); + var url = filters.attr('url'); + var grid = $('div.grid[url='+url+']'); + loading(grid); + var form = filters.find('form'); + var data = form.serialize() + '&partial=true'; + $.post(url, data, function(data) { + grid.replaceWith(data); + }); + return false; }); $('div.grid table th.sortable a').live('click', function() { - var div = $(this).parents('div.grid:first'); - var th = $(this).parents('th:first'); - var dir = 'asc'; - if (th.hasClass('sorted') && th.hasClass('asc')) { - dir = 'desc'; - } - loading(div); - var url = div.attr('url'); - url += url.match(/\?/) ? '&' : '?'; - url += 'sort=' + th.attr('field') + '&dir=' + dir; - url += '&partial=true'; - div.load(url); - return false; + var div = $(this).parents('div.grid:first'); + var th = $(this).parents('th:first'); + var dir = 'asc'; + if (th.hasClass('sorted') && th.hasClass('asc')) { + dir = 'desc'; + } + loading(div); + var url = div.attr('url'); + url += url.match(/\?/) ? '&' : '?'; + url += 'sort=' + th.attr('field') + '&dir=' + dir; + url += '&page=1'; + url += '&partial=true'; + div.load(url); + return false; }); $('div.grid.hoverable table tbody tr').live('mouseenter', function() { - $(this).addClass('hovering'); + $(this).addClass('hovering'); }); $('div.grid.hoverable table tbody tr').live('mouseleave', function() { - $(this).removeClass('hovering'); + $(this).removeClass('hovering'); }); $('div.grid.clickable table tbody tr').live('mouseenter', function() { - $(this).addClass('hovering'); + $(this).addClass('hovering'); }); $('div.grid.clickable table tbody tr').live('mouseleave', function() { - $(this).removeClass('hovering'); + $(this).removeClass('hovering'); }); $('div.grid.selectable table tbody tr').live('mouseenter', function() { - $(this).addClass('hovering'); + $(this).addClass('hovering'); }); $('div.grid.selectable table tbody tr').live('mouseleave', function() { - $(this).removeClass('hovering'); + $(this).removeClass('hovering'); }); $('div.grid.checkable table tbody tr').live('mouseenter', function() { - $(this).addClass('hovering'); + $(this).addClass('hovering'); }); $('div.grid.checkable table tbody tr').live('mouseleave', function() { - $(this).removeClass('hovering'); + $(this).removeClass('hovering'); }); $('div.grid.clickable table tbody tr').live('click', function() { - var div = $(this).parents('div.grid:first'); - if (div.attr('usedlg') == 'True') { - var dlg = get_dialog('grid-object'); - var data = { - 'uuid': get_uuid(this), - 'partial': true, - }; - dlg.load(div.attr('objurl'), data, function() { - dlg.dialog({ - width: 500, - height: 450, - }); - }); - } else { - location.href = div.attr('objurl').replace(/%7Buuid%7D/, get_uuid(this)); - } + var div = $(this).parents('div.grid:first'); + if (div.attr('usedlg') == 'True') { + var dlg = get_dialog('grid-object'); + var data = { + 'uuid': get_uuid(this), + 'partial': true, + }; + dlg.load(div.attr('objurl'), data, function() { + dlg.dialog({ + width: 500, + height: 450, + }); + }); + } else { + location.href = div.attr('objurl').replace(/%7Buuid%7D/, get_uuid(this)); + } }); $('div.grid.checkable table thead th.checkbox input[type=checkbox]').live('click', function() { - var checked = $(this).is(':checked'); - var table = $(this).parents('table:first'); - table.find('tbody tr').each(function() { - $(this).find('td.checkbox input[type=checkbox]').attr('checked', checked); - if (checked) { - $(this).addClass('selected'); - } else { - $(this).removeClass('selected'); - } - }); + var checked = $(this).is(':checked'); + var table = $(this).parents('table:first'); + table.find('tbody tr').each(function() { + $(this).find('td.checkbox input[type=checkbox]').attr('checked', checked); + if (checked) { + $(this).addClass('selected'); + } else { + $(this).removeClass('selected'); + } + }); }); $('div.grid.selectable table tbody tr').live('click', function() { - var table = $(this).parents('table:first'); - if (! table.hasClass('multiple')) { - table.find('tbody tr').removeClass('selected'); - } - $(this).addClass('selected'); + var table = $(this).parents('table:first'); + if (! table.hasClass('multiple')) { + table.find('tbody tr').removeClass('selected'); + } + $(this).addClass('selected'); }); $('div.grid.checkable table tbody tr').live('click', function() { - var checkbox = $(this).find('td:first input[type=checkbox]'); - checkbox.attr('checked', !checkbox.is(':checked')); - $(this).toggleClass('selected'); + var checkbox = $(this).find('td:first input[type=checkbox]'); + checkbox.attr('checked', !checkbox.is(':checked')); + $(this).toggleClass('selected'); }); $('div.grid td.delete').live('click', function() { - var grid = $(this).parents('div.grid:first'); - var url = grid.attr('delurl'); - if (url) { - if (confirm("Do you really wish to delete this object?")) { - location.href = url.replace(/%7Buuid%7D/, get_uuid(this)); - } - } else { - alert("Hm, I don't know how to delete that..\n\n" - + "(Add a 'delurl' parameter to the AlchemyGrid instance.)"); - } - return false; + var grid = $(this).parents('div.grid:first'); + var url = grid.attr('delurl'); + if (url) { + if (confirm("Do you really wish to delete this object?")) { + location.href = url.replace(/%7Buuid%7D/, get_uuid(this)); + } + } else { + alert("Hm, I don't know how to delete that..\n\n" + + "(Add a 'delurl' parameter to the AlchemyGrid instance.)"); + } + return false; }); $('#grid-page-count').live('change', function() { - var div = $(this).parents('div.grid:first'); - loading(div); - div.load(div.attr('url') + '&per_page=' + $(this).val()); + var div = $(this).parents('div.grid:first'); + loading(div); + url = div.attr('url'); + url += url.match(/\?/) ? '&' : '?'; + url += 'per_page=' + $(this).val(); + url += '&partial=true'; + div.load(url); }); $('button.autocomplete-change').live('click', function() { - var container = $(this).parents('div.autocomplete-container:first'); - container.find('div.autocomplete-display').hide(); - var textbox = container.find('input.autocomplete-textbox'); - textbox.show(); - textbox.select(); - textbox.focus(); + var container = $(this).parents('div.autocomplete-container:first'); + container.find('div.autocomplete-display').hide(); + var textbox = container.find('input.autocomplete-textbox'); + textbox.show(); + textbox.select(); + textbox.focus(); }); $('div.dialog form').live('submit', function() { - var form = $(this); - var dialog = form.parents('div.dialog:first'); - $.ajax({ - type: 'POST', - url: form.attr('action'), - data: form.serialize(), - success: function(data) { - if (json_success(data)) { - if (dialog.attr('callback')) { - eval(dialog.attr('callback'))(data); - } - dialog.dialog('close'); - } else if (typeof(data) == 'object') { - alert(data.message); - } else { - dialog.html(data); - } - }, - error: function() { - alert("Sorry, something went wrong...try again?"); - }, - }); - return false; + var form = $(this); + var dialog = form.parents('div.dialog:first'); + $.ajax({ + type: 'POST', + url: form.attr('action'), + data: form.serialize(), + success: function(data) { + if (json_success(data)) { + if (dialog.attr('callback')) { + eval(dialog.attr('callback'))(data); + } + dialog.dialog('close'); + } else if (typeof(data) == 'object') { + alert(data.message); + } else { + dialog.html(data); + } + }, + error: function() { + alert("Sorry, something went wrong...try again?"); + }, + }); + return false; }); $('div.dialog button.close').live('click', function() { - var dialog = $(this).parents('div.dialog:first'); - dialog.dialog('close'); + var dialog = $(this).parents('div.dialog:first'); + dialog.dialog('close'); }); $('div.dialog button.cancel').live('click', function() { - var dialog = $(this).parents('div.dialog:first'); - dialog.dialog('close'); + var dialog = $(this).parents('div.dialog:first'); + dialog.dialog('close'); }); $('div.dialog.lookup button.ok').live('click', function() { - var dialog = $(this).parents('div.dialog.lookup:first'); - var tr = dialog.find('div.grid table tbody tr.selected'); - if (! tr.length) { - alert("You haven't selected anything."); - return false; - } - var uuid = get_uuid(tr); - var col = parseInt(dialog.attr('textcol')); - var text = tr.find('td:eq('+col+')').html(); - eval(dialog.attr('callback'))(uuid, text); - dialog.dialog('close'); + var dialog = $(this).parents('div.dialog.lookup:first'); + var tr = dialog.find('div.grid table tbody tr.selected'); + if (! tr.length) { + alert("You haven't selected anything."); + return false; + } + var uuid = get_uuid(tr); + var col = parseInt(dialog.attr('textcol')); + var text = tr.find('td:eq('+col+')').html(); + eval(dialog.attr('callback'))(uuid, text); + dialog.dialog('close'); }); }); diff --git a/edbob/pyramid/templates/edbob/index.mako b/edbob/pyramid/templates/edbob/index.mako index 44d1af7..2aeae65 100644 --- a/edbob/pyramid/templates/edbob/index.mako +++ b/edbob/pyramid/templates/edbob/index.mako @@ -12,22 +12,26 @@
`` elements for each "action" - defined in the grid. - """ - - def get_class(text): - return text.lower().replace(' ', '-') - - res = '' - for action in self.config['actions']: - if isinstance(action, basestring): - text = action - cls = get_class(text) - else: - text = action[0] - if len(action) == 2: - cls = action[1] - else: - cls = get_class(text) - res += literal( - '%s
%s
- + % if search: + + % else: + + % endif
- ${search.render()|n} - + ${search.render()} +   -
    - ${self.context_menu_items()} -
+
    + ${self.context_menu_items()} +
- ${self.tools()} + ${self.tools()}
- ${grid|n} + ${grid}
diff --git a/edbob/pyramid/templates/forms/grid_readonly.mako b/edbob/pyramid/templates/forms/grid_readonly.mako index 72920ca..fab23d9 100644 --- a/edbob/pyramid/templates/forms/grid_readonly.mako +++ b/edbob/pyramid/templates/forms/grid_readonly.mako @@ -7,7 +7,7 @@ ${h.checkbox('check-all')} % endif % for field in grid.iter_fields(): - ${grid.th_sortable(field)|n} + ${grid.column_header(field)} % endfor % for col in grid.extra_columns: ${col.label} diff --git a/edbob/pyramid/templates/grids/grid.mako b/edbob/pyramid/templates/grids/grid.mako new file mode 100644 index 0000000..44ee62e --- /dev/null +++ b/edbob/pyramid/templates/grids/grid.mako @@ -0,0 +1,50 @@ +
+ + + + % if checkboxes: + + % endif + % for field in grid.iter_fields(): + ${grid.column_header(field)} + % endfor + % for col in grid.extra_columns: + + % endif + + + + % for i, row in enumerate(grid.iter_rows(), 1): + + % if grid.checkboxes: + + % endif + % for field in grid.iter_fields(): + + % endfor + % for col in grid.extra_columns: + + % endfor + % if grid.deletable: + + % endif + + % endfor + +
${h.checkbox('check-all')}${col.label} + % endfor + % if grid.deletable: +  
${grid.checkbox(row)}${grid.render_field(field)}${col.callback(row)} 
+ % if grid.pager: +
+

+ ${grid.pager.pager('showing $first_item thru $last_item of $item_count (page $page of $page_count)')} +

+ +
+ % endif +
diff --git a/edbob/pyramid/templates/grids/search.mako b/edbob/pyramid/templates/grids/search.mako new file mode 100644 index 0000000..17fbabd --- /dev/null +++ b/edbob/pyramid/templates/grids/search.mako @@ -0,0 +1,36 @@ +
+ ${search.begin()} + ${search.hidden('filters', 'true')} + <% visible = [] %> + % for f in search.sorted_filters(): + + % if search.config.get('include_filter_'+f.name): + <% visible.append(f.name) %> + % endif + % endfor +
+ ${search.add_filter(visible)} + ${search.submit('submit', "Search", style='display: none;' if not visible else None)} + +
+ ${search.end()} + % if visible: + + % endif +
diff --git a/edbob/pyramid/views/__init__.py b/edbob/pyramid/views/__init__.py index 1fed843..f51f74c 100644 --- a/edbob/pyramid/views/__init__.py +++ b/edbob/pyramid/views/__init__.py @@ -32,9 +32,10 @@ from pyramid.security import authenticated_userid from webhelpers.html import literal from webhelpers.html.tags import link_to +from edbob.pyramid.views.core import * from edbob.pyramid.views.autocomplete import * from edbob.pyramid.views.form import * -from edbob.pyramid.views.grid import * +from edbob.pyramid.views.grids import * def forbidden(request): diff --git a/edbob/pyramid/views/core.py b/edbob/pyramid/views/core.py new file mode 100644 index 0000000..8171679 --- /dev/null +++ b/edbob/pyramid/views/core.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.core`` -- Core Views +""" + + +__all__ = ['View'] + + +class View(object): + + def __init__(self, request): + self.request = request diff --git a/edbob/pyramid/views/grid.py b/edbob/pyramid/views/grid.py deleted file mode 100644 index 2dabe18..0000000 --- a/edbob/pyramid/views/grid.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.pyramid.views.grid`` -- Base Grid View -""" - -from edbob.pyramid import filters -from edbob.pyramid import forms -from edbob.pyramid import grids -from edbob.pyramid import Session -from edbob.util import requires_impl - - -__all__ = ['GridView'] - - -class GridView(object): - - @property - @requires_impl(is_property=True) - def mapped_class(self): - raise NotImplementedError - - @property - @requires_impl(is_property=True) - def route_name(self): - raise NotImplementedError - - @property - @requires_impl(is_property=True) - def route_prefix(self): - raise NotImplementedError - - def __init__(self, request): - self.request = request - - def join_map(self): - return {} - - def make_filter_map(self, **kwargs): - return filters.get_filter_map(self.mapped_class, **kwargs) - - def filter_map(self): - return self.make_filter_map() - - def make_search_config(self, fmap, **kwargs): - return filters.get_search_config(self.route_name, self.request, fmap, - **kwargs) - - def search_config(self, fmap): - return self.make_search_config(fmap) - - def make_search_form(self, config, **labels): - return filters.get_search_form(config, **labels) - - def search_form(self, config): - return self.make_search_form(config) - - def make_sort_map(self, *args, **kwargs): - return grids.get_sort_map(self.mapped_class, names=args or None, **kwargs) - - def sort_map(self): - return self.make_sort_map() - - def make_grid_config(self, search, fmap, **kwargs): - return grids.get_grid_config( - self.route_name, self.request, - search, filter_map=fmap, **kwargs) - - def grid_config(self, search, fmap): - return self.make_grid_config(search, fmap) - - def filter_query(self, q): - return q - - def make_query(self, config, jmap=None): - if jmap is None: - jmap = self.join_map() - smap = self.sort_map() - q = Session.query(self.mapped_class) - q = self.filter_query(q) - q = filters.filter_query(q, config, jmap) - q = grids.sort_query(q, config, smap, jmap) - return q - - def query(self, config): - return self.make_query(config) - - def make_grid(self, data, config, gridurl=None, objurl=None, delurl=None): - if not gridurl: - gridurl = self.request.route_url(self.route_name) - if not objurl: - objurl = '%s.edit' % self.route_prefix - if not delurl: - delurl = '%s.delete' % self.route_prefix - g = forms.AlchemyGrid( - self.mapped_class, data, config, - gridurl=gridurl, objurl=objurl, delurl=delurl) - return g - - def grid(self, data, config): - g = self.make_grid(data, config) - g.configure(readonly=True) - return g - - def __call__(self): - """ - View callable method. - """ - - fmap = self.filter_map() - config = self.search_config(fmap) - search = self.search_form(config) - config = self.grid_config(search, fmap) - grid = grids.get_pager(self.query, config) - - g = self.grid(grid, config) - cls = self.mapped_class.__name__ - if g.clickable: - cls = 'clickable %s' % cls - else: - cls = 'hoverable %s' % cls - grid = g.render(class_=cls) - return grids.render_grid(self.request, grid, search) - - @classmethod - def add_route(cls, config, route_name, url_prefix, template_prefix=None, permission=None): - if not template_prefix: - template_prefix = url_prefix - if not permission: - permission = route_name - config.add_route(route_name, url_prefix) - config.add_view(cls, route_name=route_name, renderer='%s/index.mako' % template_prefix, - permission=permission, http_cache=0) diff --git a/edbob/pyramid/views/grids/__init__.py b/edbob/pyramid/views/grids/__init__.py new file mode 100644 index 0000000..5e98506 --- /dev/null +++ b/edbob/pyramid/views/grids/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.grids`` -- Grid Views +""" + +from edbob.pyramid.views.grids.core import * +from edbob.pyramid.views.grids.alchemy import * diff --git a/edbob/pyramid/views/grids/alchemy.py b/edbob/pyramid/views/grids/alchemy.py new file mode 100644 index 0000000..8365842 --- /dev/null +++ b/edbob/pyramid/views/grids/alchemy.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.grids`` -- Grid Views +""" + +from webhelpers import paginate + +from edbob.pyramid import grids +from edbob.pyramid import Session +from edbob.pyramid.views.grids.core import GridView + + +__all__ = ['AlchemyGridView', 'SortableAlchemyGridView', + 'PagedAlchemyGridView', 'SearchableAlchemyGridView'] + + +class AlchemyGridView(GridView): + + def make_query(self): + q = Session.query(self.mapped_class) + return q + + def query(self): + return self.make_query() + + def make_grid(self, data, **kwargs): + kwargs.setdefault('partial_only', self.partial_only) + return grids.AlchemyGrid( + self.request, self.mapped_class, data, **kwargs) + + def grid(self, data): + return self.make_grid(data) + + def __call__(self): + query = self.query() + grid = self.grid(query) + return grids.util.render_grid(grid) + + +class SortableAlchemyGridView(AlchemyGridView): + + sort = None + + def join_map(self): + return {} + + def make_sort_map(self, *args, **kwargs): + return grids.util.get_sort_map( + self.mapped_class, names=args or None, **kwargs) + + def sort_map(self): + return self.make_sort_map() + + def make_sort_config(self, **kwargs): + return grids.util.get_sort_config( + self.route_name, self.request, **kwargs) + + def sort_config(self): + return self.make_sort_config(sort=self.sort) + + def make_query(self): + query = Session.query(self.mapped_class) + query = grids.util.sort_query( + query, self._sort_config, self.sort_map(), self.join_map()) + return query + + def query(self): + return self.make_query() + + def make_grid(self, **kwargs): + kwargs.setdefault('partial_only', self.partial_only) + return grids.AlchemyGrid( + self.request, self.mapped_class, self._data, + sort_map=self.sort_map(), config=self._sort_config, **kwargs) + + def grid(self): + return self.make_grid() + + def __call__(self): + self._sort_config = self.sort_config() + self._data = self.query() + grid = self.grid() + return grids.util.render_grid(grid) + + +class PagedAlchemyGridView(SortableAlchemyGridView): + + def make_pager(self): + config = self._sort_config + query = self.query() + return paginate.Page( + query, item_count=query.count(), + items_per_page=int(config['per_page']), + page=int(config['page']), + url=paginate.PageURL_WebOb(self.request)) + + def __call__(self): + self._sort_config = self.sort_config() + self._data = self.make_pager() + grid = self.grid() + grid.pager = self._data + return grids.util.render_grid(grid) + + +class SearchableAlchemyGridView(PagedAlchemyGridView): + + def make_filter_map(self, **kwargs): + return grids.search.get_filter_map(self.mapped_class, **kwargs) + + def filter_map(self): + return self.make_filter_map() + + def make_filter_config(self, **kwargs): + return grids.search.get_filter_config( + self.route_name, self.request, self.filter_map(), **kwargs) + + def filter_config(self): + return self.make_filter_config() + + def make_search_form(self): + return grids.search.get_search_form( + self.request, self.filter_map(), self._filter_config) + + def search_form(self): + return self.make_search_form() + + def make_query(self): + join_map = self.join_map() + query = Session.query(self.mapped_class) + query = grids.search.filter_query( + query, self._filter_config, self.filter_map(), join_map) + query = grids.util.sort_query( + query, self._sort_config, self.sort_map(), join_map) + return query + + def __call__(self): + self._filter_config = self.filter_config() + search = self.search_form() + self._sort_config = self.sort_config() + self._data = self.make_pager() + grid = self.grid() + grid.pager = self._data + return grids.util.render_grid(grid, search) diff --git a/edbob/pyramid/views/grids/core.py b/edbob/pyramid/views/grids/core.py new file mode 100644 index 0000000..3a47232 --- /dev/null +++ b/edbob/pyramid/views/grids/core.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.grids.core`` -- Core Grid View +""" + +from edbob.pyramid import grids +from edbob.pyramid.views.core import View + + +__all__ = ['GridView'] + + +class GridView(View): + + route_name = None + route_url = None + renderer = None + permission = None + partial_only = False + + def make_grid(self, **kwargs): + kwargs.setdefault('partial_only', self.partial_only) + return grids.Grid(self.request, **kwargs) + + def grid(self): + return self.make_grid() + + def __call__(self): + grid = self.grid() + return grids.util.render_grid(grid) + + @classmethod + def add_route(cls, config, route_name=None, route_url=None, renderer=None, permission=None): + route_name = route_name or cls.route_name + route_url = route_url or cls.route_url + renderer = renderer or cls.renderer + permission = permission or cls.permission + config.add_route(route_name, route_url) + config.add_view(cls, route_name=route_name, renderer=renderer, + permission=permission, http_cache=0)