extensive commit (see note)
The following changes are included: - ``edbob.pyramid.Session`` uses ``sessionmaker()`` instead of ``edbob.db.Session``. - ``edbob.pyramid.includeme()`` now configures ``pyramid_beaker`` directly. - ``edbob.pyramid.includeme()`` now configures auth/auth policies directly. - Pyramid progress indicator added. - ``edbob.pyramid.Session`` added to global template render context. - ``request.get_referrer()`` method added (removed ``edbob.pyramid.util`` module). - ``request.get_setting()`` and ``request.save_setting()`` methods added. - ``Grid.column_header()`` now supports ``title`` attribute. - ``Grid.editable`` support added. - Template / style tweaks. - ``text`` argument to ``disable_button()`` JS function is now optional. - Forbidden view flash message no longer duplicated when multiple redirects occur. - ``CrudView`` class improved to support various workflow needs (e.g. post-delete procesing). - Extra renderer keyword args support added to ``GridView`` class. - ``SearchableAlchemyGridView`` class improved to support various workflow needs (e.g. obtaining an unsorted query).
This commit is contained in:
parent
03fe6a5c8e
commit
36634e8306
20 changed files with 481 additions and 122 deletions
|
@ -26,15 +26,13 @@
|
||||||
``edbob.pyramid`` -- Pyramid Framework
|
``edbob.pyramid`` -- Pyramid Framework
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sqlalchemy.orm import scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
from zope.sqlalchemy import ZopeTransactionExtension
|
from zope.sqlalchemy import ZopeTransactionExtension
|
||||||
|
|
||||||
import edbob.db
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Session']
|
__all__ = ['Session']
|
||||||
|
|
||||||
Session = scoped_session(edbob.db.Session)
|
Session = scoped_session(sessionmaker())
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
@ -51,16 +49,23 @@ def includeme(config):
|
||||||
The other thing added is the ``edbob`` static view for CSS files etc.
|
The other thing added is the ``edbob`` static view for CSS files etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Session is extended here instead of at module scope to prevent import
|
# Configure Beaker session.
|
||||||
# side-effects.
|
config.include('pyramid_beaker')
|
||||||
|
|
||||||
|
# Bring in transaction manager.
|
||||||
|
config.include('pyramid_tm')
|
||||||
|
|
||||||
|
# Configure SQLAlchemy session.
|
||||||
Session.configure(extension=ZopeTransactionExtension())
|
Session.configure(extension=ZopeTransactionExtension())
|
||||||
|
|
||||||
# Forbidden view is configured here instead of within edbob.pyramid.views
|
# Configure user authentication / authorization.
|
||||||
# since it's so "important."
|
from pyramid.authentication import SessionAuthenticationPolicy
|
||||||
|
config.set_authentication_policy(SessionAuthenticationPolicy())
|
||||||
|
from edbob.pyramid.auth import EdbobAuthorizationPolicy
|
||||||
|
config.set_authorization_policy(EdbobAuthorizationPolicy())
|
||||||
|
|
||||||
|
# Add forbidden view.
|
||||||
config.add_forbidden_view('edbob.pyramid.views.forbidden')
|
config.add_forbidden_view('edbob.pyramid.views.forbidden')
|
||||||
|
|
||||||
# Same goes with the edbob static route; we need that JS.
|
# Add static views.
|
||||||
config.include('edbob.pyramid.static')
|
config.include('edbob.pyramid.static')
|
||||||
|
|
||||||
# Include transaction manager tween.
|
|
||||||
config.include('pyramid_tm')
|
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
``edbob.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes
|
``edbob.pyramid.grids.alchemy`` -- FormAlchemy Grid Classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from webhelpers.html import literal
|
|
||||||
from webhelpers.html import tags
|
from webhelpers.html import tags
|
||||||
|
from webhelpers.html import HTML
|
||||||
|
|
||||||
import formalchemy
|
import formalchemy
|
||||||
|
|
||||||
|
@ -70,16 +70,18 @@ class AlchemyGrid(Grid):
|
||||||
return {'uuid': row.uuid}
|
return {'uuid': row.uuid}
|
||||||
|
|
||||||
def column_header(self, field):
|
def column_header(self, field):
|
||||||
cls = ''
|
class_ = None
|
||||||
label = field.label()
|
label = field.label()
|
||||||
if field.key in self.sort_map:
|
if field.key in self.sort_map:
|
||||||
cls = 'sortable'
|
class_ = 'sortable'
|
||||||
if field.key == self.config['sort']:
|
if field.key == self.config['sort']:
|
||||||
cls += ' sorted ' + self.config['dir']
|
class_ += ' sorted ' + self.config['dir']
|
||||||
label = tags.link_to(label, '#')
|
label = tags.link_to(label, '#')
|
||||||
if cls:
|
return HTML.tag('th', class_=class_, field=field.key,
|
||||||
cls = ' class="%s"' % cls
|
title=self.column_titles.get(field.key), c=label)
|
||||||
return literal('<th%s field="%s">' % (cls, field.key)) + label + literal('</th>')
|
|
||||||
|
def edit_route_kwargs(self, row):
|
||||||
|
return {'uuid': row.uuid}
|
||||||
|
|
||||||
def delete_route_kwargs(self, row):
|
def delete_route_kwargs(self, row):
|
||||||
return {'uuid': row.uuid}
|
return {'uuid': row.uuid}
|
||||||
|
|
|
@ -31,7 +31,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from ordereddict import OrderedDict
|
from ordereddict import OrderedDict
|
||||||
|
|
||||||
from webhelpers.html import literal
|
from webhelpers.html import HTML
|
||||||
from webhelpers.html.builder import format_attrs
|
from webhelpers.html.builder import format_attrs
|
||||||
|
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
|
@ -48,6 +48,7 @@ class Grid(edbob.Object):
|
||||||
hoverable = True
|
hoverable = True
|
||||||
clickable = False
|
clickable = False
|
||||||
checkboxes = False
|
checkboxes = False
|
||||||
|
editable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
|
|
||||||
partial_only = False
|
partial_only = False
|
||||||
|
@ -55,11 +56,15 @@ class Grid(edbob.Object):
|
||||||
click_route_name = None
|
click_route_name = None
|
||||||
click_route_kwargs = None
|
click_route_kwargs = None
|
||||||
|
|
||||||
|
edit_route_name = None
|
||||||
|
edit_route_kwargs = None
|
||||||
|
|
||||||
delete_route_name = None
|
delete_route_name = None
|
||||||
delete_route_kwargs = None
|
delete_route_kwargs = None
|
||||||
|
|
||||||
def __init__(self, request, **kwargs):
|
def __init__(self, request, **kwargs):
|
||||||
kwargs.setdefault('fields', OrderedDict())
|
kwargs.setdefault('fields', OrderedDict())
|
||||||
|
kwargs.setdefault('column_titles', {})
|
||||||
kwargs.setdefault('extra_columns', [])
|
kwargs.setdefault('extra_columns', [])
|
||||||
super(Grid, self).__init__(**kwargs)
|
super(Grid, self).__init__(**kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
|
@ -69,7 +74,9 @@ class Grid(edbob.Object):
|
||||||
edbob.Object(name=name, label=label, callback=callback))
|
edbob.Object(name=name, label=label, callback=callback))
|
||||||
|
|
||||||
def column_header(self, field):
|
def column_header(self, field):
|
||||||
return literal('<th field="%s">%s</th>' % (field.name, field.label))
|
return HTML.tag('th', field=field.name,
|
||||||
|
title=self.column_titles.get(field.name),
|
||||||
|
c=field.label)
|
||||||
|
|
||||||
def div_attrs(self):
|
def div_attrs(self):
|
||||||
classes = ['grid']
|
classes = ['grid']
|
||||||
|
@ -92,6 +99,15 @@ class Grid(edbob.Object):
|
||||||
kwargs = self.delete_route_kwargs
|
kwargs = self.delete_route_kwargs
|
||||||
return self.request.route_url(self.delete_route_name, **kwargs)
|
return self.request.route_url(self.delete_route_name, **kwargs)
|
||||||
|
|
||||||
|
def get_edit_url(self, row):
|
||||||
|
kwargs = {}
|
||||||
|
if self.edit_route_kwargs:
|
||||||
|
if callable(self.edit_route_kwargs):
|
||||||
|
kwargs = self.edit_route_kwargs(row)
|
||||||
|
else:
|
||||||
|
kwargs = self.edit_route_kwargs
|
||||||
|
return self.request.route_url(self.edit_route_name, **kwargs)
|
||||||
|
|
||||||
def get_row_attrs(self, row, i):
|
def get_row_attrs(self, row, i):
|
||||||
attrs = self.row_attrs(row, i)
|
attrs = self.row_attrs(row, i)
|
||||||
if self.clickable:
|
if self.clickable:
|
||||||
|
|
96
edbob/pyramid/progress.py
Normal file
96
edbob/pyramid/progress.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
#!/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.progress`` -- Progress Indicator
|
||||||
|
"""
|
||||||
|
|
||||||
|
from beaker.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
def get_progress_session(session, key):
|
||||||
|
request = session.request
|
||||||
|
id = '%s.progress.%s' % (session.id, key)
|
||||||
|
session = Session(request, id)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
class SessionProgress(object):
|
||||||
|
"""
|
||||||
|
Provides a session-based progress bar mechanism.
|
||||||
|
|
||||||
|
This class is only responsible for keeping the progress *data* current. It
|
||||||
|
is the responsibility of some client-side AJAX (etc.) to consume the data
|
||||||
|
for display to the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session, key):
|
||||||
|
self.session = get_progress_session(session, key)
|
||||||
|
self.cancelled = False
|
||||||
|
|
||||||
|
def __call__(self, message, maximum):
|
||||||
|
self.session['complete'] = False
|
||||||
|
self.session['message'] = message
|
||||||
|
self.session['maximum'] = maximum
|
||||||
|
self.session['cancelled'] = False
|
||||||
|
self.session['value'] = 0
|
||||||
|
self.session.save()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update(self, value):
|
||||||
|
self.session.load()
|
||||||
|
if self.session.get('cancelled'):
|
||||||
|
self.cancelled = True
|
||||||
|
else:
|
||||||
|
self.session['value'] = value
|
||||||
|
self.session.save()
|
||||||
|
return not self.cancelled
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
if not self.cancelled:
|
||||||
|
self.session['complete'] = True
|
||||||
|
self.session.save()
|
||||||
|
|
||||||
|
def secondary_progress(self):
|
||||||
|
return SecondarySessionProgress(self)
|
||||||
|
|
||||||
|
|
||||||
|
class SecondarySessionProgress(object):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.parent = parent
|
||||||
|
self.session = parent.session
|
||||||
|
|
||||||
|
def __call__(self, message, maximum):
|
||||||
|
self.session['message'] = message
|
||||||
|
self.session['value'] = 0
|
||||||
|
self.session['maximum'] = maximum
|
||||||
|
self.session.save()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update(self, value):
|
||||||
|
return self.parent.update(value)
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
|
@ -66,7 +66,8 @@ div.field-wrapper div.field {
|
||||||
|
|
||||||
div.field-wrapper div.field input[type=text],
|
div.field-wrapper div.field input[type=text],
|
||||||
div.field-wrapper div.field input[type=password],
|
div.field-wrapper div.field input[type=password],
|
||||||
div.field-wrapper div.field select {
|
div.field-wrapper div.field select,
|
||||||
|
div.field-wrapper div.field textarea {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,15 @@ table.grid-header {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Form (Filters etc.)
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
table.grid-header td.form {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************************
|
/******************************
|
||||||
* Context Menu
|
* Context Menu
|
||||||
******************************/
|
******************************/
|
||||||
|
@ -104,15 +113,6 @@ div.grid table tr.odd {
|
||||||
/* width: 15px; */
|
/* width: 15px; */
|
||||||
/* } */
|
/* } */
|
||||||
|
|
||||||
div.grid table tbody td.delete {
|
|
||||||
background-image: url(../img/delete.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 18px;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.grid table tbody tr.hovering {
|
div.grid table tbody tr.hovering {
|
||||||
background-color: #bbbbbb;
|
background-color: #bbbbbb;
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,24 @@ div.grid table.checkable tbody tr {
|
||||||
|
|
||||||
div.grid.clickable table tbody tr td.noclick {
|
div.grid.clickable table tbody tr td.noclick {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
text-align: center;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.edit,
|
||||||
|
div.grid table tbody tr td.noclick.delete {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.edit {
|
||||||
|
background-image: url(../img/edit.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.grid table tbody tr td.noclick.delete {
|
||||||
|
background-image: url(../img/delete.png);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* div.grid table.selectable tbody tr.selected, */
|
/* div.grid table.selectable tbody tr.selected, */
|
||||||
|
|
BIN
edbob/pyramid/static/img/edit.png
Normal file
BIN
edbob/pyramid/static/img/edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 533 B |
|
@ -16,7 +16,9 @@ var filters_to_disable = [];
|
||||||
|
|
||||||
|
|
||||||
function disable_button(button, text) {
|
function disable_button(button, text) {
|
||||||
$(button).html(text + ", please wait...");
|
if (text) {
|
||||||
|
$(button).html(text + ", please wait...");
|
||||||
|
}
|
||||||
$(button).attr('disabled', 'disabled');
|
$(button).attr('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +289,13 @@ $(function() {
|
||||||
$(this).toggleClass('selected');
|
$(this).toggleClass('selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('div.grid table tbody td.edit').live('click', function() {
|
||||||
|
var url = $(this).attr('url');
|
||||||
|
if (url) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('div.grid table tbody td.delete').live('click', function() {
|
$('div.grid table tbody td.delete').live('click', function() {
|
||||||
var url = $(this).attr('url');
|
var url = $(this).attr('url');
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|
|
@ -37,11 +37,7 @@ from edbob.pyramid import Session
|
||||||
|
|
||||||
def before_render(event):
|
def before_render(event):
|
||||||
"""
|
"""
|
||||||
Adds goodies to the global template renderer context:
|
Adds goodies to the global template renderer context.
|
||||||
|
|
||||||
* ``h``
|
|
||||||
* ``url``
|
|
||||||
* ``edbob``
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request = event.get('request') or threadlocal.get_current_request()
|
request = event.get('request') or threadlocal.get_current_request()
|
||||||
|
@ -50,32 +46,63 @@ def before_render(event):
|
||||||
renderer_globals['h'] = helpers
|
renderer_globals['h'] = helpers
|
||||||
renderer_globals['url'] = request.route_url
|
renderer_globals['url'] = request.route_url
|
||||||
renderer_globals['edbob'] = edbob
|
renderer_globals['edbob'] = edbob
|
||||||
|
renderer_globals['Session'] = Session
|
||||||
|
|
||||||
|
|
||||||
def context_found(event):
|
def context_found(event):
|
||||||
"""
|
"""
|
||||||
This hook attaches the :class:`edbob.User` instance for the currently
|
This hook attaches various attributes and methods to the ``request``
|
||||||
logged-in user to the request (if there is one) as ``request.user``.
|
object. Specifically:
|
||||||
|
|
||||||
Also adds a ``has_perm()`` function to the request, which is a shortcut for
|
The :class:`edbob.User` instance currently logged-in (if indeed there is
|
||||||
|
one) is attached as ``request.user``.
|
||||||
|
|
||||||
|
A ``request.has_perm()`` method is attached, which is a shortcut for
|
||||||
:func:`edbob.db.auth.has_permission()`.
|
:func:`edbob.db.auth.has_permission()`.
|
||||||
|
|
||||||
|
A ``request.get_referrer()`` method is attached, which contains some
|
||||||
|
convenient logic for determining the referring URL.
|
||||||
|
|
||||||
|
The ``request.get_setting()`` and ``request.save_setting()`` methods are
|
||||||
|
attached, which are shortcuts for :func:`edbob.get_setting()` and
|
||||||
|
:func:`edbob.save_setting()`, respectively.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def has_perm_func(request):
|
|
||||||
def has_perm(perm):
|
|
||||||
if not request.user:
|
|
||||||
return False
|
|
||||||
return has_permission(request.user, perm)
|
|
||||||
return has_perm
|
|
||||||
|
|
||||||
request = event.request
|
request = event.request
|
||||||
request.has_perm = has_perm_func(request)
|
|
||||||
|
|
||||||
request.user = None
|
request.user = None
|
||||||
uuid = authenticated_userid(request)
|
uuid = authenticated_userid(request)
|
||||||
if uuid:
|
if uuid:
|
||||||
request.user = Session.query(edbob.User).get(uuid)
|
request.user = Session.query(edbob.User).get(uuid)
|
||||||
|
|
||||||
|
def has_perm(perm):
|
||||||
|
if not request.user:
|
||||||
|
return False
|
||||||
|
return has_permission(request.user, perm)
|
||||||
|
request.has_perm = has_perm
|
||||||
|
|
||||||
|
def get_referrer(default=None):
|
||||||
|
if request.params.get('referrer'):
|
||||||
|
return request.params['referrer']
|
||||||
|
if request.session.get('referrer'):
|
||||||
|
return request.session.pop('referrer')
|
||||||
|
referrer = request.referrer
|
||||||
|
if not referrer or referrer == request.current_route_url():
|
||||||
|
if default:
|
||||||
|
referrer = default
|
||||||
|
else:
|
||||||
|
referrer = request.route_url('home')
|
||||||
|
return referrer
|
||||||
|
request.get_referrer = get_referrer
|
||||||
|
|
||||||
|
def get_setting(name):
|
||||||
|
return edbob.get_setting(name, Session())
|
||||||
|
request.get_setting = get_setting
|
||||||
|
|
||||||
|
def save_setting(name, value):
|
||||||
|
edbob.save_setting(name, value, Session())
|
||||||
|
request.save_setting = save_setting
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.add_subscriber('edbob.pyramid.subscribers:before_render',
|
config.add_subscriber('edbob.pyramid.subscribers:before_render',
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
<%inherit file="/base.mako" />
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
<%def name="context_menu_items()"></%def>
|
<%def name="context_menu_items()"></%def>
|
||||||
|
|
||||||
|
<%def name="form()">
|
||||||
|
% if search:
|
||||||
|
${search.render()}
|
||||||
|
% else:
|
||||||
|
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="tools()"></%def>
|
<%def name="tools()"></%def>
|
||||||
|
|
||||||
<div class="grid-wrapper">
|
<div class="grid-wrapper">
|
||||||
|
|
||||||
<table class="grid-header">
|
<table class="grid-header">
|
||||||
<tr>
|
<tr>
|
||||||
% if search:
|
<td rowspan="2" class="form">
|
||||||
<td rowspan="2" class="filters">
|
${self.form()}
|
||||||
${search.render()}
|
</td>
|
||||||
</td>
|
|
||||||
% else:
|
|
||||||
<td rowspan="2"> </td>
|
|
||||||
% endif
|
|
||||||
<td class="context-menu">
|
<td class="context-menu">
|
||||||
<ul>
|
<ul>
|
||||||
${self.context_menu_items()}
|
${self.context_menu_items()}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<%def name="title()">Login</%def>
|
<%def name="title()">Login</%def>
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/login.css'))}
|
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/login.css'))}
|
||||||
${self.logo_styles()}
|
${self.logo_styles()}
|
||||||
</%def>
|
</%def>
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
<div class="form">
|
<div class="form">
|
||||||
${h.form('')}
|
${h.form('')}
|
||||||
## <input type="hidden" name="login" value="True" />
|
## <input type="hidden" name="login" value="True" />
|
||||||
<input type="hidden" name="referer" value="${referer}" />
|
<input type="hidden" name="referrer" value="${referrer}" />
|
||||||
|
|
||||||
% if error:
|
% if error:
|
||||||
<div class="error">${error}</div>
|
<div class="error">${error}</div>
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
% for col in grid.extra_columns:
|
% for col in grid.extra_columns:
|
||||||
<th>${col.label}</td>
|
<th>${col.label}</td>
|
||||||
% endfor
|
% endfor
|
||||||
|
% if grid.editable:
|
||||||
|
<th> </th>
|
||||||
|
% endif
|
||||||
% if grid.deletable:
|
% if grid.deletable:
|
||||||
<th> </th>
|
<th> </th>
|
||||||
% endif
|
% endif
|
||||||
|
@ -28,6 +31,9 @@
|
||||||
% for col in grid.extra_columns:
|
% for col in grid.extra_columns:
|
||||||
<td class="noclick ${col.name}">${col.callback(row)}</td>
|
<td class="noclick ${col.name}">${col.callback(row)}</td>
|
||||||
% endfor
|
% endfor
|
||||||
|
% if grid.editable:
|
||||||
|
<td class="noclick edit" url="${grid.get_edit_url(row)}"> </td>
|
||||||
|
% endif
|
||||||
% if grid.deletable:
|
% if grid.deletable:
|
||||||
<td class="noclick delete" url="${grid.get_delete_url(row)}"> </td>
|
<td class="noclick delete" url="${grid.get_delete_url(row)}"> </td>
|
||||||
% endif
|
% endif
|
||||||
|
|
138
edbob/pyramid/templates/progress.mako
Normal file
138
edbob/pyramid/templates/progress.mako
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Working...</title>
|
||||||
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
|
||||||
|
${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
|
||||||
|
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
#container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
height: 60px;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -45px;
|
||||||
|
margin-left: -350px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress-wrapper {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress {
|
||||||
|
border-collapse: collapse;
|
||||||
|
height: 25px;
|
||||||
|
width: 550px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#complete {
|
||||||
|
background-color: Gray;
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#remaining {
|
||||||
|
background-color: LightGray;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#percentage {
|
||||||
|
padding-left: 3px;
|
||||||
|
min-width: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancel {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
|
||||||
|
var updater = null;
|
||||||
|
|
||||||
|
function update_progress() {
|
||||||
|
$.ajax({
|
||||||
|
url: '${url('progress', key=key)}',
|
||||||
|
success: function(data) {
|
||||||
|
$('#message').html(data.message);
|
||||||
|
$('#total').html('('+data.maximum+' total)');
|
||||||
|
$('#cancel button').show();
|
||||||
|
if (data.complete) {
|
||||||
|
clearInterval(updater);
|
||||||
|
$('#cancel button').hide();
|
||||||
|
$('#total').html('done!');
|
||||||
|
$('#complete').css('width', '100%');
|
||||||
|
$('#remaining').hide();
|
||||||
|
$('#percentage').html('100%');
|
||||||
|
location.href = data.success_url;
|
||||||
|
} else {
|
||||||
|
var width = parseInt(data.value) / parseInt(data.maximum);
|
||||||
|
width = Math.round(100 * width);
|
||||||
|
if (width > 0) {
|
||||||
|
$('#complete').css('width', width+'%');
|
||||||
|
$('#remaining').css('width', 'auto');
|
||||||
|
}
|
||||||
|
$('#percentage').html(width+'%');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updater = setInterval(function() {update_progress()}, 1000);
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$('#cancel button').click(function() {
|
||||||
|
if (confirm("Do you really wish to cancel this operation?")) {
|
||||||
|
clearInterval(updater);
|
||||||
|
disable_button(this, "Cancelling");
|
||||||
|
$.ajax({
|
||||||
|
url: '${url('progress.cancel', key=key)}',
|
||||||
|
data: {
|
||||||
|
'cancel_msg': '${cancel_msg}',
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
location.href = '${cancel_url}';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
|
||||||
|
<div id="wrapper">
|
||||||
|
|
||||||
|
<p><span id="message">Working</span> ... <span id="total"></span></p>
|
||||||
|
|
||||||
|
<table id="progress-wrapper">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table id="progress">
|
||||||
|
<tr>
|
||||||
|
<td id="complete"></td>
|
||||||
|
<td id="remaining"></td>
|
||||||
|
</tr>
|
||||||
|
</table><!-- #progress -->
|
||||||
|
</td>
|
||||||
|
<td id="percentage"></td>
|
||||||
|
<td id="cancel">
|
||||||
|
<button type="button" style="display: none;">Cancel</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table><!-- #progress-wrapper -->
|
||||||
|
|
||||||
|
</div><!-- #wrapper -->
|
||||||
|
|
||||||
|
</div><!-- #container -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -49,7 +49,7 @@ def forbidden(request):
|
||||||
if not authenticated_userid(request):
|
if not authenticated_userid(request):
|
||||||
msg += literal(" (Perhaps you should %s?)" %
|
msg += literal(" (Perhaps you should %s?)" %
|
||||||
link_to("log in", request.route_url('login')))
|
link_to("log in", request.route_url('login')))
|
||||||
request.session.flash(msg)
|
request.session.flash(msg, allow_duplicate=False)
|
||||||
|
|
||||||
url = request.referer
|
url = request.referer
|
||||||
if not url or url == request.current_route_url():
|
if not url or url == request.current_route_url():
|
||||||
|
@ -60,4 +60,5 @@ def forbidden(request):
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.include('edbob.pyramid.views.auth')
|
config.include('edbob.pyramid.views.auth')
|
||||||
config.include('edbob.pyramid.views.people')
|
config.include('edbob.pyramid.views.people')
|
||||||
|
config.include('edbob.pyramid.views.progress')
|
||||||
config.include('edbob.pyramid.views.users')
|
config.include('edbob.pyramid.views.users')
|
||||||
|
|
|
@ -35,7 +35,6 @@ from pyramid_simpleform.renderers import FormRenderer
|
||||||
import edbob
|
import edbob
|
||||||
from edbob.db.auth import authenticate_user
|
from edbob.db.auth import authenticate_user
|
||||||
from edbob.pyramid import Session
|
from edbob.pyramid import Session
|
||||||
from edbob.pyramid.util import get_referer
|
|
||||||
|
|
||||||
|
|
||||||
class UserLogin(formencode.Schema):
|
class UserLogin(formencode.Schema):
|
||||||
|
@ -50,11 +49,11 @@ def login(request):
|
||||||
The login view, responsible for displaying and handling the login form.
|
The login view, responsible for displaying and handling the login form.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
referer = get_referer(request)
|
referrer = request.get_referrer()
|
||||||
|
|
||||||
# Redirect if already logged in.
|
# Redirect if already logged in.
|
||||||
if request.user:
|
if request.user:
|
||||||
return HTTPFound(location=referer)
|
return HTTPFound(location=referrer)
|
||||||
|
|
||||||
form = Form(request, schema=UserLogin)
|
form = Form(request, schema=UserLogin)
|
||||||
if form.validate():
|
if form.validate():
|
||||||
|
@ -66,7 +65,7 @@ def login(request):
|
||||||
user.display_name,
|
user.display_name,
|
||||||
edbob.local_time().strftime('%I:%M %p')))
|
edbob.local_time().strftime('%I:%M %p')))
|
||||||
headers = remember(request, user.uuid)
|
headers = remember(request, user.uuid)
|
||||||
return HTTPFound(location=referer, headers=headers)
|
return HTTPFound(location=referrer, headers=headers)
|
||||||
request.session.flash("Invalid username or password")
|
request.session.flash("Invalid username or password")
|
||||||
|
|
||||||
url = edbob.config.get('edbob.pyramid', 'login.logo_url',
|
url = edbob.config.get('edbob.pyramid', 'login.logo_url',
|
||||||
|
@ -74,7 +73,7 @@ def login(request):
|
||||||
kwargs = eval(edbob.config.get('edbob.pyramid', 'login.logo_kwargs',
|
kwargs = eval(edbob.config.get('edbob.pyramid', 'login.logo_kwargs',
|
||||||
default="dict(width=500)"))
|
default="dict(width=500)"))
|
||||||
|
|
||||||
return {'form': FormRenderer(form), 'referer': referer,
|
return {'form': FormRenderer(form), 'referrer': referrer,
|
||||||
'logo_url': url, 'logo_kwargs': kwargs}
|
'logo_url': url, 'logo_kwargs': kwargs}
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,8 +88,8 @@ def logout(request):
|
||||||
request.session.delete()
|
request.session.delete()
|
||||||
request.session.invalidate()
|
request.session.invalidate()
|
||||||
headers = forget(request)
|
headers = forget(request)
|
||||||
referer = get_referer(request)
|
referrer = request.get_referrer()
|
||||||
return HTTPFound(location=referer, headers=headers)
|
return HTTPFound(location=referrer, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -33,7 +33,7 @@ import formalchemy
|
||||||
from edbob.pyramid import Session
|
from edbob.pyramid import Session
|
||||||
from edbob.pyramid.forms.formalchemy import AlchemyForm
|
from edbob.pyramid.forms.formalchemy import AlchemyForm
|
||||||
from edbob.pyramid.views.core import View
|
from edbob.pyramid.views.core import View
|
||||||
from edbob.util import requires_impl
|
from edbob.util import requires_impl, prettify
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['CrudView']
|
__all__ = ['CrudView']
|
||||||
|
@ -41,7 +41,9 @@ __all__ = ['CrudView']
|
||||||
|
|
||||||
class CrudView(View):
|
class CrudView(View):
|
||||||
|
|
||||||
|
readonly = False
|
||||||
allow_successive_creates = False
|
allow_successive_creates = False
|
||||||
|
update_cancel_route = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_impl(is_property=True)
|
@requires_impl(is_property=True)
|
||||||
|
@ -72,6 +74,7 @@ class CrudView(View):
|
||||||
def make_fieldset(self, model, **kwargs):
|
def make_fieldset(self, model, **kwargs):
|
||||||
kwargs.setdefault('session', Session())
|
kwargs.setdefault('session', Session())
|
||||||
fieldset = formalchemy.FieldSet(model, **kwargs)
|
fieldset = formalchemy.FieldSet(model, **kwargs)
|
||||||
|
fieldset.prettify = prettify
|
||||||
return fieldset
|
return fieldset
|
||||||
|
|
||||||
def fieldset(self, model):
|
def fieldset(self, model):
|
||||||
|
@ -84,7 +87,11 @@ class CrudView(View):
|
||||||
fieldset = self.fieldset(model)
|
fieldset = self.fieldset(model)
|
||||||
kwargs.setdefault('pretty_name', self.pretty_name)
|
kwargs.setdefault('pretty_name', self.pretty_name)
|
||||||
kwargs.setdefault('action_url', self.request.current_route_url())
|
kwargs.setdefault('action_url', self.request.current_route_url())
|
||||||
kwargs.setdefault('cancel_url', self.cancel_url)
|
if self.updating and self.update_cancel_route:
|
||||||
|
kwargs.setdefault('cancel_url', self.request.route_url(
|
||||||
|
self.update_cancel_route, uuid=model.uuid))
|
||||||
|
else:
|
||||||
|
kwargs.setdefault('cancel_url', self.cancel_url)
|
||||||
kwargs.setdefault('creating', self.creating)
|
kwargs.setdefault('creating', self.creating)
|
||||||
kwargs.setdefault('updating', self.updating)
|
kwargs.setdefault('updating', self.updating)
|
||||||
form = AlchemyForm(self.request, fieldset, **kwargs)
|
form = AlchemyForm(self.request, fieldset, **kwargs)
|
||||||
|
@ -104,6 +111,9 @@ class CrudView(View):
|
||||||
|
|
||||||
def crud(self, model, readonly=False):
|
def crud(self, model, readonly=False):
|
||||||
|
|
||||||
|
if readonly:
|
||||||
|
self.readonly = True
|
||||||
|
|
||||||
form = self.form(model)
|
form = self.form(model)
|
||||||
if readonly:
|
if readonly:
|
||||||
form.readonly = True
|
form.readonly = True
|
||||||
|
@ -125,7 +135,7 @@ class CrudView(View):
|
||||||
and self.request.params.get('create_and_continue')):
|
and self.request.params.get('create_and_continue')):
|
||||||
return HTTPFound(location=self.request.current_route_url())
|
return HTTPFound(location=self.request.current_route_url())
|
||||||
|
|
||||||
return HTTPFound(location=self.home_url)
|
return HTTPFound(location=self.post_save_url(form))
|
||||||
|
|
||||||
self.validation_failed(form)
|
self.validation_failed(form)
|
||||||
|
|
||||||
|
@ -139,6 +149,9 @@ class CrudView(View):
|
||||||
def post_save(self, form):
|
def post_save(self, form):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def post_save_url(self, form):
|
||||||
|
return self.home_url
|
||||||
|
|
||||||
def validation_failed(self, form):
|
def validation_failed(self, form):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -160,7 +173,8 @@ class CrudView(View):
|
||||||
def read(self):
|
def read(self):
|
||||||
uuid = self.request.matchdict['uuid']
|
uuid = self.request.matchdict['uuid']
|
||||||
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
||||||
assert model
|
if not model:
|
||||||
|
return HTTPFound(location=self.home_url)
|
||||||
return self.crud(model, readonly=True)
|
return self.crud(model, readonly=True)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
@ -172,6 +186,9 @@ class CrudView(View):
|
||||||
def pre_delete(self, model):
|
def pre_delete(self, model):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def post_delete(self, model):
|
||||||
|
pass
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
uuid = self.request.matchdict['uuid']
|
uuid = self.request.matchdict['uuid']
|
||||||
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
model = Session.query(self.mapped_class).get(uuid) if uuid else None
|
||||||
|
@ -181,5 +198,6 @@ class CrudView(View):
|
||||||
return result
|
return result
|
||||||
Session.delete(model)
|
Session.delete(model)
|
||||||
Session.flush() # Don't set flash message if delete fails.
|
Session.flush() # Don't set flash message if delete fails.
|
||||||
|
self.post_delete(model)
|
||||||
self.flash_delete(model)
|
self.flash_delete(model)
|
||||||
return HTTPFound(location=self.home_url)
|
return HTTPFound(location=self.home_url)
|
||||||
|
|
|
@ -164,14 +164,15 @@ class SearchableAlchemyGridView(PagedAlchemyGridView):
|
||||||
def search_form(self):
|
def search_form(self):
|
||||||
return self.make_search_form()
|
return self.make_search_form()
|
||||||
|
|
||||||
def make_query(self):
|
def make_query(self, session=Session):
|
||||||
join_map = self.join_map()
|
join_map = self.join_map()
|
||||||
query = Session.query(self.mapped_class)
|
query = session.query(self.mapped_class)
|
||||||
query = grids.search.filter_query(
|
query = grids.search.filter_query(
|
||||||
query, self._filter_config, self.filter_map(), join_map)
|
query, self._filter_config, self.filter_map(), join_map)
|
||||||
self._sort_config['joins'] = self._filter_config['joins']
|
if hasattr(self, '_sort_config'):
|
||||||
query = grids.util.sort_query(
|
self._sort_config['joins'] = self._filter_config['joins']
|
||||||
query, self._sort_config, self.sort_map(), join_map)
|
query = grids.util.sort_query(
|
||||||
|
query, self._sort_config, self.sort_map(), join_map)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
|
@ -181,4 +182,5 @@ class SearchableAlchemyGridView(PagedAlchemyGridView):
|
||||||
self._data = self.make_pager()
|
self._data = self.make_pager()
|
||||||
grid = self.grid()
|
grid = self.grid()
|
||||||
grid.pager = self._data
|
grid.pager = self._data
|
||||||
return grids.util.render_grid(grid, search)
|
kwargs = self.render_kwargs()
|
||||||
|
return grids.util.render_grid(grid, search, **kwargs)
|
||||||
|
|
|
@ -61,6 +61,10 @@ class GridView(View):
|
||||||
def grid(self):
|
def grid(self):
|
||||||
return self.make_grid()
|
return self.make_grid()
|
||||||
|
|
||||||
|
def render_kwargs(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
grid = self.grid()
|
grid = self.grid()
|
||||||
return grids.util.render_grid(grid)
|
kwargs = self.render_kwargs()
|
||||||
|
return grids.util.render_grid(grid, **kwargs)
|
||||||
|
|
|
@ -23,23 +23,34 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
"""
|
"""
|
||||||
``edbob.pyramid.util`` -- Utilities
|
``edbob.pyramid.views.progress`` -- Progress Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from edbob.pyramid.progress import get_progress_session
|
||||||
|
|
||||||
def get_referer(request, default=None):
|
|
||||||
"""
|
|
||||||
Returns a "referer" URL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if request.params.get('referer'):
|
def progress(request):
|
||||||
return request.params['referer']
|
key = request.matchdict['key']
|
||||||
if request.session.get('referer'):
|
session = get_progress_session(request.session, key)
|
||||||
return request.session.pop('referer')
|
if session.get('complete') and session.get('success_msg'):
|
||||||
referer = request.referer
|
request.session.flash(session['success_msg'])
|
||||||
if not referer or referer == request.current_route_url():
|
return session
|
||||||
if default:
|
|
||||||
referer = default
|
|
||||||
else:
|
def cancel(request):
|
||||||
referer = request.route_url('home')
|
key = request.matchdict['key']
|
||||||
return referer
|
session = get_progress_session(request.session, key)
|
||||||
|
session.clear()
|
||||||
|
session['cancelled'] = True
|
||||||
|
session.save()
|
||||||
|
msg = request.params.get('cancel_msg', "The operation was cancelled.")
|
||||||
|
request.session.flash(msg)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
config.add_route('progress', '/progress/{key}')
|
||||||
|
config.add_view(progress, route_name='progress', renderer='json')
|
||||||
|
|
||||||
|
config.add_route('progress.cancel', '/progress/{key}/cancel')
|
||||||
|
config.add_view(cancel, route_name='progress.cancel', renderer='json')
|
Loading…
Add table
Add a link
Reference in a new issue