massive grid overhaul
This commit is contained in:
parent
c788601572
commit
75d680a8b0
21 changed files with 1243 additions and 782 deletions
|
@ -42,15 +42,13 @@ from edbob.pyramid import Session, helpers
|
|||
from edbob.time import localize
|
||||
|
||||
from edbob.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']
|
||||
|
||||
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.forms.formalchemy.grid`` -- FormAlchemy Grid
|
||||
"""
|
||||
|
||||
from webhelpers import paginate
|
||||
from webhelpers.html.builder import format_attrs
|
||||
from webhelpers.html.tags import literal
|
||||
|
||||
import formalchemy
|
||||
|
||||
import edbob
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
__all__ = ['AlchemyGrid']
|
||||
|
||||
|
||||
class AlchemyGrid(formalchemy.Grid):
|
||||
"""
|
||||
Provides an "enhanced" version of the :class:`formalchemy.Grid` class.
|
||||
"""
|
||||
|
||||
prettify = staticmethod(prettify)
|
||||
|
||||
# uuid_key = None
|
||||
|
||||
# def __init__(self, cls, instances, config, url_kwargs={}, *args, **kwargs):
|
||||
# formalchemy.Grid.__init__(self, cls, instances, *args, **kwargs)
|
||||
# self.pager = instances if isinstance(instances, paginate.Page) else None
|
||||
# self.config = config
|
||||
# self.url_kwargs = url_kwargs
|
||||
# self.sortable = config.get('sortable', False)
|
||||
|
||||
def __init__(self, cls, instances, config, gridurl=None, objurl=None,
|
||||
delurl=None, **kwargs):
|
||||
"""
|
||||
Grid constructor.
|
||||
|
||||
``url`` must be the URL used to access the grid itself. This url/view
|
||||
must accept a GET query string parameter of "partial=True", which will
|
||||
indicate that the grid *only* is being requested, as opposed to the
|
||||
full page in which the grid normally resides.
|
||||
"""
|
||||
|
||||
formalchemy.Grid.__init__(self, cls, instances, **kwargs)
|
||||
self.config = config
|
||||
self.request = config['request']
|
||||
self.gridurl = gridurl
|
||||
self.objurl = objurl
|
||||
self.delurl = delurl
|
||||
self.sortable = config.get('sortable', False)
|
||||
self.clickable = config.get('clickable', False)
|
||||
self.deletable = config.get('deletable', False)
|
||||
self.pager = instances if isinstance(instances, paginate.Page) else None
|
||||
self.extra_columns = []
|
||||
|
||||
def add_column(self, name, label, callback):
|
||||
self.extra_columns.append(
|
||||
edbob.Object(name=name, label=label, callback=callback))
|
||||
|
||||
def field_name(self, field):
|
||||
return field.name
|
||||
|
||||
def iter_fields(self):
|
||||
for field in self.render_fields.itervalues():
|
||||
yield field
|
||||
|
||||
def render_field(self, field, readonly):
|
||||
if readonly:
|
||||
return field.render_readonly()
|
||||
return field.render()
|
||||
|
||||
def row_attrs(self, i):
|
||||
attrs = dict(class_='even' if i % 2 else 'odd')
|
||||
if hasattr(self.model, 'uuid'):
|
||||
attrs['uuid'] = self.model.uuid
|
||||
return format_attrs(**attrs)
|
||||
|
||||
def url_attrs(self):
|
||||
attrs = {}
|
||||
url = self.request.route_url
|
||||
if self.gridurl:
|
||||
attrs['url'] = self.gridurl
|
||||
if self.objurl:
|
||||
attrs['objurl'] = url(self.objurl, uuid='{uuid}')
|
||||
if self.delurl:
|
||||
attrs['delurl'] = url(self.delurl, uuid='{uuid}')
|
||||
return format_attrs(**attrs)
|
||||
|
||||
# def render(self, class_=None, **kwargs):
|
||||
# """
|
||||
# Renders the grid into HTML, and returns the result.
|
||||
|
||||
# ``class_`` (if provided) is used to define the class of the ``<div>``
|
||||
# (wrapper) and ``<table>`` elements of the grid.
|
||||
|
||||
# Any remaining ``kwargs`` are passed directly to the underlying
|
||||
# ``formalchemy.Grid.render()`` method.
|
||||
# """
|
||||
|
||||
# kwargs['class_'] = class_
|
||||
# # kwargs.setdefault('get_uuid', self.get_uuid)
|
||||
# kwargs.setdefault('checkboxes', False)
|
||||
# return formalchemy.Grid.render(self, **kwargs)
|
||||
|
||||
def render(self, **kwargs):
|
||||
engine = self.engine or formalchemy.config.engine
|
||||
if self.readonly:
|
||||
return engine('grid_readonly', grid=self, **kwargs)
|
||||
kwargs.setdefault('request', self._request)
|
||||
return engine('grid', grid=self, **kwargs)
|
||||
|
||||
def th_sortable(self, field):
|
||||
class_ = ''
|
||||
label = field.label()
|
||||
if self.sortable and field.key in self.config.get('sort_map', {}):
|
||||
class_ = 'sortable'
|
||||
if field.key == self.config['sort']:
|
||||
class_ += ' sorted ' + self.config['dir']
|
||||
label = literal('<a href="#">') + label + literal('</a>')
|
||||
if class_:
|
||||
class_ = ' class="%s"' % class_
|
||||
return literal('<th' + class_ + ' field="' + field.key + '">') + label + literal('</th>')
|
||||
|
||||
# def url(self):
|
||||
# # TODO: Probably clean this up somehow...
|
||||
# if self.pager is not None:
|
||||
# u = self.pager._url_generator(self.pager.page, partial=True)
|
||||
# else:
|
||||
# u = self._url or ''
|
||||
# qs = self.query_string()
|
||||
# if qs:
|
||||
# if '?' not in u:
|
||||
# u += '?'
|
||||
# u += qs
|
||||
# elif '?' not in u:
|
||||
# u += '?partial=True'
|
||||
# return u
|
||||
|
||||
# def query_string(self):
|
||||
# # TODO: Probably clean this up somehow...
|
||||
# qs = ''
|
||||
# if self.url_kwargs:
|
||||
# for k, v in self.url_kwargs.items():
|
||||
# qs += '&%s=%s' % (urllib.quote_plus(k), urllib.quote_plus(v))
|
||||
# return qs
|
||||
|
||||
def get_actions(self):
|
||||
"""
|
||||
Returns an HTML snippet containing ``<td>`` elements for each "action"
|
||||
defined in the grid.
|
||||
"""
|
||||
|
||||
def get_class(text):
|
||||
return text.lower().replace(' ', '-')
|
||||
|
||||
res = ''
|
||||
for action in self.config['actions']:
|
||||
if isinstance(action, basestring):
|
||||
text = action
|
||||
cls = get_class(text)
|
||||
else:
|
||||
text = action[0]
|
||||
if len(action) == 2:
|
||||
cls = action[1]
|
||||
else:
|
||||
cls = get_class(text)
|
||||
res += literal(
|
||||
'<td class="action %s"><a href="#">%s</a></td>' %
|
||||
(cls, text))
|
||||
return res
|
||||
|
||||
# def get_uuid(self):
|
||||
# """
|
||||
# .. highlight:: none
|
||||
|
||||
# Returns a unique identifier for a given record, in the form of an HTML
|
||||
# attribute for direct inclusion in a ``<tr>`` element within a template.
|
||||
# An example of what this function might return would be the string::
|
||||
|
||||
# 'uuid="420"'
|
||||
|
||||
# Rattail itself will tend to use *universally-unique* IDs (true UUIDs),
|
||||
# but this method may be overridden to support legacy databases with
|
||||
# auto-increment IDs, etc. Really the only important thing is that the
|
||||
# value returned be unique across the relevant data set.
|
||||
|
||||
# If the concept is unsupported, the method should return an empty
|
||||
# string.
|
||||
# """
|
||||
|
||||
# def uuid():
|
||||
# if self.uuid_key and hasattr(self.model, self.uuid_key):
|
||||
# return getattr(self.model, self.uuid_key)
|
||||
# if hasattr(self.model, 'uuid'):
|
||||
# return getattr(self.model, 'uuid')
|
||||
# if hasattr(self.model, 'id'):
|
||||
# return getattr(self.model, 'id')
|
||||
|
||||
# uuid = uuid()
|
||||
# if uuid:
|
||||
# return literal('uuid="%s"' % uuid)
|
||||
# return ''
|
|
@ -1,204 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids`` -- Grid Tables
|
||||
"""
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
from sqlalchemy.orm import Query
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
|
||||
from pyramid.renderers import render
|
||||
from pyramid.response import Response
|
||||
from webhelpers import paginate
|
||||
from webhelpers.html import literal
|
||||
from webhelpers.html.builder import format_attrs
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid.filters import SearchFormRenderer
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
class BasicGrid(edbob.Object):
|
||||
"""
|
||||
Basic grid class for those times when SQLAlchemy is not needed.
|
||||
"""
|
||||
|
||||
def __init__(self, columns, rows, config, url, sortable=True, deletable=False, **kwargs):
|
||||
edbob.Object.__init__(self, **kwargs)
|
||||
self.rows = rows
|
||||
self.config = config
|
||||
self.url = url
|
||||
self.sortable = sortable
|
||||
self.deletable = deletable
|
||||
self.columns = OrderedDict()
|
||||
for col in columns:
|
||||
if isinstance(col, (tuple, list)):
|
||||
if len(col) == 2:
|
||||
self.columns[col[0]] = col[1]
|
||||
continue
|
||||
elif isinstance(col, basestring):
|
||||
self.columns[col] = prettify(col)
|
||||
continue
|
||||
raise ValueError("Column element must be either a string or 2-tuple")
|
||||
|
||||
def _set_active(self, row):
|
||||
self.model = {}
|
||||
for i, col in enumerate(self.columns.keys()):
|
||||
if i >= len(row):
|
||||
break
|
||||
self.model[col] = row[i]
|
||||
|
||||
def field_label(self, name):
|
||||
return self.columns[name]
|
||||
|
||||
def field_name(self, field):
|
||||
return field
|
||||
|
||||
def iter_fields(self):
|
||||
for col in self.columns.keys():
|
||||
yield col
|
||||
|
||||
def render(self, **kwargs):
|
||||
kwargs['grid'] = self
|
||||
return render('forms/grid_readonly.mako', kwargs)
|
||||
|
||||
def render_field(self, field, readonly):
|
||||
return self.model[field]
|
||||
|
||||
def row_attrs(self, i):
|
||||
return format_attrs(class_='even' if i % 2 else 'odd')
|
||||
|
||||
def th_sortable(self, field):
|
||||
class_ = ''
|
||||
label = self.field_label(field)
|
||||
if self.sortable and field in self.config.get('sort_map', {}):
|
||||
class_ = 'sortable'
|
||||
if field == self.config['sort']:
|
||||
class_ += ' sorted ' + self.config['dir']
|
||||
label = literal('<a href="#">') + label + literal('</a>')
|
||||
if class_:
|
||||
class_ = ' class="%s"' % class_
|
||||
return literal('<th' + class_ + ' field="' + field + '">') + label + literal('</th>')
|
||||
|
||||
def url_attrs(self):
|
||||
return format_attrs(url=self.url)
|
||||
|
||||
|
||||
def get_grid_config(name, request, search=None, url=None, **kwargs):
|
||||
config = {
|
||||
'actions': [],
|
||||
'per_page': 20,
|
||||
'page': 1,
|
||||
'sortable': True,
|
||||
'dir': 'asc',
|
||||
'object_url': '',
|
||||
'deletable': False,
|
||||
'delete_url': '',
|
||||
'use_dialog': False,
|
||||
}
|
||||
config.update(kwargs)
|
||||
# words = name.split('.')
|
||||
# if len(words) == 2:
|
||||
# config.setdefault('object_url', request.route_url(words[0], action='crud'))
|
||||
# config.setdefault('delete_url', config['object_url'])
|
||||
for key in config:
|
||||
full_key = name+'_'+key
|
||||
if request.params.get(key):
|
||||
value = request.params[key]
|
||||
config[key] = value
|
||||
request.session[full_key] = value
|
||||
elif request.session.get(full_key):
|
||||
value = request.session[full_key]
|
||||
config[key] = value
|
||||
config['request'] = request
|
||||
config['search'] = search
|
||||
config['url'] = url
|
||||
return config
|
||||
|
||||
|
||||
def get_pager(query, config):
|
||||
query = query(config)
|
||||
count = None
|
||||
if isinstance(query, Query):
|
||||
count = query.count()
|
||||
return paginate.Page(
|
||||
query, item_count=count,
|
||||
items_per_page=int(config['per_page']),
|
||||
page=int(config['page']),
|
||||
url=paginate.PageURL(config['url'], {}),
|
||||
)
|
||||
|
||||
|
||||
def get_sort_map(cls, names=None, **kwargs):
|
||||
"""
|
||||
Convenience function which returns a sort map.
|
||||
"""
|
||||
|
||||
smap = {}
|
||||
if names is None:
|
||||
names = []
|
||||
for attr in cls.__dict__:
|
||||
obj = getattr(cls, attr)
|
||||
if isinstance(obj, InstrumentedAttribute):
|
||||
if obj.key != 'uuid':
|
||||
names.append(obj.key)
|
||||
for name in names:
|
||||
smap[name] = sorter(getattr(cls, name))
|
||||
smap.update(kwargs)
|
||||
return smap
|
||||
|
||||
|
||||
def render_grid(request, grid, search=None, **kwargs):
|
||||
if request.params.get('partial'):
|
||||
return Response(body=grid, content_type='text/html')
|
||||
kwargs['grid'] = grid
|
||||
if search:
|
||||
kwargs['search'] = SearchFormRenderer(search)
|
||||
return kwargs
|
||||
|
||||
|
||||
def sort_query(query, config, sort_map, join_map={}):
|
||||
field = config.get('sort')
|
||||
if not field:
|
||||
return query
|
||||
joins = config.setdefault('joins', [])
|
||||
if field in join_map and field not in joins:
|
||||
query = join_map[field](query)
|
||||
joins.append(field)
|
||||
config['sort_map'] = sort_map
|
||||
return sort_map[field](query, config['dir'])
|
||||
|
||||
|
||||
def sorter(field):
|
||||
"""
|
||||
Returns a function suitable for a sort map callable, with typical
|
||||
logic built in for sorting applied to ``field``.
|
||||
"""
|
||||
return lambda q, d: q.order_by(getattr(field, d)())
|
32
edbob/pyramid/grids/__init__.py
Normal file
32
edbob/pyramid/grids/__init__.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids`` -- Grids
|
||||
"""
|
||||
|
||||
from edbob.pyramid.grids.core import *
|
||||
from edbob.pyramid.grids.alchemy import *
|
||||
from edbob.pyramid.grids import util
|
||||
from edbob.pyramid.grids import search
|
103
edbob/pyramid/grids/alchemy.py
Normal file
103
edbob/pyramid/grids/alchemy.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes
|
||||
"""
|
||||
|
||||
from webhelpers.html import literal
|
||||
from webhelpers.html import tags
|
||||
|
||||
import formalchemy
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.grids.core import Grid
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
__all__ = ['AlchemyGrid']
|
||||
|
||||
|
||||
class AlchemyGrid(Grid):
|
||||
|
||||
sort_map = {}
|
||||
|
||||
pager = None
|
||||
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
|
||||
|
||||
def __init__(self, request, cls, instances, **kwargs):
|
||||
super(AlchemyGrid, self).__init__(request, **kwargs)
|
||||
self._formalchemy_grid = formalchemy.Grid(
|
||||
cls, instances, session=Session(), request=request)
|
||||
self._formalchemy_grid.prettify = prettify
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._formalchemy_grid, attr)
|
||||
|
||||
def column_header(self, field):
|
||||
cls = ''
|
||||
label = field.label()
|
||||
if field.key in self.sort_map:
|
||||
cls = 'sortable'
|
||||
if field.key == self.config['sort']:
|
||||
cls += ' sorted ' + self.config['dir']
|
||||
label = tags.link_to(label, '#')
|
||||
if cls:
|
||||
cls = ' class="%s"' % cls
|
||||
return literal('<th%s field="%s">' % (cls, field.key)) + label + literal('</th>')
|
||||
|
||||
def iter_fields(self):
|
||||
return self._formalchemy_grid.render_fields.itervalues()
|
||||
|
||||
def iter_rows(self):
|
||||
for row in self._formalchemy_grid.rows:
|
||||
self._formalchemy_grid._set_active(row)
|
||||
yield row
|
||||
|
||||
def page_count_options(self):
|
||||
options = edbob.config.get('edbob.pyramid', 'grid.page_count_options')
|
||||
if options:
|
||||
options = options.split(',')
|
||||
options = [int(x.strip()) for x in options]
|
||||
else:
|
||||
options = [5, 10, 20, 50, 100]
|
||||
return options
|
||||
|
||||
def page_links(self):
|
||||
return self.pager.pager(self.pager_format,
|
||||
symbol_next='next',
|
||||
symbol_previous='prev',
|
||||
onclick="grid_navigate_page(this, '$partial_url'); return false;")
|
||||
|
||||
def render_field(self, field):
|
||||
if self._formalchemy_grid.readonly:
|
||||
return field.render_readonly()
|
||||
return field.render()
|
||||
|
||||
def _row_attrs(self, row, i):
|
||||
attrs = super(AlchemyGrid, self)._row_attrs(row, i)
|
||||
if hasattr(row, 'uuid'):
|
||||
attrs['uuid'] = row.uuid
|
||||
return attrs
|
98
edbob/pyramid/grids/core.py
Normal file
98
edbob/pyramid/grids/core.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids.core`` -- Core Grid Classes
|
||||
"""
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
from webhelpers.html import literal
|
||||
from webhelpers.html.builder import format_attrs
|
||||
|
||||
from pyramid.renderers import render
|
||||
|
||||
import edbob
|
||||
|
||||
|
||||
__all__ = ['Grid']
|
||||
|
||||
|
||||
class Grid(edbob.Object):
|
||||
|
||||
hoverable = True
|
||||
clickable = False
|
||||
checkboxes = False
|
||||
deletable = False
|
||||
partial_only = False
|
||||
|
||||
def __init__(self, request, **kwargs):
|
||||
kwargs.setdefault('fields', OrderedDict())
|
||||
kwargs.setdefault('extra_columns', [])
|
||||
super(Grid, self).__init__(**kwargs)
|
||||
self.request = request
|
||||
|
||||
def column_header(self, field):
|
||||
return literal('<th field="%s">%s</th>' % (field.name, field.label))
|
||||
|
||||
def div_class(self):
|
||||
if self.clickable:
|
||||
return 'grid clickable'
|
||||
if self.hoverable:
|
||||
return 'grid hoverable'
|
||||
return 'grid'
|
||||
|
||||
def _div_attrs(self):
|
||||
attrs = {'class_':'grid', 'url':self.request.current_route_url()}
|
||||
if self.clickable:
|
||||
attrs['class_'] = 'grid clickable'
|
||||
elif self.hoverable:
|
||||
attrs['class_'] = 'grid hoverable'
|
||||
return attrs
|
||||
|
||||
def div_attrs(self):
|
||||
return format_attrs(**self._div_attrs())
|
||||
|
||||
def iter_fields(self):
|
||||
return self.fields.itervalues()
|
||||
|
||||
def iter_rows(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def render(self, template='/grids/grid.mako', **kwargs):
|
||||
kwargs.setdefault('grid', self)
|
||||
return render(template, kwargs)
|
||||
|
||||
def render_field(self, field):
|
||||
raise NotImplementedError
|
||||
|
||||
def _row_attrs(self, row, i):
|
||||
attrs = {'class_': 'odd' if i % 2 else 'even'}
|
||||
return attrs
|
||||
|
||||
def row_attrs(self, row, i):
|
||||
return format_attrs(**self._row_attrs(row, i))
|
272
edbob/pyramid/grids/search.py
Normal file
272
edbob/pyramid/grids/search.py
Normal file
|
@ -0,0 +1,272 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids.search`` -- Grid Search Filters
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from webhelpers.html import tags
|
||||
from webhelpers.html import literal
|
||||
|
||||
from pyramid.renderers import render
|
||||
from pyramid_simpleform import Form
|
||||
from pyramid_simpleform.renderers import FormRenderer
|
||||
|
||||
import edbob
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
class SearchFilter(edbob.Object):
|
||||
"""
|
||||
Base class and default implementation for search filters.
|
||||
"""
|
||||
|
||||
def __init__(self, name, label=None, **kwargs):
|
||||
edbob.Object.__init__(self, **kwargs)
|
||||
self.name = name
|
||||
self.label = label or prettify(name)
|
||||
|
||||
def types_select(self):
|
||||
types = [
|
||||
('is', "is"),
|
||||
('nt', "is not"),
|
||||
('lk', "contains"),
|
||||
('nl', "doesn't contain"),
|
||||
]
|
||||
options = []
|
||||
filter_map = self.search.filter_map[self.name]
|
||||
for value, label in types:
|
||||
if value in filter_map:
|
||||
options.append((value, label))
|
||||
return tags.select('filter_type_'+self.name,
|
||||
self.search.config.get('filter_type_'+self.name),
|
||||
options, class_='filter-type')
|
||||
|
||||
def value_control(self):
|
||||
return tags.text(self.name, self.search.config.get(self.name))
|
||||
|
||||
|
||||
class SearchForm(Form):
|
||||
"""
|
||||
Generic form class which aggregates :class:`SearchFilter` instances.
|
||||
"""
|
||||
|
||||
def __init__(self, request, filter_map, config, *args, **kwargs):
|
||||
super(SearchForm, self).__init__(request, *args, **kwargs)
|
||||
self.filter_map = filter_map
|
||||
self.config = config
|
||||
self.filters = {}
|
||||
|
||||
def add_filter(self, filter_):
|
||||
filter_.search = self
|
||||
self.filters[filter_.name] = filter_
|
||||
|
||||
|
||||
class SearchFormRenderer(FormRenderer):
|
||||
"""
|
||||
Renderer for :class:`SearchForm` instances.
|
||||
"""
|
||||
|
||||
def __init__(self, form, *args, **kwargs):
|
||||
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
|
||||
self.request = form.request
|
||||
self.filters = form.filters
|
||||
self.config = form.config
|
||||
|
||||
def add_filter(self, visible):
|
||||
options = ['add a filter']
|
||||
for f in self.sorted_filters():
|
||||
options.append((f.name, f.label))
|
||||
return self.select('add-filter', options,
|
||||
style='display: none;' if len(visible) == len(self.filters) else None)
|
||||
|
||||
def checkbox(self, name, checked=None, **kwargs):
|
||||
if name.startswith('include_filter_'):
|
||||
if checked is None:
|
||||
checked = self.config[name]
|
||||
return tags.checkbox(name, checked=checked, **kwargs)
|
||||
if checked is None:
|
||||
checked = False
|
||||
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
|
||||
|
||||
def render(self, **kwargs):
|
||||
kwargs['search'] = self
|
||||
return literal(render('/grids/search.mako', kwargs))
|
||||
|
||||
def sorted_filters(self):
|
||||
return sorted(self.filters.values(), key=lambda x: x.label)
|
||||
|
||||
def text(self, name, **kwargs):
|
||||
return tags.text(name, value=self.config.get(name), **kwargs)
|
||||
|
||||
|
||||
def filter_exact(field):
|
||||
"""
|
||||
Convenience function which returns a filter map entry, with typical logic
|
||||
built in for "exact match" queries applied to ``field``.
|
||||
"""
|
||||
|
||||
return {
|
||||
'is':
|
||||
lambda q, v: q.filter(field == v) if v else q,
|
||||
'nt':
|
||||
lambda q, v: q.filter(field != v) if v else q,
|
||||
}
|
||||
|
||||
|
||||
def filter_ilike(field):
|
||||
"""
|
||||
Convenience function which returns a filter map entry, with typical logic
|
||||
built in for "ILIKE" queries applied to ``field``.
|
||||
"""
|
||||
|
||||
return {
|
||||
'lk':
|
||||
lambda q, v: q.filter(field.ilike('%%%s%%' % v)) if v else q,
|
||||
'nl':
|
||||
lambda q, v: q.filter(~field.ilike('%%%s%%' % v)) if v else q,
|
||||
}
|
||||
|
||||
|
||||
def get_filter_config(name, request, filter_map, **kwargs):
|
||||
"""
|
||||
Returns a configuration dictionary for a search form.
|
||||
"""
|
||||
|
||||
config = {}
|
||||
|
||||
def update_config(dict_, prefix='', exclude_by_default=False):
|
||||
"""
|
||||
Updates the ``config`` dictionary based on the contents of ``dict_``.
|
||||
"""
|
||||
|
||||
for field in filter_map:
|
||||
if prefix+'include_filter_'+field in dict_:
|
||||
include = dict_[prefix+'include_filter_'+field]
|
||||
include = bool(include) and include != '0'
|
||||
config['include_filter_'+field] = include
|
||||
elif exclude_by_default:
|
||||
config['include_filter_'+field] = False
|
||||
if prefix+'filter_type_'+field in dict_:
|
||||
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
|
||||
if prefix+field in dict_:
|
||||
config[field] = dict_[prefix+field]
|
||||
|
||||
# Update config to exclude all filters by default.
|
||||
for field in filter_map:
|
||||
config['include_filter_'+field] = False
|
||||
|
||||
# Update config to honor default settings.
|
||||
config.update(kwargs)
|
||||
|
||||
# Update config with data cached in session.
|
||||
update_config(request.session, prefix=name+'.')
|
||||
|
||||
# Update config with data from GET/POST request.
|
||||
if request.params.get('filters') == 'true':
|
||||
update_config(request.params, exclude_by_default=True)
|
||||
|
||||
# Cache filter data in session.
|
||||
for key in config:
|
||||
if (not key.startswith('filter_factory_')
|
||||
and not key.startswith('filter_label_')):
|
||||
request.session[name+'.'+key] = config[key]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
|
||||
"""
|
||||
Convenience function which returns a "filter map" for ``cls``.
|
||||
|
||||
``exact``, if provided, should be a list of field names for which "exact"
|
||||
filtering is to be allowed.
|
||||
|
||||
``ilike``, if provided, should be a list of field names for which "ILIKE"
|
||||
filtering is to be allowed.
|
||||
|
||||
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
|
||||
and are added directly to the map.
|
||||
"""
|
||||
|
||||
fmap = {}
|
||||
for name in exact:
|
||||
fmap[name] = filter_exact(getattr(cls, name))
|
||||
for name in ilike:
|
||||
fmap[name] = filter_ilike(getattr(cls, name))
|
||||
fmap.update(kwargs)
|
||||
return fmap
|
||||
|
||||
|
||||
def get_search_form(request, filter_map, config):
|
||||
"""
|
||||
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
|
||||
each filter in ``filter_map``, using configuration from ``config``.
|
||||
"""
|
||||
|
||||
search = SearchForm(request, filter_map, config)
|
||||
for field in filter_map:
|
||||
factory = config.get('filter_factory_%s' % field, SearchFilter)
|
||||
label = config.get('filter_label_%s' % field)
|
||||
search.add_filter(factory(field, label=label))
|
||||
return search
|
||||
|
||||
|
||||
# def filter_query(query, config, join_map={}):
|
||||
# filter_map = config['filter_map']
|
||||
# if config.get('search'):
|
||||
# search = config['search'].config
|
||||
# joins = config.setdefault('joins', [])
|
||||
# include_filter = re.compile(r'^include_filter_(.*)$')
|
||||
# for key in search:
|
||||
# m = include_filter.match(key)
|
||||
# if m and search[key]:
|
||||
# field = m.group(1)
|
||||
# if field in join_map and field not in joins:
|
||||
# query = join_map[field](query)
|
||||
# joins.append(field)
|
||||
# value = search.get(field)
|
||||
# if value:
|
||||
# f = filter_map[field][search['filter_type_'+field]]
|
||||
# query = f(query, value)
|
||||
# return query
|
||||
|
||||
|
||||
def filter_query(query, config, filter_map, join_map):
|
||||
joins = config.setdefault('joins', [])
|
||||
include_filter = re.compile(r'^include_filter_(.*)$')
|
||||
for key in config:
|
||||
m = include_filter.match(key)
|
||||
if m and config[key]:
|
||||
field = m.group(1)
|
||||
if field in join_map and field not in joins:
|
||||
query = join_map[field](query)
|
||||
joins.append(field)
|
||||
value = config.get(field)
|
||||
if value:
|
||||
f = filter_map[field][config['filter_type_'+field]]
|
||||
query = f(query, value)
|
||||
return query
|
138
edbob/pyramid/grids/util.py
Normal file
138
edbob/pyramid/grids/util.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.grids.util`` -- Grid Utilities
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
|
||||
from webhelpers.html import literal
|
||||
|
||||
from pyramid.response import Response
|
||||
|
||||
from edbob.pyramid.grids.search import SearchFormRenderer
|
||||
|
||||
|
||||
def get_sort_config(name, request, **kwargs):
|
||||
"""
|
||||
Returns a configuration dictionary for grid sorting.
|
||||
"""
|
||||
|
||||
# Initial config uses some default values.
|
||||
config = {
|
||||
'dir': 'asc',
|
||||
'per_page': 20,
|
||||
'page': 1,
|
||||
}
|
||||
|
||||
# Override with defaults provided by caller.
|
||||
config.update(kwargs)
|
||||
|
||||
# Override with values from GET/POST request and/or session.
|
||||
for key in config:
|
||||
full_key = name+'_'+key
|
||||
if request.params.get(key):
|
||||
value = request.params[key]
|
||||
config[key] = value
|
||||
request.session[full_key] = value
|
||||
elif request.session.get(full_key):
|
||||
value = request.session[full_key]
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_sort_map(cls, names=None, **kwargs):
|
||||
"""
|
||||
Convenience function which returns a sort map for ``cls``.
|
||||
|
||||
If ``names`` is not specified, the map will include all "standard" fields
|
||||
present on the mapped class. Otherwise, the map will be limited to only
|
||||
the fields which are named.
|
||||
|
||||
All remaining ``kwargs`` are assumed to be sort map entries, and will be
|
||||
added to the map directly.
|
||||
"""
|
||||
|
||||
smap = {}
|
||||
if names is None:
|
||||
names = []
|
||||
for attr in cls.__dict__:
|
||||
obj = getattr(cls, attr)
|
||||
if isinstance(obj, InstrumentedAttribute):
|
||||
if obj.key != 'uuid':
|
||||
names.append(obj.key)
|
||||
for name in names:
|
||||
smap[name] = sorter(getattr(cls, name))
|
||||
smap.update(kwargs)
|
||||
return smap
|
||||
|
||||
|
||||
def render_grid(grid, search_form=None, **kwargs):
|
||||
"""
|
||||
Convenience function to render ``grid`` (which should be a
|
||||
:class:`edbob.pyramid.grids.Grid` instance).
|
||||
|
||||
This "usually" will return a dictionary to be used as context for rendering
|
||||
the final view template.
|
||||
|
||||
However, if a partial grid is requested (or mandated), then the grid body
|
||||
will be rendered and a :class:`pyramid.response.Response` object will be
|
||||
returned instead.
|
||||
"""
|
||||
|
||||
if grid.partial_only or grid.request.params.get('partial'):
|
||||
return Response(body=grid.render(), content_type='text/html')
|
||||
kwargs['grid'] = literal(grid.render())
|
||||
if search_form:
|
||||
kwargs['search'] = SearchFormRenderer(search_form)
|
||||
return kwargs
|
||||
|
||||
|
||||
def sort_query(query, config, sort_map, join_map={}):
|
||||
"""
|
||||
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
|
||||
used, if necessary, to join additional tables to the base query. The
|
||||
sorted query is returned.
|
||||
"""
|
||||
|
||||
field = config.get('sort')
|
||||
if not field:
|
||||
return query
|
||||
joins = config.setdefault('joins', [])
|
||||
if field in join_map and field not in joins:
|
||||
query = join_map[field](query)
|
||||
joins.append(field)
|
||||
sort = sort_map[field]
|
||||
return sort(query, config['dir'])
|
||||
|
||||
|
||||
def sorter(field):
|
||||
"""
|
||||
Returns a function suitable for a sort map callable, with typical logic
|
||||
built in for sorting applied to ``field``.
|
||||
"""
|
||||
|
||||
return lambda q, d: q.order_by(getattr(field, d)())
|
|
@ -181,6 +181,15 @@ div.dialog {
|
|||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Filters
|
||||
******************************/
|
||||
|
||||
div.filters div.filter div.value {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Grids
|
||||
******************************/
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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"> </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>
|
||||
|
|
|
@ -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>
|
||||
|
|
50
edbob/pyramid/templates/grids/grid.mako
Normal file
50
edbob/pyramid/templates/grids/grid.mako
Normal file
|
@ -0,0 +1,50 @@
|
|||
<div ${grid.div_attrs()}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
% if checkboxes:
|
||||
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||
% endif
|
||||
% for field in grid.iter_fields():
|
||||
${grid.column_header(field)}
|
||||
% endfor
|
||||
% for col in grid.extra_columns:
|
||||
<th>${col.label}</td>
|
||||
% endfor
|
||||
% if grid.deletable:
|
||||
<th> </th>
|
||||
% endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, row in enumerate(grid.iter_rows(), 1):
|
||||
<tr ${grid.row_attrs(row, i)}>
|
||||
% if grid.checkboxes:
|
||||
<td class="checkbox">${grid.checkbox(row)}</td>
|
||||
% endif
|
||||
% for field in grid.iter_fields():
|
||||
<td class="${field.name}">${grid.render_field(field)}</td>
|
||||
% endfor
|
||||
% for col in grid.extra_columns:
|
||||
<td class="${col.name}">${col.callback(row)}</td>
|
||||
% endfor
|
||||
% if grid.deletable:
|
||||
<td class="delete"> </td>
|
||||
% endif
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% if grid.pager:
|
||||
<div class="pager">
|
||||
<p class="showing">
|
||||
${grid.pager.pager('showing $first_item thru $last_item of $item_count (page $page of $page_count)')}
|
||||
</p>
|
||||
<p class="page-links">
|
||||
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
|
||||
per page
|
||||
${grid.page_links()}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
36
edbob/pyramid/templates/grids/search.mako
Normal file
36
edbob/pyramid/templates/grids/search.mako
Normal file
|
@ -0,0 +1,36 @@
|
|||
<div class="filters" url="${search.request.current_route_url()}">
|
||||
${search.begin()}
|
||||
${search.hidden('filters', 'true')}
|
||||
<% visible = [] %>
|
||||
% for f in search.sorted_filters():
|
||||
<div class="filter" id="filter-${f.name}"${' style="display: none;"' if not search.config.get('include_filter_'+f.name) else ''|n}>
|
||||
${search.checkbox('include_filter_'+f.name)}
|
||||
<label for="${f.name}">${f.label}</label>
|
||||
${f.types_select()}
|
||||
<div class="value">
|
||||
${f.value_control()}
|
||||
</div>
|
||||
</div>
|
||||
% if search.config.get('include_filter_'+f.name):
|
||||
<% visible.append(f.name) %>
|
||||
% endif
|
||||
% endfor
|
||||
<div class="buttons">
|
||||
${search.add_filter(visible)}
|
||||
${search.submit('submit', "Search", style='display: none;' if not visible else None)}
|
||||
<button type="reset"${' style="display: none;"' if not visible else ''|n}>Reset</button>
|
||||
</div>
|
||||
${search.end()}
|
||||
% if visible:
|
||||
<script language="javascript" type="text/javascript">
|
||||
filters_to_disable = [
|
||||
% for field in visible:
|
||||
'${field}',
|
||||
% endfor
|
||||
];
|
||||
$(function() {
|
||||
disable_filter_options();
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
</div>
|
|
@ -32,9 +32,10 @@ from pyramid.security import authenticated_userid
|
|||
from webhelpers.html import literal
|
||||
from webhelpers.html.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):
|
||||
|
|
36
edbob/pyramid/views/core.py
Normal file
36
edbob/pyramid/views/core.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.views.core`` -- Core Views
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ['View']
|
||||
|
||||
|
||||
class View(object):
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
|
@ -1,156 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.views.grid`` -- Base Grid View
|
||||
"""
|
||||
|
||||
from edbob.pyramid import filters
|
||||
from edbob.pyramid import forms
|
||||
from edbob.pyramid import grids
|
||||
from edbob.pyramid import Session
|
||||
from edbob.util import requires_impl
|
||||
|
||||
|
||||
__all__ = ['GridView']
|
||||
|
||||
|
||||
class GridView(object):
|
||||
|
||||
@property
|
||||
@requires_impl(is_property=True)
|
||||
def mapped_class(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@requires_impl(is_property=True)
|
||||
def route_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@requires_impl(is_property=True)
|
||||
def route_prefix(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
def join_map(self):
|
||||
return {}
|
||||
|
||||
def make_filter_map(self, **kwargs):
|
||||
return filters.get_filter_map(self.mapped_class, **kwargs)
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map()
|
||||
|
||||
def make_search_config(self, fmap, **kwargs):
|
||||
return filters.get_search_config(self.route_name, self.request, fmap,
|
||||
**kwargs)
|
||||
|
||||
def search_config(self, fmap):
|
||||
return self.make_search_config(fmap)
|
||||
|
||||
def make_search_form(self, config, **labels):
|
||||
return filters.get_search_form(config, **labels)
|
||||
|
||||
def search_form(self, config):
|
||||
return self.make_search_form(config)
|
||||
|
||||
def make_sort_map(self, *args, **kwargs):
|
||||
return grids.get_sort_map(self.mapped_class, names=args or None, **kwargs)
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map()
|
||||
|
||||
def make_grid_config(self, search, fmap, **kwargs):
|
||||
return grids.get_grid_config(
|
||||
self.route_name, self.request,
|
||||
search, filter_map=fmap, **kwargs)
|
||||
|
||||
def grid_config(self, search, fmap):
|
||||
return self.make_grid_config(search, fmap)
|
||||
|
||||
def filter_query(self, q):
|
||||
return q
|
||||
|
||||
def make_query(self, config, jmap=None):
|
||||
if jmap is None:
|
||||
jmap = self.join_map()
|
||||
smap = self.sort_map()
|
||||
q = Session.query(self.mapped_class)
|
||||
q = self.filter_query(q)
|
||||
q = filters.filter_query(q, config, jmap)
|
||||
q = grids.sort_query(q, config, smap, jmap)
|
||||
return q
|
||||
|
||||
def query(self, config):
|
||||
return self.make_query(config)
|
||||
|
||||
def make_grid(self, data, config, gridurl=None, objurl=None, delurl=None):
|
||||
if not gridurl:
|
||||
gridurl = self.request.route_url(self.route_name)
|
||||
if not objurl:
|
||||
objurl = '%s.edit' % self.route_prefix
|
||||
if not delurl:
|
||||
delurl = '%s.delete' % self.route_prefix
|
||||
g = forms.AlchemyGrid(
|
||||
self.mapped_class, data, config,
|
||||
gridurl=gridurl, objurl=objurl, delurl=delurl)
|
||||
return g
|
||||
|
||||
def grid(self, data, config):
|
||||
g = self.make_grid(data, config)
|
||||
g.configure(readonly=True)
|
||||
return g
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
View callable method.
|
||||
"""
|
||||
|
||||
fmap = self.filter_map()
|
||||
config = self.search_config(fmap)
|
||||
search = self.search_form(config)
|
||||
config = self.grid_config(search, fmap)
|
||||
grid = grids.get_pager(self.query, config)
|
||||
|
||||
g = self.grid(grid, config)
|
||||
cls = self.mapped_class.__name__
|
||||
if g.clickable:
|
||||
cls = 'clickable %s' % cls
|
||||
else:
|
||||
cls = 'hoverable %s' % cls
|
||||
grid = g.render(class_=cls)
|
||||
return grids.render_grid(self.request, grid, search)
|
||||
|
||||
@classmethod
|
||||
def add_route(cls, config, route_name, url_prefix, template_prefix=None, permission=None):
|
||||
if not template_prefix:
|
||||
template_prefix = url_prefix
|
||||
if not permission:
|
||||
permission = route_name
|
||||
config.add_route(route_name, url_prefix)
|
||||
config.add_view(cls, route_name=route_name, renderer='%s/index.mako' % template_prefix,
|
||||
permission=permission, http_cache=0)
|
30
edbob/pyramid/views/grids/__init__.py
Normal file
30
edbob/pyramid/views/grids/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.views.grids`` -- Grid Views
|
||||
"""
|
||||
|
||||
from edbob.pyramid.views.grids.core import *
|
||||
from edbob.pyramid.views.grids.alchemy import *
|
166
edbob/pyramid/views/grids/alchemy.py
Normal file
166
edbob/pyramid/views/grids/alchemy.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.views.grids`` -- Grid Views
|
||||
"""
|
||||
|
||||
from webhelpers import paginate
|
||||
|
||||
from edbob.pyramid import grids
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.views.grids.core import GridView
|
||||
|
||||
|
||||
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
|
||||
'PagedAlchemyGridView', 'SearchableAlchemyGridView']
|
||||
|
||||
|
||||
class AlchemyGridView(GridView):
|
||||
|
||||
def make_query(self):
|
||||
q = Session.query(self.mapped_class)
|
||||
return q
|
||||
|
||||
def query(self):
|
||||
return self.make_query()
|
||||
|
||||
def make_grid(self, data, **kwargs):
|
||||
kwargs.setdefault('partial_only', self.partial_only)
|
||||
return grids.AlchemyGrid(
|
||||
self.request, self.mapped_class, data, **kwargs)
|
||||
|
||||
def grid(self, data):
|
||||
return self.make_grid(data)
|
||||
|
||||
def __call__(self):
|
||||
query = self.query()
|
||||
grid = self.grid(query)
|
||||
return grids.util.render_grid(grid)
|
||||
|
||||
|
||||
class SortableAlchemyGridView(AlchemyGridView):
|
||||
|
||||
sort = None
|
||||
|
||||
def join_map(self):
|
||||
return {}
|
||||
|
||||
def make_sort_map(self, *args, **kwargs):
|
||||
return grids.util.get_sort_map(
|
||||
self.mapped_class, names=args or None, **kwargs)
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map()
|
||||
|
||||
def make_sort_config(self, **kwargs):
|
||||
return grids.util.get_sort_config(
|
||||
self.route_name, self.request, **kwargs)
|
||||
|
||||
def sort_config(self):
|
||||
return self.make_sort_config(sort=self.sort)
|
||||
|
||||
def make_query(self):
|
||||
query = Session.query(self.mapped_class)
|
||||
query = grids.util.sort_query(
|
||||
query, self._sort_config, self.sort_map(), self.join_map())
|
||||
return query
|
||||
|
||||
def query(self):
|
||||
return self.make_query()
|
||||
|
||||
def make_grid(self, **kwargs):
|
||||
kwargs.setdefault('partial_only', self.partial_only)
|
||||
return grids.AlchemyGrid(
|
||||
self.request, self.mapped_class, self._data,
|
||||
sort_map=self.sort_map(), config=self._sort_config, **kwargs)
|
||||
|
||||
def grid(self):
|
||||
return self.make_grid()
|
||||
|
||||
def __call__(self):
|
||||
self._sort_config = self.sort_config()
|
||||
self._data = self.query()
|
||||
grid = self.grid()
|
||||
return grids.util.render_grid(grid)
|
||||
|
||||
|
||||
class PagedAlchemyGridView(SortableAlchemyGridView):
|
||||
|
||||
def make_pager(self):
|
||||
config = self._sort_config
|
||||
query = self.query()
|
||||
return paginate.Page(
|
||||
query, item_count=query.count(),
|
||||
items_per_page=int(config['per_page']),
|
||||
page=int(config['page']),
|
||||
url=paginate.PageURL_WebOb(self.request))
|
||||
|
||||
def __call__(self):
|
||||
self._sort_config = self.sort_config()
|
||||
self._data = self.make_pager()
|
||||
grid = self.grid()
|
||||
grid.pager = self._data
|
||||
return grids.util.render_grid(grid)
|
||||
|
||||
|
||||
class SearchableAlchemyGridView(PagedAlchemyGridView):
|
||||
|
||||
def make_filter_map(self, **kwargs):
|
||||
return grids.search.get_filter_map(self.mapped_class, **kwargs)
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map()
|
||||
|
||||
def make_filter_config(self, **kwargs):
|
||||
return grids.search.get_filter_config(
|
||||
self.route_name, self.request, self.filter_map(), **kwargs)
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config()
|
||||
|
||||
def make_search_form(self):
|
||||
return grids.search.get_search_form(
|
||||
self.request, self.filter_map(), self._filter_config)
|
||||
|
||||
def search_form(self):
|
||||
return self.make_search_form()
|
||||
|
||||
def make_query(self):
|
||||
join_map = self.join_map()
|
||||
query = Session.query(self.mapped_class)
|
||||
query = grids.search.filter_query(
|
||||
query, self._filter_config, self.filter_map(), join_map)
|
||||
query = grids.util.sort_query(
|
||||
query, self._sort_config, self.sort_map(), join_map)
|
||||
return query
|
||||
|
||||
def __call__(self):
|
||||
self._filter_config = self.filter_config()
|
||||
search = self.search_form()
|
||||
self._sort_config = self.sort_config()
|
||||
self._data = self.make_pager()
|
||||
grid = self.grid()
|
||||
grid.pager = self._data
|
||||
return grids.util.render_grid(grid, search)
|
63
edbob/pyramid/views/grids/core.py
Normal file
63
edbob/pyramid/views/grids/core.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``edbob.pyramid.views.grids.core`` -- Core Grid View
|
||||
"""
|
||||
|
||||
from edbob.pyramid import grids
|
||||
from edbob.pyramid.views.core import View
|
||||
|
||||
|
||||
__all__ = ['GridView']
|
||||
|
||||
|
||||
class GridView(View):
|
||||
|
||||
route_name = None
|
||||
route_url = None
|
||||
renderer = None
|
||||
permission = None
|
||||
partial_only = False
|
||||
|
||||
def make_grid(self, **kwargs):
|
||||
kwargs.setdefault('partial_only', self.partial_only)
|
||||
return grids.Grid(self.request, **kwargs)
|
||||
|
||||
def grid(self):
|
||||
return self.make_grid()
|
||||
|
||||
def __call__(self):
|
||||
grid = self.grid()
|
||||
return grids.util.render_grid(grid)
|
||||
|
||||
@classmethod
|
||||
def add_route(cls, config, route_name=None, route_url=None, renderer=None, permission=None):
|
||||
route_name = route_name or cls.route_name
|
||||
route_url = route_url or cls.route_url
|
||||
renderer = renderer or cls.renderer
|
||||
permission = permission or cls.permission
|
||||
config.add_route(route_name, route_url)
|
||||
config.add_view(cls, route_name=route_name, renderer=renderer,
|
||||
permission=permission, http_cache=0)
|
Loading…
Add table
Add a link
Reference in a new issue