From 6911b4380f8fb6ad607990c437d41c9af16b0839 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 8 Oct 2012 10:52:17 -0700 Subject: [PATCH 01/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index 481bbf2..ceec557 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a18' +__version__ = '0.1a19' From 26e11bbae0f11d96e1a3d091a0925622ae9a9851 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Oct 2012 16:02:43 -0700 Subject: [PATCH 02/59] add active_extensions stub to alembic scaffold --- .../edbob/+package+/alembic/script.py.mako | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/edbob/scaffolds/edbob/+package+/alembic/script.py.mako b/edbob/scaffolds/edbob/+package+/alembic/script.py.mako index 46eb3a1..454755d 100644 --- a/edbob/scaffolds/edbob/+package+/alembic/script.py.mako +++ b/edbob/scaffolds/edbob/+package+/alembic/script.py.mako @@ -24,8 +24,20 @@ active_extensions = sa.Table( def upgrade(): - ${upgrades if upgrades else "pass"} + ${upgrades if upgrades else 'pass'} + + # active extensions + + # op.execute( + # active_extensions.insert().values( + # name='dummy')) def downgrade(): - ${downgrades if downgrades else "pass"} + ${downgrades if downgrades else 'pass'} + + # active extensions + + # op.execute( + # active_extensions.delete().where( + # active_extensions.c.name == 'dummy')) From 4eb648f47ae456f513334954ce21483376c160e5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Oct 2012 16:04:24 -0700 Subject: [PATCH 03/59] update user views --- edbob/pyramid/templates/users/base.mako | 2 -- edbob/pyramid/templates/users/crud.mako | 10 +++--- edbob/pyramid/templates/users/edit.mako | 2 -- edbob/pyramid/templates/users/index.mako | 7 ++-- edbob/pyramid/templates/users/new.mako | 2 -- edbob/pyramid/views/users.py | 45 +++++++++++++++++++----- 6 files changed, 47 insertions(+), 21 deletions(-) delete mode 100644 edbob/pyramid/templates/users/base.mako delete mode 100644 edbob/pyramid/templates/users/edit.mako delete mode 100644 edbob/pyramid/templates/users/new.mako diff --git a/edbob/pyramid/templates/users/base.mako b/edbob/pyramid/templates/users/base.mako deleted file mode 100644 index 27f7dd9..0000000 --- a/edbob/pyramid/templates/users/base.mako +++ /dev/null @@ -1,2 +0,0 @@ -<%inherit file="/base.mako" /> -${parent.body()} diff --git a/edbob/pyramid/templates/users/crud.mako b/edbob/pyramid/templates/users/crud.mako index c9314e9..2e5a70e 100644 --- a/edbob/pyramid/templates/users/crud.mako +++ b/edbob/pyramid/templates/users/crud.mako @@ -1,10 +1,12 @@ -<%inherit file="/users/base.mako" /> <%inherit file="/crud.mako" /> -<%def name="crud_name()">User - <%def name="context_menu_items()"> -
  • ${h.link_to("Back to Users", url('users.list'))}
  • +
  • ${h.link_to("Back to Users", url('users'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}
  • + % endif ${parent.body()} diff --git a/edbob/pyramid/templates/users/edit.mako b/edbob/pyramid/templates/users/edit.mako deleted file mode 100644 index 77fd24c..0000000 --- a/edbob/pyramid/templates/users/edit.mako +++ /dev/null @@ -1,2 +0,0 @@ -<%inherit file="/users/crud.mako" /> -${parent.body()} diff --git a/edbob/pyramid/templates/users/index.mako b/edbob/pyramid/templates/users/index.mako index 493a309..c9f9918 100644 --- a/edbob/pyramid/templates/users/index.mako +++ b/edbob/pyramid/templates/users/index.mako @@ -1,10 +1,11 @@ -<%inherit file="/users/base.mako" /> -<%inherit file="/index.mako" /> +<%inherit file="/grid.mako" /> <%def name="title()">Users <%def name="context_menu_items()"> -
  • ${h.link_to("Create a new User", url('user.new'))}
  • + % if request.has_perm('users.create'): +
  • ${h.link_to("Create a new User", url('user.create'))}
  • + % endif ${parent.body()} diff --git a/edbob/pyramid/templates/users/new.mako b/edbob/pyramid/templates/users/new.mako deleted file mode 100644 index 77fd24c..0000000 --- a/edbob/pyramid/templates/users/new.mako +++ /dev/null @@ -1,2 +0,0 @@ -<%inherit file="/users/crud.mako" /> -${parent.body()} diff --git a/edbob/pyramid/views/users.py b/edbob/pyramid/views/users.py index be0a611..1ef9500 100644 --- a/edbob/pyramid/views/users.py +++ b/edbob/pyramid/views/users.py @@ -34,15 +34,13 @@ from formalchemy.fields import SelectFieldRenderer import edbob from edbob.db.auth import set_user_password from edbob.pyramid import Session -from edbob.pyramid.views import SearchableAlchemyGridView -from edbob.pyramid.views.crud import Crud +from edbob.pyramid.views import SearchableAlchemyGridView, CrudView class UsersGrid(SearchableAlchemyGridView): mapped_class = edbob.User - route_name = 'users' - route_url = '/users' + config_prefix = 'users' sort = 'username' def join_map(self): @@ -76,6 +74,15 @@ class UsersGrid(SearchableAlchemyGridView): g.person, ], readonly=True) + if self.request.has_perm('users.read'): + g.clickable = True + g.click_route_name = 'user.read' + if self.request.has_perm('users.update'): + g.editable = True + g.edit_route_name = 'user.update' + if self.request.has_perm('users.delete'): + g.deletable = True + g.delete_route_name = 'user.delete' return g @@ -176,10 +183,10 @@ class PasswordField(formalchemy.Field): set_user_password(self.model, password) -class UserCrud(Crud): +class UserCrud(CrudView): mapped_class = edbob.User - home_route = 'users.list' + home_route = 'users' def fieldset(self, user): fs = self.make_fieldset(user) @@ -213,5 +220,27 @@ class UserCrud(Crud): def includeme(config): - UsersGrid.add_route(config) - UserCrud.add_routes(config) + + config.add_route('users', '/users') + config.add_view(UsersGrid, route_name='users', + renderer='/users/index.mako', + permission='users.list') + + config.add_route('user.create', '/users/new') + config.add_view(UserCrud, attr='create', route_name='user.create', + renderer='/users/crud.mako', + permission='users.create') + + config.add_route('user.read', '/users/{uuid}') + config.add_view(UserCrud, attr='read', route_name='user.read', + renderer='/users/crud.mako', + permission='users.read') + + config.add_route('user.update', '/users/{uuid}/edit') + config.add_view(UserCrud, attr='update', route_name='user.update', + renderer='/users/crud.mako', + permission='users.update') + + config.add_route('user.delete', '/users/{uuid}/delete') + config.add_view(UserCrud, attr='delete', route_name='user.delete', + permission='users.delete') From 6d81cb38f9910b7646045b7de7099372b8f87cd9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Oct 2012 16:32:27 -0700 Subject: [PATCH 04/59] update changelog --- CHANGES.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 167e8ef..e5e784d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,13 @@ +0.1a19 +------ + +- [general] Added ``active_extensions`` stubs to alembic migration script + template. + +- [general] Updated pyramid user views and templates. These were sorely out of + date. + 0.1a18 ------ From 01972f4c18a6a24fc1ba3df3c7ae1f9be8c3ea3b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Oct 2012 16:34:44 -0700 Subject: [PATCH 05/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index ceec557..7a9c8ed 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a19' +__version__ = '0.1a20' From 12b0b3bb6027b9c583146b035579491053270a31 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 26 Oct 2012 08:39:20 -0700 Subject: [PATCH 06/59] add filemon pid path config for linux --- edbob/filemon/linux.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/edbob/filemon/linux.py b/edbob/filemon/linux.py index 23bbad1..d5902cc 100644 --- a/edbob/filemon/linux.py +++ b/edbob/filemon/linux.py @@ -87,7 +87,10 @@ def get_pid_path(): """ basename = os.path.basename(sys.argv[0]) - return '/tmp/%s_filemon.pid' % basename + pid_path = edbob.config.get('%s.filemon' % basename, 'pid_path') + if not pid_path: + pid_path = '/tmp/%s_filemon.pid' % basename + return pid_path def start_daemon(appname, daemonize=True): From 5ff3ba41200d94e5473c3c437fd94fa8581cefe8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 26 Oct 2012 10:25:26 -0700 Subject: [PATCH 07/59] update changelog --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index e5e784d..f7e6f1d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,11 @@ +0.1a20 +------ + +- [feature] Added ability to configure path to the PID file used by file + monitor daemons running on Linux. This was necessary to allow multiple + daemons to run on the same machine. + 0.1a19 ------ From 4e7302a2eb662bc3c570d75ee2fcd46676b68fe9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 26 Oct 2012 10:27:00 -0700 Subject: [PATCH 08/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index 7a9c8ed..d02b9c5 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a20' +__version__ = '0.1a21' From 99bde9e6963194a87534caaf46cc97f4c5e273b2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 08:00:40 -0800 Subject: [PATCH 09/59] add template layer to error emails --- MANIFEST.in | 2 ++ edbob/errors.py | 56 ++++++++++++++++++++++++----- edbob/files.py | 14 ++++++++ edbob/templates/errors/redmine.mako | 9 +++++ 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 edbob/templates/errors/redmine.mako diff --git a/MANIFEST.in b/MANIFEST.in index 4943929..acc928b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -19,3 +19,5 @@ recursive-include edbob/pyramid/templates *.mako recursive-include edbob/scaffolds/edbob *.py recursive-include edbob/scaffolds/edbob *_tmpl recursive-include edbob/scaffolds/edbob/+package+/pyramid/templates *.mako + +recursive-include edbob/templates *.mako diff --git a/edbob/errors.py b/edbob/errors.py index 4151350..5b74b12 100644 --- a/edbob/errors.py +++ b/edbob/errors.py @@ -26,6 +26,7 @@ ``edbob.errors`` -- Error Alert Emails """ +import os.path import sys import socket import logging @@ -33,6 +34,7 @@ from traceback import format_exception from cStringIO import StringIO import edbob +from edbob.files import resource_path from edbob.mail import sendmail_with_config @@ -60,15 +62,51 @@ def email_exception(type=None, value=None, traceback=None): if not (type and value and traceback): type, value, traceback = sys.exc_info() - body = StringIO() - hostname = socket.gethostname() - body.write("An exception occurred.\n") - body.write("\n") - body.write("Machine Name: %s (%s)\n" % (hostname, socket.gethostbyname(hostname))) - body.write("Local Time: %s\n" % (edbob.local_time().strftime('%Y-%m-%d %H:%M:%S %Z%z'))) - body.write("\n") - body.write("%s\n" % ''.join(format_exception(type, value, traceback))) + traceback = ''.join(format_exception(type, value, traceback)) + traceback = traceback.strip() + data = { + 'host_name': hostname, + 'host_ip': socket.gethostbyname(hostname), + 'host_time': edbob.local_time(), + 'traceback': traceback, + } - sendmail_with_config('errors', body.getvalue()) + body, ctype = render_exception(data) + sendmail_with_config('errors', body, content_type=ctype) + + +def render_exception(data): + """ + Renders the exception data using a Mako template if one is configured; + otherwise as a simple string. + """ + + template = edbob.config.get('edbob.errors', 'template') + if template: + template = resource_path(template) + if os.path.exists(template): + + # Assume Mako template; render and return. + from mako.template import Template + template = Template(filename=template) + return template.render(**data), 'text/plain' + + # If not a Mako template, return regular text with substitutions. + body = StringIO() + data['host_time'] = data['host_time'].strftime('%Y-%m-%d %H:%M:%S %Z%z') + + body.write("""\ +An unhandled exception occurred. + +Machine Name: %(host_name)s (%(host_ip)s) + +Machine Time: %(host_time)s + +%(traceback)s +""" % data) + + b = body.getvalue() body.close() + + return b, 'text/plain' diff --git a/edbob/files.py b/edbob/files.py index e50a8af..3cab3b4 100644 --- a/edbob/files.py +++ b/edbob/files.py @@ -33,6 +33,8 @@ import shutil import tempfile import lockfile +import pkg_resources + __all__ = ['temp_path'] @@ -98,6 +100,18 @@ def count_lines(path): return lines +def resource_path(path): + """ + Returns a resource file path. ``path`` is assumed either to be a package + resource, or a regular file path. In the latter case it is returned + unchanged. + """ + + if not os.path.isabs(path) and ':' in path: + return pkg_resources.resource_filename(*path.split(':')) + return path + + def temp_path(suffix='.tmp', prefix='edbob.'): """ Convenience function to return a temporary file path. The arguments' diff --git a/edbob/templates/errors/redmine.mako b/edbob/templates/errors/redmine.mako new file mode 100644 index 0000000..0188bd5 --- /dev/null +++ b/edbob/templates/errors/redmine.mako @@ -0,0 +1,9 @@ +An unhandled exception occurred. + +*Machine Name:* ${host_name} (${host_ip}) + +*Machine Time:* ${host_time.strftime('%Y-%m-%d %H:%M:%S %Z%z')} + +
    +${traceback}
    +
    From 1babe591adb41aad7ff3f1753297e6221a0c0d3c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 08:05:46 -0800 Subject: [PATCH 10/59] make error email content_type configurable --- edbob/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edbob/errors.py b/edbob/errors.py index 5b74b12..8f15c4e 100644 --- a/edbob/errors.py +++ b/edbob/errors.py @@ -73,6 +73,7 @@ def email_exception(type=None, value=None, traceback=None): } body, ctype = render_exception(data) + ctype = edbob.config.get('edbob.errors', 'content_type', default=ctype) sendmail_with_config('errors', body, content_type=ctype) @@ -90,7 +91,7 @@ def render_exception(data): # Assume Mako template; render and return. from mako.template import Template template = Template(filename=template) - return template.render(**data), 'text/plain' + return template.render(**data), 'text/html' # If not a Mako template, return regular text with substitutions. body = StringIO() From 873d51b77352e40efefa0a41c3c4bbaf2bb970e1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 08:08:16 -0800 Subject: [PATCH 11/59] update changelog --- CHANGES.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index f7e6f1d..b8ebc9c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,14 @@ +0.1a21 +------ + +- [feature] Added the ability to specify a Mako template for use when + generating error emails. Templates may be referenced in the config file + using either a resource path or an absolute file path. If no template is + specified, the plain text fallback will be used. The ``content_type`` of the + email is also configurable (defaults to ``'text/html'`` if a Mako template is + used; ``'text/plain'`` otherwise). + 0.1a20 ------ From 97f7eedad6d74289769c3f2d35a69d9c8f415cbf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 08:12:00 -0800 Subject: [PATCH 12/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index d02b9c5..48aa257 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a21' +__version__ = '0.1a22' From 28281ea8a1738ba3e07e765a8d30e26c8bb4dd5d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 15:32:49 -0800 Subject: [PATCH 13/59] add change password view --- edbob/pyramid/static/css/edbob.css | 4 + edbob/pyramid/static/css/forms.css | 9 +- edbob/pyramid/templates/{edbob => }/base.mako | 2 +- edbob/pyramid/templates/change_password.mako | 15 ++++ edbob/pyramid/views/auth.py | 83 ++++++++++++++++++- 5 files changed, 108 insertions(+), 5 deletions(-) rename edbob/pyramid/templates/{edbob => }/base.mako (96%) create mode 100644 edbob/pyramid/templates/change_password.mako diff --git a/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/edbob.css index 5417a07..e09bcb0 100644 --- a/edbob/pyramid/static/css/edbob.css +++ b/edbob/pyramid/static/css/edbob.css @@ -142,6 +142,10 @@ body > #container { margin: 8px 20px auto auto; } +#login a.username { + font-weight: bold; +} + #user-menu { float: left; } diff --git a/edbob/pyramid/static/css/forms.css b/edbob/pyramid/static/css/forms.css index 3047756..b698cfd 100644 --- a/edbob/pyramid/static/css/forms.css +++ b/edbob/pyramid/static/css/forms.css @@ -38,11 +38,18 @@ div.fieldset { div.field-wrapper { clear: both; - overflow: auto; min-height: 30px; + overflow: auto; + padding: 5px; +} + +div.field-wrapper.error { + background-color: #ddcccc; + border: 2px solid #dd6666; } div.field-wrapper label { + color: #000000; display: block; float: left; width: 140px; diff --git a/edbob/pyramid/templates/edbob/base.mako b/edbob/pyramid/templates/base.mako similarity index 96% rename from edbob/pyramid/templates/edbob/base.mako rename to edbob/pyramid/templates/base.mako index f3af0bf..b026322 100644 --- a/edbob/pyramid/templates/edbob/base.mako +++ b/edbob/pyramid/templates/base.mako @@ -37,7 +37,7 @@

    ${self.title()}

    % if request.user: - logged in as ${request.user.display_name} + ${h.link_to(request.user.display_name, url('change_password'), class_='username')} (${h.link_to("logout", url('logout'))}) % else: ${h.link_to("login", url('login'))} diff --git a/edbob/pyramid/templates/change_password.mako b/edbob/pyramid/templates/change_password.mako new file mode 100644 index 0000000..e00d2a5 --- /dev/null +++ b/edbob/pyramid/templates/change_password.mako @@ -0,0 +1,15 @@ +<%inherit file="/base.mako" /> + +<%def name="title()">Change Password + +
    + ${h.form(url('change_password'))} + ${form.referrer_field()} + ${form.field_div('current_password', form.password('current_password'))} + ${form.field_div('new_password', form.password('new_password'))} + ${form.field_div('confirm_password', form.password('confirm_password'))} +
    + ${h.submit('submit', "Change Password")} +
    + ${h.end_form()} +
    diff --git a/edbob/pyramid/views/auth.py b/edbob/pyramid/views/auth.py index 09344aa..3f55619 100644 --- a/edbob/pyramid/views/auth.py +++ b/edbob/pyramid/views/auth.py @@ -26,17 +26,50 @@ ``edbob.pyramid.views.auth`` -- Auth Views """ -import formencode from pyramid.httpexceptions import HTTPFound from pyramid.security import remember, forget + +import formencode from pyramid_simpleform import Form -from pyramid_simpleform.renderers import FormRenderer +import pyramid_simpleform.renderers + +from webhelpers.html import tags +from webhelpers.html.builder import HTML import edbob -from edbob.db.auth import authenticate_user +from edbob.db.auth import authenticate_user, set_user_password from edbob.pyramid import Session +from edbob.util import prettify +class FormRenderer(pyramid_simpleform.renderers.FormRenderer): + """ + Customized form renderer. Provides some extra methods for convenience. + """ + + # Note that as of this writing, this renderer is used only by the + # ``change_password`` view. This should probably change, and this class + # definition should be moved elsewhere. + + def field_div(self, name, field, label=None): + errors = self.errors_for(name) + if errors: + errors = [HTML.tag('div', class_='field-error', c=x) for x in errors] + errors = tags.literal('').join(errors) + + label = HTML.tag('label', for_=name, c=label or prettify(name)) + inner = HTML.tag('div', class_='field', c=field) + + outer_class = 'field-wrapper' + if errors: + outer_class += ' error' + outer = HTML.tag('div', class_=outer_class, c=(errors or '') + label + inner) + return outer + + def referrer_field(self): + return self.hidden('referrer', value=self.form.request.get_referrer()) + + class UserLogin(formencode.Schema): allow_extra_fields = True filter_extra_fields = True @@ -92,6 +125,47 @@ def logout(request): return HTTPFound(location=referrer, headers=headers) +class CurrentPasswordCorrect(formencode.validators.FancyValidator): + + def _to_python(self, value, state): + user = state + if not authenticate_user(user.username, value, session=Session()): + raise formencode.Invalid("The password is incorrect.", value, state) + return value + + +class ChangePassword(formencode.Schema): + + allow_extra_fields = True + filter_extra_fields = True + + current_password = formencode.All( + formencode.validators.NotEmpty(), + CurrentPasswordCorrect()) + + new_password = formencode.validators.NotEmpty() + confirm_password = formencode.validators.NotEmpty() + + chained_validators = [formencode.validators.FieldsMatch( + 'new_password', 'confirm_password')] + + +def change_password(request): + """ + Allows a user to change his or her password. + """ + + if not request.user: + return HTTPFound(location=request.route_url('home')) + + form = Form(request, schema=ChangePassword, state=request.user) + if form.validate(): + set_user_password(request.user, form.data['new_password']) + return HTTPFound(location=request.get_referrer()) + + return {'form': FormRenderer(form)} + + def includeme(config): config.add_route('login', '/login') @@ -99,3 +173,6 @@ def includeme(config): config.add_route('logout', '/logout') config.add_view(logout, route_name='logout') + + config.add_route('change_password', '/change-password') + config.add_view(change_password, route_name='change_password', renderer='/change_password.mako') From 8fe619895470367539519127ae55bd552b7dec62 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 18:22:10 -0800 Subject: [PATCH 14/59] style tweak for form errors --- edbob/pyramid/static/css/edbob.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/edbob.css index e09bcb0..a53ab9e 100644 --- a/edbob/pyramid/static/css/edbob.css +++ b/edbob/pyramid/static/css/edbob.css @@ -87,6 +87,16 @@ div.error { margin-bottom: 10px; } +ul.error { + color: #dd6666; + font-weight: bold; + padding: 0px; +} + +ul.error li { + list-style-type: none; +} + /* td.right { */ /* float: none; */ /* } */ From 50bd5bbf4d96b25de88f519875af472685695017 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Nov 2012 18:22:36 -0800 Subject: [PATCH 15/59] add formencode.Schema subclass for convenience --- edbob/pyramid/forms/simpleform.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/edbob/pyramid/forms/simpleform.py b/edbob/pyramid/forms/simpleform.py index c1d9c51..0abcec4 100644 --- a/edbob/pyramid/forms/simpleform.py +++ b/edbob/pyramid/forms/simpleform.py @@ -26,15 +26,29 @@ ``edbob.pyramid.forms.simpleform`` -- pyramid_simpleform Forms """ -import pyramid_simpleform from pyramid.renderers import render + +import formencode +import pyramid_simpleform from pyramid_simpleform.renderers import FormRenderer from edbob.pyramid import helpers from edbob.pyramid.forms import Form -__all__ = ['SimpleForm'] +__all__ = ['Schema', 'SimpleForm'] + + +class Schema(formencode.Schema): + """ + Subclass of ``formencode.Schema``, which exists only to ignore extra + fields. These normally would cause a schema instance to be deemed invalid, + and pretty much *every* form has a submit button which would be considered + an extra field. + """ + + allow_extra_fields = True + filter_extra_fields = True class SimpleForm(Form): From 76e37f0e75eb4c62e781a9a000cbbcb03bbcde13 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Nov 2012 16:06:35 -0800 Subject: [PATCH 16/59] split styles into base.css and layout.css --- .../static/css/{edbob.css => base.css} | 83 ------------------- edbob/pyramid/static/css/layout.css | 74 +++++++++++++++++ edbob/pyramid/templates/base.mako | 3 +- 3 files changed, 76 insertions(+), 84 deletions(-) rename edbob/pyramid/static/css/{edbob.css => base.css} (52%) create mode 100644 edbob/pyramid/static/css/layout.css diff --git a/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/base.css similarity index 52% rename from edbob/pyramid/static/css/edbob.css rename to edbob/pyramid/static/css/base.css index a53ab9e..b3b3408 100644 --- a/edbob/pyramid/static/css/edbob.css +++ b/edbob/pyramid/static/css/base.css @@ -96,86 +96,3 @@ ul.error { ul.error li { list-style-type: none; } - -/* td.right { */ -/* float: none; */ -/* } */ - -/* table.wrapper td.right { */ -/* vertical-align: bottom; */ -/* } */ - - -/****************************** - * Main Layout - ******************************/ - -html, body, #container { - height: 100%; -} - -body > #container { - height: auto; - min-height: 100%; -} - -#container { - margin: 0 auto; - width: 1000px; -} - -#header { - border-bottom: 1px solid #000000; - overflow: auto; -} - -#body { - padding-top: 15px; - padding-bottom: 5em; -} - -#footer { - margin-top: -4em; - text-align: center; -} - - -/****************************** - * Header - ******************************/ - -#header h1 { - margin: 0px 5px 10px 5px; -} - -#login { - margin: 8px 20px auto auto; -} - -#login a.username { - font-weight: bold; -} - -#user-menu { - float: left; -} - -#home-link { - font-weight: bold; -} - -#header-links { - float: right; - text-align: right; -} - -#main-menu { - border-top: 1px solid black; - clear: both; - font-weight: bold; -} - -#main-menu li { - display: inline; - margin-right: 15px; -} diff --git a/edbob/pyramid/static/css/layout.css b/edbob/pyramid/static/css/layout.css new file mode 100644 index 0000000..c6092ba --- /dev/null +++ b/edbob/pyramid/static/css/layout.css @@ -0,0 +1,74 @@ + +/****************************** + * Main Layout + ******************************/ + +html, body, #container { + height: 100%; +} + +body > #container { + height: auto; + min-height: 100%; +} + +#container { + margin: 0 auto; + width: 1000px; +} + +#header { + border-bottom: 1px solid #000000; + overflow: auto; +} + +#body { + padding-top: 15px; + padding-bottom: 5em; +} + +#footer { + margin-top: -4em; + text-align: center; +} + + +/****************************** + * Header + ******************************/ + +#header h1 { + margin: 0px 5px 10px 5px; +} + +#login { + margin: 8px 20px auto auto; +} + +#login a.username { + font-weight: bold; +} + +#user-menu { + float: left; +} + +#home-link { + font-weight: bold; +} + +#header-links { + float: right; + text-align: right; +} + +#main-menu { + border-top: 1px solid black; + clear: both; + font-weight: bold; +} + +#main-menu li { + display: inline; + margin-right: 15px; +} diff --git a/edbob/pyramid/templates/base.mako b/edbob/pyramid/templates/base.mako index b026322..dab999b 100644 --- a/edbob/pyramid/templates/base.mako +++ b/edbob/pyramid/templates/base.mako @@ -18,7 +18,8 @@ ${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.autocomplete.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'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/grids.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/filters.css'))} ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/forms.css'))} From 6ff7ff09b58ab3ba55d1dfa7399d96931d977dea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Nov 2012 17:10:21 -0800 Subject: [PATCH 17/59] add files.overwriting_move() --- edbob/files.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/edbob/files.py b/edbob/files.py index 3cab3b4..f60f542 100644 --- a/edbob/files.py +++ b/edbob/files.py @@ -100,6 +100,19 @@ def count_lines(path): return lines +def overwriting_move(src, dst): + """ + Convenience function which is equivalent to ``shutil.move()``, except it + will cause the destination file to be overwritten if it exists. + """ + + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + if os.path.exists(dst): + os.remove(dst) + shutil.move(src, dst) + + def resource_path(path): """ Returns a resource file path. ``path`` is assumed either to be a package From 6a478fc2102f664dc3ee1a3cc13bf8bef5ebed0a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Nov 2012 19:06:56 -0800 Subject: [PATCH 18/59] update changelog --- CHANGES.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index b8ebc9c..cb458ca 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,19 @@ +0.1a22 +------ + +- [feature] Added a view which allows a user to change his/her password from + within Pyramid web apps. + +- [general] Tweaked styles for form validation errors. + +- [general] Added convenience subclass of ``formencode.Schema``. + +- [general] Split CSS styles into ``base.css`` and ``layout.css``, since really + they serve different purposes. + +- [feature] Added ``overwriting_move()`` convenience function. + 0.1a21 ------ From 8d3e47443660583919fd1915d64d4b8efecfd5c9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Nov 2012 19:11:22 -0800 Subject: [PATCH 19/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index 48aa257..3d57e83 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a22' +__version__ = '0.1a23' From fb4a49b5700db5de9dea1e19e0026e7b296d2968 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Nov 2012 08:59:58 -0800 Subject: [PATCH 20/59] add win32.capture_output() --- edbob/win32.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/edbob/win32.py b/edbob/win32.py index e74e650..96a76bd 100644 --- a/edbob/win32.py +++ b/edbob/win32.py @@ -163,6 +163,20 @@ def RegDeleteTree(key, subkey): pass +def capture_output(command): + """ + Runs ``command`` and returns any output it produces. + """ + + # We *need* to pipe ``stdout`` because that's how we capture the output of + # the ``hg`` command. However, we must pipe *all* handles in order to + # prevent issues when running as a GUI but *from* the Windows console. + # See also: http://bugs.python.org/issue3905 + kwargs = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output = subprocess.Popen(command, **kwargs).communicate()[0] + return output + + def delayed_auto_start_service(name): """ Configures the Windows service named ``name`` such that its startup type is From ed04caf68c46f4045b79c307171f49e7c7257ac9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Nov 2012 09:00:33 -0800 Subject: [PATCH 21/59] remove pyramid/handlers (what was that still doing there?) --- edbob/pyramid/handlers/__init__.py | 0 edbob/pyramid/handlers/base.py | 291 ----------------------------- edbob/pyramid/handlers/util.py | 72 ------- 3 files changed, 363 deletions(-) delete mode 100644 edbob/pyramid/handlers/__init__.py delete mode 100644 edbob/pyramid/handlers/base.py delete mode 100644 edbob/pyramid/handlers/util.py diff --git a/edbob/pyramid/handlers/__init__.py b/edbob/pyramid/handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/edbob/pyramid/handlers/base.py b/edbob/pyramid/handlers/base.py deleted file mode 100644 index bf2eac2..0000000 --- a/edbob/pyramid/handlers/base.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.pyramid.handlers.base`` -- Base Handlers -""" - -from pyramid.renderers import render_to_response -from pyramid.httpexceptions import HTTPException, HTTPFound, HTTPOk, HTTPUnauthorized - -# import sqlahelper - -# # import rattail.pyramid.forms.util as util -# from rattail.db.perms import has_permission -# from rattail.pyramid.forms.formalchemy import Grid - - -class needs_perm(object): - """ - Decorator to be used for handler methods which should restrict access based - on the current user's permissions. - """ - - def __init__(self, permission, **kwargs): - self.permission = permission - self.kwargs = kwargs - - def __call__(self, fn): - permission = self.permission - kw = self.kwargs - def wrapped(self): - if not self.request.current_user: - self.request.session['referrer'] = self.request.url_generator.current() - self.request.session.flash("You must be logged in to do that.", 'error') - return HTTPFound(location=self.request.route_url('login')) - if not has_permission(self.request.current_user, permission): - self.request.session.flash("You do not have permission to do that.", 'error') - home = kw.get('redirect', self.request.route_url('home')) - return HTTPFound(location=home) - return fn(self) - return wrapped - - -def needs_user(fn): - """ - Decorator for handler methods which require simply that a user be currently - logged in. - """ - - def wrapped(self): - if not self.request.current_user: - self.request.session['referrer'] = self.request.url_generator.current() - self.request.session.flash("You must be logged in to do that.", 'error') - return HTTPFound(location=self.request.route_url('login')) - return fn(self) - return wrapped - - -class Handler(object): - - def __init__(self, request): - self.request = request - self.Session = sqlahelper.get_session() - - # def json_response(self, data={}): - # response = render_to_response('json', data, request=self.request) - # response.headers['Content-Type'] = 'application/json' - # return response - - -class CrudHandler(Handler): - # """ - # This handler provides all the goodies typically associated with general - # CRUD functionality, e.g. search filters and grids. - # """ - - def crud(self, cls, fieldset_factory, home=None, delete=None, post_sync=None, pre_render=None): - """ - Adds a common CRUD mechanism for objects. - - ``cls`` should be a SQLAlchemy-mapped class, presumably deriving from - :class:`rattail.Object`. - - ``fieldset_factory`` must be a callable which accepts the fieldset's - "model" as its only positional argument. - - ``home`` will be used as the redirect location once a form is fully - validated and data saved. If you do not speficy this parameter, the - user will be redirected to be the CRUD page for the new object (e.g. so - an object may be created before certain properties may be edited). - - ``delete`` may either be a string containing a URL to which the user - should be redirected after the object has been deleted, or else a - callback which will be executed *instead of* the normal algorithm - (which is merely to delete the object via the Session). - - ``post_sync`` may be a callback which will be executed immediately - after ``FieldSet.sync()`` is called, i.e. after validation as well. - - ``pre_render`` may be a callback which will be executed after any POST - processing has occured, but just before rendering. - """ - - uuid = self.request.params.get('uuid') - obj = self.Session.query(cls).get(uuid) if uuid else cls - assert obj - - if self.request.params.get('delete'): - if delete: - if isinstance(delete, basestring): - self.Session.delete(obj) - return HTTPFound(location=delete) - res = delete(obj) - if res: - return res - else: - self.Session.delete(obj) - if not home: - raise ValueError("Must specify 'home' or 'delete' url " - "in call to CrudHandler.crud()") - return HTTPFound(location=home) - - fs = fieldset_factory(obj) - - # if not fs.readonly and self.request.params.get('fieldset'): - # fs.rebind(data=self.request.params) - # if fs.validate(): - # fs.sync() - # if post_sync: - # res = post_sync(fs) - # if isinstance(res, HTTPFound): - # return res - # if self.request.params.get('partial'): - # self.Session.flush() - # return self.json_success(uuid=fs.model.uuid) - # return HTTPFound(location=self.request.route_url(objects, action='index')) - - if not fs.readonly and self.request.POST: - # print self.request.POST - fs.rebind(data=self.request.params) - if fs.validate(): - fs.sync() - if post_sync: - res = post_sync(fs) - if res: - return res - if self.request.params.get('partial'): - self.Session.flush() - return self.json_success(uuid=fs.model.uuid) - - if not home: - self.Session.flush() - home = self.request.url_generator.current() + '?uuid=' + fs.model.uuid - self.request.session.flash("%s \"%s\" has been %s." % ( - fs.crud_title, fs.get_display_text(), - 'updated' if fs.edit else 'created')) - return HTTPFound(location=home) - - data = {'fieldset': fs, 'crud': True} - - if pre_render: - res = pre_render(fs) - if res: - if isinstance(res, HTTPException): - return res - data.update(res) - - # data = {'fieldset':fs} - # if self.request.params.get('partial'): - # return render_to_response('/%s/crud_partial.mako' % objects, - # data, request=self.request) - # return data - - return data - - def grid(self, *args, **kwargs): - """ - Convenience function which returns a grid. The only functionality this - method adds is the ``session`` parameter. - """ - - return Grid(session=self.Session(), *args, **kwargs) - - # def get_grid(self, name, grid, query, search=None, url=None, **defaults): - # """ - # Convenience function for obtaining the configuration for a grid, - # and then obtaining the grid itself. - - # ``name`` is essentially the config key, e.g. ``'products.lookup'``, and - # in fact is expected to take that precise form (where the first part is - # considered the handler name and the second part the action name). - - # ``grid`` must be a callable with a signature of ``grid(query, - # config)``, and ``query`` will be passed directly to the ``grid`` - # callable. ``search`` will be used to inform the grid of the search in - # effect, if any. ``defaults`` will be used to customize the grid config. - # """ - - # if not url: - # handler, action = name.split('.') - # url = self.request.route_url(handler, action=action) - # config = util.get_grid_config(name, self.request, search, - # url=url, **defaults) - # return grid(query, config) - - # def get_search_form(self, name, labels={}, **defaults): - # """ - # Convenience function for obtaining the configuration for a search form, - # and then obtaining the form itself. - - # ``name`` is essentially the config key, e.g. ``'products.lookup'``. - # The ``labels`` dictionary can be used to override the default labels - # displayed for the various search fields. The ``defaults`` dictionary - # is used to customize the search config. - # """ - - # config = util.get_search_config(name, self.request, - # self.filter_map(), **defaults) - # form = util.get_search_form(config, **labels) - # return form - - # def object_crud(self, cls, objects=None, post_sync=None): - # """ - # This method is a desperate attempt to encapsulate shared CRUD logic - # which is useful across all editable data objects. - - # ``objects``, if provided, should be the plural name for the class as - # used in internal naming, e.g. ``'products'``. A default will be used - # if you do not provide this value. - - # ``post_sync``, if provided, should be a callable which accepts a - # ``formalchemy.Fieldset`` instance as its only argument. It will be - # called immediately after the fieldset is synced. - # """ - - # if not objects: - # objects = cls.__name__.lower() + 's' - - # uuid = self.request.params.get('uuid') - # obj = self.Session.query(cls).get(uuid) if uuid else cls - # assert obj - - # fs = self.fieldset(obj) - - # if not fs.readonly and self.request.params.get('fieldset'): - # fs.rebind(data=self.request.params) - # if fs.validate(): - # fs.sync() - # if post_sync: - # res = post_sync(fs) - # if isinstance(res, HTTPFound): - # return res - # if self.request.params.get('partial'): - # self.Session.flush() - # return self.json_success(uuid=fs.model.uuid) - # return HTTPFound(location=self.request.route_url(objects, action='index')) - - # data = {'fieldset':fs} - # if self.request.params.get('partial'): - # return render_to_response('/%s/crud_partial.mako' % objects, - # data, request=self.request) - # return data - - # def render_grid(self, grid, search=None, **kwargs): - # """ - # Convenience function to render a standard grid. Really just calls - # :func:`dtail.forms.util.render_grid()`. - # """ - - # return util.render_grid(self.request, grid, search, **kwargs) diff --git a/edbob/pyramid/handlers/util.py b/edbob/pyramid/handlers/util.py deleted file mode 100644 index 0530947..0000000 --- a/edbob/pyramid/handlers/util.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -################################################################################ -# -# edbob -- Pythonic Software Framework -# Copyright © 2010-2012 Lance Edgar -# -# This file is part of edbob. -# -# edbob is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# edbob is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with edbob. If not, see . -# -################################################################################ - -""" -``edbob.pyramid.handlers.util`` -- Handler Utilities -""" - -from pyramid.httpexceptions import HTTPFound - -from edbob.db.perms import has_permission - - -class needs_perm(object): - """ - Decorator to be used for handler methods which should restrict access based - on the current user's permissions. - """ - - def __init__(self, permission, **kwargs): - self.permission = permission - self.kwargs = kwargs - - def __call__(self, fn): - permission = self.permission - kw = self.kwargs - def wrapped(self): - if not self.request.current_user: - self.request.session['referrer'] = self.request.url_generator.current() - self.request.session.flash("You must be logged in to do that.", 'error') - return HTTPFound(location=self.request.route_url('login')) - if not has_permission(self.request.current_user, permission): - self.request.session.flash("You do not have permission to do that.", 'error') - home = kw.get('redirect', self.request.route_url('home')) - return HTTPFound(location=home) - return fn(self) - return wrapped - - -def needs_user(fn): - """ - Decorator for handler methods which require simply that a user be currently - logged in. - """ - - def wrapped(self): - if not self.request.current_user: - self.request.session['referrer'] = self.request.url_generator.current() - self.request.session.flash("You must be logged in to do that.", 'error') - return HTTPFound(location=self.request.route_url('login')) - return fn(self) - return wrapped From e95a23ead85287194b16473e03dee008955031a3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Nov 2012 09:42:02 -0800 Subject: [PATCH 22/59] tweak footer css --- edbob/pyramid/static/css/layout.css | 1 + 1 file changed, 1 insertion(+) diff --git a/edbob/pyramid/static/css/layout.css b/edbob/pyramid/static/css/layout.css index c6092ba..d389c0d 100644 --- a/edbob/pyramid/static/css/layout.css +++ b/edbob/pyramid/static/css/layout.css @@ -28,6 +28,7 @@ body > #container { } #footer { + clear: both; margin-top: -4em; text-align: center; } From 9b2589ca120f88f0b50a1b8bb64c3cd5c932c02d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Nov 2012 07:41:03 -0800 Subject: [PATCH 23/59] update user/role management (now it works) --- edbob/db/extensions/auth/model.py | 4 +- edbob/pyramid/grids/alchemy.py | 3 + edbob/pyramid/static/css/perms.css | 24 +- edbob/pyramid/subscribers.py | 7 + edbob/pyramid/templates/crud.mako | 17 +- edbob/pyramid/templates/edbob/crud.mako | 5 - .../templates/forms/fieldset_readonly.mako | 2 +- edbob/pyramid/templates/roles/base.mako | 2 - edbob/pyramid/templates/roles/crud.mako | 18 + edbob/pyramid/templates/roles/index.mako | 7 +- edbob/pyramid/templates/roles/role.mako | 15 - edbob/pyramid/views/roles.py | 334 ++++++++---------- edbob/pyramid/views/users.py | 6 +- 13 files changed, 225 insertions(+), 219 deletions(-) delete mode 100644 edbob/pyramid/templates/edbob/crud.mako delete mode 100644 edbob/pyramid/templates/roles/base.mako create mode 100644 edbob/pyramid/templates/roles/crud.mako delete mode 100644 edbob/pyramid/templates/roles/role.mako diff --git a/edbob/db/extensions/auth/model.py b/edbob/db/extensions/auth/model.py index 44f486b..5b5f122 100644 --- a/edbob/db/extensions/auth/model.py +++ b/edbob/db/extensions/auth/model.py @@ -89,7 +89,9 @@ class Role(Base): creator=lambda x: Permission(permission=x), getset_factory=getset_factory) - _users = relationship(UserRole, backref='role') + _users = relationship( + UserRole, backref='role', + cascade='save-update, merge, delete, delete-orphan') users = association_proxy('_users', 'user', creator=lambda x: UserRole(user=x), getset_factory=getset_factory) diff --git a/edbob/pyramid/grids/alchemy.py b/edbob/pyramid/grids/alchemy.py index 4402ac4..ca9e787 100644 --- a/edbob/pyramid/grids/alchemy.py +++ b/edbob/pyramid/grids/alchemy.py @@ -54,6 +54,9 @@ class AlchemyGrid(Grid): self._formalchemy_grid.prettify = prettify self.noclick_fields = [] + def __delattr__(self, attr): + delattr(self._formalchemy_grid, attr) + def __getattr__(self, attr): return getattr(self._formalchemy_grid, attr) diff --git a/edbob/pyramid/static/css/perms.css b/edbob/pyramid/static/css/perms.css index 764f8eb..86fcfce 100644 --- a/edbob/pyramid/static/css/perms.css +++ b/edbob/pyramid/static/css/perms.css @@ -1,17 +1,33 @@ /****************************** - * perms.css + * Permission Lists ******************************/ -div.field-couple.permissions div.field p.group { +div.field-wrapper.permissions div.field div.group { + margin-bottom: 10px; +} + +div.field-wrapper.permissions div.field div.group p { font-weight: bold; } -div.field-couple.permissions div.field label { +div.field-wrapper.permissions div.field label { float: none; font-weight: normal; } -div.field-couple.permissions div.field label input { +div.field-wrapper.permissions div.field label input { + margin-left: 15px; + margin-right: 10px; +} + +div.field-wrapper.permissions div.field div.group p.perm { + font-weight: normal; + margin-left: 15px; +} + +div.field-wrapper.permissions div.field div.group p.perm span { + font-family: monospace; + /* font-weight: bold; */ margin-right: 10px; } diff --git a/edbob/pyramid/subscribers.py b/edbob/pyramid/subscribers.py index 9331a35..0340d97 100644 --- a/edbob/pyramid/subscribers.py +++ b/edbob/pyramid/subscribers.py @@ -79,6 +79,13 @@ def context_found(event): return has_permission(request.user, perm, session=Session()) request.has_perm = has_perm + def has_any_perm(perms): + for perm in perms: + if has_permission(request.user, perm, session=Session()): + return True + return False + request.has_any_perm = has_any_perm + def get_referrer(default=None): if request.params.get('referrer'): return request.params['referrer'] diff --git a/edbob/pyramid/templates/crud.mako b/edbob/pyramid/templates/crud.mako index 0ca6e40..8844cd1 100644 --- a/edbob/pyramid/templates/crud.mako +++ b/edbob/pyramid/templates/crud.mako @@ -1,3 +1,18 @@ -<%inherit file="/edbob/crud.mako" /> +<%inherit file="/form.mako" /> + +<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+h.literal(str(form.fieldset.model))} + +<%def name="head_tags()"> + ${parent.head_tags()} + + ${parent.body()} diff --git a/edbob/pyramid/templates/edbob/crud.mako b/edbob/pyramid/templates/edbob/crud.mako deleted file mode 100644 index 4fc6112..0000000 --- a/edbob/pyramid/templates/edbob/crud.mako +++ /dev/null @@ -1,5 +0,0 @@ -<%inherit file="/form.mako" /> - -<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+h.literal(str(form.fieldset.model))} - -${parent.body()} diff --git a/edbob/pyramid/templates/forms/fieldset_readonly.mako b/edbob/pyramid/templates/forms/fieldset_readonly.mako index 0eea814..350a315 100644 --- a/edbob/pyramid/templates/forms/fieldset_readonly.mako +++ b/edbob/pyramid/templates/forms/fieldset_readonly.mako @@ -1,7 +1,7 @@
    % for field in fieldset.render_fields.itervalues(): % if field.requires_label: -
    +
    ${field.label_tag()|n}
    ${field.render_readonly()} diff --git a/edbob/pyramid/templates/roles/base.mako b/edbob/pyramid/templates/roles/base.mako deleted file mode 100644 index 27f7dd9..0000000 --- a/edbob/pyramid/templates/roles/base.mako +++ /dev/null @@ -1,2 +0,0 @@ -<%inherit file="/base.mako" /> -${parent.body()} diff --git a/edbob/pyramid/templates/roles/crud.mako b/edbob/pyramid/templates/roles/crud.mako new file mode 100644 index 0000000..863a773 --- /dev/null +++ b/edbob/pyramid/templates/roles/crud.mako @@ -0,0 +1,18 @@ +<%inherit file="edbob.pyramid:templates/crud.mako" /> + +<%def name="head_tags()"> + ${parent.head_tags()} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/perms.css'))} + + +<%def name="context_menu_items()"> +
  • ${h.link_to("Back to Roles", url('roles'))}
  • + % if form.readonly: +
  • ${h.link_to("Edit this Role", url('role.update', uuid=form.fieldset.model.uuid))}
  • + % elif form.updating: +
  • ${h.link_to("View this Role", url('role.read', uuid=form.fieldset.model.uuid))}
  • + % endif +
  • ${h.link_to("Delete this Role", url('role.delete', uuid=form.fieldset.model.uuid), class_='delete')}
  • + + +${parent.body()} diff --git a/edbob/pyramid/templates/roles/index.mako b/edbob/pyramid/templates/roles/index.mako index 773153a..49deacb 100644 --- a/edbob/pyramid/templates/roles/index.mako +++ b/edbob/pyramid/templates/roles/index.mako @@ -1,10 +1,11 @@ -<%inherit file="/roles/base.mako" /> -<%inherit file="/index.mako" /> +<%inherit file="/grid.mako" /> <%def name="title()">Roles <%def name="context_menu_items()"> -
  • ${h.link_to("Create a new Role", url('role.new'))}
  • + % if request.has_perm('roles.create'): +
  • ${h.link_to("Create a new Role", url('role.create'))}
  • + % endif ${parent.body()} diff --git a/edbob/pyramid/templates/roles/role.mako b/edbob/pyramid/templates/roles/role.mako deleted file mode 100644 index a532b06..0000000 --- a/edbob/pyramid/templates/roles/role.mako +++ /dev/null @@ -1,15 +0,0 @@ -<%inherit file="/roles/base.mako" /> -<%inherit file="/crud.mako" /> - -<%def name="crud_name()">Role - -<%def name="head_tags()"> - ${parent.head_tags()} - ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/perms.css'))} - - -<%def name="menu()"> -

    ${h.link_to("Back to Roles", url('roles.list'))}

    - - -${parent.body()} diff --git a/edbob/pyramid/views/roles.py b/edbob/pyramid/views/roles.py index 54c86ca..1281937 100644 --- a/edbob/pyramid/views/roles.py +++ b/edbob/pyramid/views/roles.py @@ -26,75 +26,83 @@ ``edbob.pyramid.views.roles`` -- Role Views """ -import transaction from pyramid.httpexceptions import HTTPFound -from formalchemy import Field, FieldRenderer -from webhelpers.html import literal -from webhelpers.html.tags import checkbox, hidden +import formalchemy +from webhelpers.html import tags +from webhelpers.html.builder import HTML import edbob -from edbob.db.auth import administrator_role, has_permission -from edbob.pyramid import filters -from edbob.pyramid import forms -from edbob.pyramid import grids +from edbob.db import auth from edbob.pyramid import Session +from edbob.pyramid.views import SearchableAlchemyGridView, CrudView -def filter_map(): - return filters.get_filter_map( - edbob.Role, - ilike=['name']) +default_permissions = [ -def search_config(request, fmap): - return filters.get_search_config( - 'roles.list', request, fmap, - include_filter_name=True, - filter_type_name='lk') + ("People", [ + ('people.list', "List People"), + ('people.read', "View Person"), + ('people.create', "Create Person"), + ('people.update', "Edit Person"), + ('people.delete', "Delete Person"), + ]), -def search_form(config): - return filters.get_search_form(config) + ("Roles", [ + ('roles.list', "List Roles"), + ('roles.read', "View Role"), + ('roles.create', "Create Role"), + ('roles.update', "Edit Role"), + ('roles.delete', "Delete Role"), + ]), -def grid_config(request, search, fmap): - return grids.get_grid_config( - 'roles.list', request, search, - filter_map=fmap, sort='name') - -def sort_map(): - return grids.get_sort_map(edbob.Role, ['name']) - -def query(config): - smap = sort_map() - q = Session.query(edbob.Role) - q = filters.filter_query(q, config) - q = grids.sort_query(q, config, smap) - return q + ("Users", [ + ('users.list', "List Users"), + ('users.read', "View User"), + ('users.create', "Create User"), + ('users.update', "Edit User"), + ('users.delete', "Delete User"), + ]), + ] -def roles(request): +class RolesGrid(SearchableAlchemyGridView): - fmap = filter_map() - config = search_config(request, fmap) - search = search_form(config) - config = grid_config(request, search, fmap) - roles = grids.get_pager(query, config) + mapped_class = edbob.Role + config_prefix = 'roles' + sort = 'name' - g = forms.AlchemyGrid( - edbob.Role, roles, config, - gridurl=request.route_url('roles.list'), - objurl='role.edit') + def filter_map(self): + return self.make_filter_map(ilike=['name']) - g.configure( - include=[ - g.name, - ], - readonly=True) + def filter_config(self): + return self.make_filter_config( + include_filter_name=True, + filter_type_name='lk') - grid = g.render(class_='clickable roles') - return grids.render_grid(request, grid, search) + def sort_map(self): + return self.make_sort_map('name') + + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.name, + ], + readonly=True) + if self.request.has_perm('roles.read'): + g.clickable = True + g.click_route_name = 'role.read' + if self.request.has_perm('roles.update'): + g.editable = True + g.edit_route_name = 'role.update' + if self.request.has_perm('roles.delete'): + g.deletable = True + g.delete_route_name = 'role.delete' + return g -class PermissionsField(Field): +class PermissionsField(formalchemy.Field): def sync(self): if not self.is_readonly(): @@ -102,154 +110,108 @@ class PermissionsField(Field): role.permissions = self.renderer.deserialize() -class PermissionsFieldRenderer(FieldRenderer): +def PermissionsFieldRenderer(permissions, *args, **kwargs): - available_permissions = [ - - ("Batches", [ - ('batches.list', "List Batches"), - ('batches.edit', "Edit Batch"), - ('batches.create', "Create Batch"), - ]), - - ("Roles", [ - ('roles.list', "List Roles"), - ('roles.edit', "Edit Role"), - ('roles.create', "Create Role"), - ]), - ] - - def deserialize(self): - perms = [] - i = len(self.name) + 1 - for key in self.params: - if key.startswith(self.name): - perms.append(key[i:]) - return perms - - def _render(self, readonly=False, **kwargs): - # result = literal('') - # for group_name, group_label, perm_list in self.field.model_value: - # rendered_group_name = literal('

    ' + group_label + '

    \n') - # if readonly: - # result += literal('') + rendered_group_name + literal('') - # else: - # result += rendered_group_name - # result += literal('
    ') - # for perm_name, perm_label, checked in perm_list: - # if readonly: - # result += literal('' - # + '' + ('[X]' if checked else '[  ]') + '' - # + '' + perm_label + '' - # + '\n') - # else: - # name = '.'.join((self.name, group_name, perm_name)) - # result += check_box(name, label=perm_label, checked=checked) - # if not readonly: - # result += literal('
    ') - # if readonly: - # return literal('') + result + literal('
    ') - # return literal('
    ') + result + literal('
    ') - - role = self.field.model - if role is administrator_role(Session()): - res = literal('

    This is the administrative role; ' - 'it has full access to the entire system.

    ') - if not readonly: - res += hidden(self.name, value='') # ugly hack..or good idea? - else: - res = '' - for group, perms in self.available_permissions: - res += literal('

    %s

    ' % group) - for perm, title in perms: - if readonly: - res += literal('

    %s

    ' % title) - else: - checked = has_permission(role, perm) - res += checkbox(self.name + '-' + perm, - checked=checked, label=title) - return res - - def render(self, **kwargs): - return self._render(**kwargs) - - def render_readonly(self, **kwargs): - return self._render(readonly=True, **kwargs) - - -def role_fieldset(role, request): - fs = forms.make_fieldset(role, url=request.route_url, - url_action=request.current_route_url(), - route_name='roles.list') + perms = permissions - fs.append(PermissionsField('permissions', - renderer=PermissionsFieldRenderer)) + class PermissionsFieldRenderer(formalchemy.FieldRenderer): - fs.configure( - include=[ - fs.name, - fs.permissions, - ]) + permissions = perms - if not fs.edit: - del fs.permissions + def deserialize(self): + perms = [] + i = len(self.name) + 1 + for key in self.params: + if key.startswith(self.name): + perms.append(key[i:]) + return perms - return fs + def _render(self, readonly=False, **kwargs): + role = self.field.model + admin = auth.administrator_role(Session()) + if role is admin: + html = HTML.tag('p', c="This is the administrative role; " + "it has full access to the entire system.") + if not readonly: + html += tags.hidden(self.name, value='') # ugly hack..or good idea? + else: + html = '' + for group, perms in self.permissions: + inner = HTML.tag('p', c=group) + for perm, title in perms: + checked = auth.has_permission(role, perm, Session()) + if readonly: + span = HTML.tag('span', c="[X]" if checked else "[ ]") + inner += HTML.tag('p', class_='perm', c=span + ' ' + title) + else: + checked = auth.has_permission(role, perm, Session()) + inner += tags.checkbox(self.name + '-' + perm, + checked=checked, label=title) + html += HTML.tag('div', class_='group', c=inner) + return html + + def render(self, **kwargs): + return self._render(**kwargs) + + def render_readonly(self, **kwargs): + return self._render(readonly=True, **kwargs) + + return PermissionsFieldRenderer -def new_role(request): +class RoleCrud(CrudView): - fs = role_fieldset(edbob.Role, request) - if request.POST: - fs.rebind(data=request.params) - if fs.validate(): + mapped_class = edbob.Role + home_route = 'roles' + permissions = default_permissions - with transaction.manager: - fs.sync() - fs.model = Session.merge(fs.model) - request.session.flash("%s \"%s\" has been %s." % ( - fs.crud_title, fs.get_display_text(), - 'updated' if fs.edit else 'created')) - home = request.route_url('roles.list') + def fieldset(self, role): + fs = self.make_fieldset(role) + fs.append(PermissionsField( + 'permissions', + renderer=PermissionsFieldRenderer(self.permissions))) + fs.configure( + include=[ + fs.name, + fs.permissions, + ]) + return fs - return HTTPFound(location=home) - - return {'fieldset': fs, 'crud': True} - - -def edit_role(request): - uuid = request.matchdict['uuid'] - role = Session.query(edbob.Role).get(uuid) if uuid else None - assert role - - fs = role_fieldset(role, request) - if request.POST: - fs.rebind(data=request.params) - if fs.validate(): - - with transaction.manager: - Session.add(fs.model) - fs.sync() - request.session.flash("%s \"%s\" has been %s." % ( - fs.crud_title, fs.get_display_text(), - 'updated' if fs.edit else 'created')) - home = request.route_url('roles.list') - - return HTTPFound(location=home) - - return {'fieldset': fs, 'crud': True} + def pre_delete(self, model): + admin = auth.administrator_role(Session()) + guest = auth.guest_role(Session()) + if model in (admin, guest): + self.request.session.flash("You may not delete the %s role." % str(model), 'error') + return HTTPFound(location=self.request.get_referrer()) def includeme(config): + + config.add_route('roles', '/roles') + config.add_view(RolesGrid, route_name='roles', + renderer='/roles/index.mako', + permission='roles.list') - config.add_route('roles.list', '/roles') - config.add_view(roles, route_name='roles.list', renderer='/roles/index.mako', - permission='roles.list', http_cache=0) + settings = config.get_settings() + perms = settings.get('edbob.permissions') + if perms: + RoleCrud.permissions = perms - config.add_route('role.new', '/roles/new') - config.add_view(new_role, route_name='role.new', renderer='/roles/role.mako', - permission='roles.create', http_cache=0) + config.add_route('role.create', '/roles/new') + config.add_view(RoleCrud, attr='create', route_name='role.create', + renderer='/roles/crud.mako', + permission='roles.create') - config.add_route('role.edit', '/roles/{uuid}/edit') - config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako', - permission='roles.edit', http_cache=0) + config.add_route('role.read', '/roles/{uuid}') + config.add_view(RoleCrud, attr='read', route_name='role.read', + renderer='/roles/crud.mako', + permission='roles.read') + + config.add_route('role.update', '/roles/{uuid}/edit') + config.add_view(RoleCrud, attr='update', route_name='role.update', + renderer='/roles/crud.mako', + permission='roles.update') + + config.add_route('role.delete', '/roles/{uuid}/delete') + config.add_view(RoleCrud, attr='delete', route_name='role.delete', + permission='roles.delete') diff --git a/edbob/pyramid/views/users.py b/edbob/pyramid/views/users.py index 1ef9500..0ed80c5 100644 --- a/edbob/pyramid/views/users.py +++ b/edbob/pyramid/views/users.py @@ -95,7 +95,7 @@ class _RolesFieldRenderer(SelectFieldRenderer): role = roles.get(uuid) res += literal('
  • %s
  • ' % ( tags.link_to(role.name, - self.request.route_url('role.edit', uuid=role.uuid)))) + self.request.route_url('role.read', uuid=role.uuid)))) res += literal('') return res @@ -206,6 +206,10 @@ class UserCrud(CrudView): fs.roles, ]) + if self.readonly: + del fs.password + del fs.confirm_password + # if fs.edit and user.person: if isinstance(user, edbob.User) and user.person: fs.person.set(readonly=True, From 9eb33258b4d1c9d67ac89e5ff9c258749440753d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Nov 2012 15:14:46 -0800 Subject: [PATCH 24/59] update changelog --- CHANGES.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index cb458ca..7d4a904 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,14 @@ +0.1a23 +------ + +- [feature] Added ``capture_output()`` function to ``win32`` module. This is a + convenience function which works around an issue when attempting to capture + output from a command when the calling application is a Windows GUI app which + was launched via the Windows console (DOS terminal). + +- [feature] Updated ``User`` and ``Role`` management views for Pyramid apps. + 0.1a22 ------ From ba2c99d5035975d1d64a0a1473f85cc47adaaebc Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Nov 2012 15:17:22 -0800 Subject: [PATCH 25/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index 3d57e83..7732950 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a23' +__version__ = '0.1a24' From c79d6de56d25139ddc0b0a4b49c4a3d400689c21 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Nov 2012 22:52:37 -0800 Subject: [PATCH 26/59] fix guest bug in role perms editing --- edbob/db/auth.py | 5 +++-- edbob/pyramid/views/roles.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/edbob/db/auth.py b/edbob/db/auth.py index ad7d72f..31fbb5b 100644 --- a/edbob/db/auth.py +++ b/edbob/db/auth.py @@ -105,7 +105,7 @@ def grant_permission(role, permission, session=None): role.permissions.append(permission) -def has_permission(obj, perm, session=None): +def has_permission(obj, perm, include_guest=True, session=None): """ Checks the given ``obj`` (which may be either a :class:`edbob.User`` or :class:`edbob.Role` instance), and returns a boolean indicating whether or @@ -124,8 +124,9 @@ def has_permission(obj, perm, session=None): if not session: session = object_session(obj) assert session + if include_guest: + roles.append(guest_role(session)) admin = administrator_role(session) - roles.append(guest_role(session)) for role in roles: if role is admin: return True diff --git a/edbob/pyramid/views/roles.py b/edbob/pyramid/views/roles.py index 1281937..2d0aa57 100644 --- a/edbob/pyramid/views/roles.py +++ b/edbob/pyramid/views/roles.py @@ -139,12 +139,12 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs): for group, perms in self.permissions: inner = HTML.tag('p', c=group) for perm, title in perms: - checked = auth.has_permission(role, perm, Session()) + checked = auth.has_permission( + role, perm, include_guest=False, session=Session()) if readonly: span = HTML.tag('span', c="[X]" if checked else "[ ]") inner += HTML.tag('p', class_='perm', c=span + ' ' + title) else: - checked = auth.has_permission(role, perm, Session()) inner += tags.checkbox(self.name + '-' + perm, checked=checked, label=title) html += HTML.tag('div', class_='group', c=inner) From c2339ba124b061da641dd0235e70c5c0d0e7a78f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 12 Nov 2012 22:53:27 -0800 Subject: [PATCH 27/59] exclude guest when editing user roles --- edbob/pyramid/views/users.py | 38 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/edbob/pyramid/views/users.py b/edbob/pyramid/views/users.py index 0ed80c5..a2e201e 100644 --- a/edbob/pyramid/views/users.py +++ b/edbob/pyramid/views/users.py @@ -26,13 +26,14 @@ ``edbob.pyramid.views.users`` -- User Views """ -from webhelpers.html import literal, tags +from webhelpers.html import tags +from webhelpers.html.builder import HTML import formalchemy from formalchemy.fields import SelectFieldRenderer import edbob -from edbob.db.auth import set_user_password +from edbob.db import auth from edbob.pyramid import Session from edbob.pyramid.views import SearchableAlchemyGridView, CrudView @@ -86,22 +87,22 @@ class UsersGrid(SearchableAlchemyGridView): return g -class _RolesFieldRenderer(SelectFieldRenderer): - - def render_readonly(self, **kwargs): - roles = Session.query(edbob.Role) - res = literal('
      ') - for uuid in self.value: - role = roles.get(uuid) - res += literal('
    • %s
    • ' % ( - tags.link_to(role.name, - self.request.route_url('role.read', uuid=role.uuid)))) - res += literal('
    ') - return res - - def RolesFieldRenderer(request): - return type('RolesFieldRenderer', (_RolesFieldRenderer,), {'request': request}) + + class RolesFieldRenderer(SelectFieldRenderer): + + def render_readonly(self, **kwargs): + roles = Session.query(edbob.Role) + html = '' + for uuid in self.value: + role = roles.get(uuid) + link = tags.link_to( + role.name, request.route_url('role.read', uuid=role.uuid)) + html += HTML.tag('li', c=link) + html = HTML.tag('ul', c=html) + return html + + return RolesFieldRenderer class RolesField(formalchemy.Field): @@ -117,6 +118,7 @@ class RolesField(formalchemy.Field): def get_options(self): q = Session.query(edbob.Role.name, edbob.Role.uuid) + q = q.filter(edbob.Role.uuid != auth.guest_role(Session()).uuid) q = q.order_by(edbob.Role.name) return q.all() @@ -180,7 +182,7 @@ class PasswordField(formalchemy.Field): if not self.is_readonly(): password = self.renderer.deserialize() if password: - set_user_password(self.model, password) + auth.set_user_password(self.model, password) class UserCrud(CrudView): From a249fceed56dc0fc7a64144f1b21922681bf4d06 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 13 Nov 2012 07:55:38 -0800 Subject: [PATCH 28/59] update changelog --- CHANGES.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 7d4a904..a6078f0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,12 @@ +0.1a24 +------ + +- [bug] Fixed bug where creating a new ``Role`` with the UI would use default + permissions of the Guest role. Now the default permissions are empty. + +- [bug] Fixed ``User.roles`` UI so that the Guest role is never shown. + 0.1a23 ------ From c3f58d1b8c6723317d6da853fc372c83796a01f6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 13 Nov 2012 07:56:54 -0800 Subject: [PATCH 29/59] bump version --- edbob/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edbob/_version.py b/edbob/_version.py index 7732950..3badee1 100644 --- a/edbob/_version.py +++ b/edbob/_version.py @@ -1 +1 @@ -__version__ = '0.1a24' +__version__ = '0.1a25' From 8217b91aa47063aca0ae0e55cf85cace0ea1ba3f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 14 Nov 2012 05:06:31 -0800 Subject: [PATCH 30/59] add sqlerror_tween --- edbob/pyramid/tweens.py | 59 +++++++++++++++++++ .../edbob/+package+/pyramid/__init__.py_tmpl | 26 ++++---- 2 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 edbob/pyramid/tweens.py diff --git a/edbob/pyramid/tweens.py b/edbob/pyramid/tweens.py new file mode 100644 index 0000000..8039efa --- /dev/null +++ b/edbob/pyramid/tweens.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.tweens`` -- Tween Factories +""" + +import sqlalchemy.exc + +from transaction.interfaces import TransientError + + +def sqlerror_tween_factory(handler, registry): + """ + Produces a tween which will convert ``sqlalchemy.exc.OperationalError`` + instances (caused by database server restart) into a retryable + ``transaction.interfaces.TransientError`` instance, so that a second + attempt may be made to connect to the database before really giving up. + + .. note:: + This tween alone is not enough to cause the transaction to be retried; + it only marks the error as being *retryable*. If you wish more than one + attempt to be made, you must define the ``tm.attempts`` setting within + your Pyramid app configuration. See `Retrying + `_ + for more information. + """ + + def sqlerror_tween(request): + try: + response = handler(request) + except sqlalchemy.exc.OperationalError, error: + if error.connection_invalidated: + raise TransientError(str(error)) + raise + return response + + return sqlerror_tween diff --git a/edbob/scaffolds/edbob/+package+/pyramid/__init__.py_tmpl b/edbob/scaffolds/edbob/+package+/pyramid/__init__.py_tmpl index efdc27d..155371b 100644 --- a/edbob/scaffolds/edbob/+package+/pyramid/__init__.py_tmpl +++ b/edbob/scaffolds/edbob/+package+/pyramid/__init__.py_tmpl @@ -7,10 +7,8 @@ import os.path from pyramid.config import Configurator -from pyramid.authentication import SessionAuthenticationPolicy import edbob -from edbob.pyramid.auth import EdbobAuthorizationPolicy def main(global_config, **settings): @@ -27,24 +25,18 @@ def main(global_config, **settings): # * Raise an exception if a setting is missing or invalid. # * Convert values from strings to their intended type. - settings['mako.directories'] = [ - '{{package}}.pyramid:templates', - 'edbob.pyramid:templates', - ] + settings.setdefault('mako.directories', [ + '{{package}}.pyramid:templates', + 'edbob.pyramid:templates', + ]) + + # Make two attempts when "retryable" errors happen during transactions. + settings.setdefault('tm.attempts', 2) config = Configurator(settings=settings) # Configure edbob edbob.init('{{package}}', os.path.abspath(settings['edbob.config'])) - - # Configure session - config.include('pyramid_beaker') - - # Configure auth - config.set_authentication_policy(SessionAuthenticationPolicy()) - config.set_authorization_policy(EdbobAuthorizationPolicy()) - - # Include "core" stuff provided by edbob. config.include('edbob.pyramid') # Additional config is defined elsewhere within {{project}}. This includes @@ -53,4 +45,8 @@ def main(global_config, **settings): config.include('{{package}}.pyramid.subscribers') config.include('{{package}}.pyramid.views') + # Consider PostgreSQL server restart errors to be "retryable." + config.add_tween('edbob.pyramid.tweens.sqlerror_tween_factory', + under='pyramid_tm.tm_tween_factory') + return config.make_wsgi_app() From 7928461e08905146a9e5d8c13238ef5ece64f864 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 21 Nov 2012 10:05:01 -0800 Subject: [PATCH 31/59] add PHONE_TYPE enum --- edbob/db/extensions/contact/enum.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/edbob/db/extensions/contact/enum.py b/edbob/db/extensions/contact/enum.py index 9972273..bf09e97 100644 --- a/edbob/db/extensions/contact/enum.py +++ b/edbob/db/extensions/contact/enum.py @@ -38,3 +38,14 @@ EMAIL_PREFERENCE = { EMAIL_PREFERENCE_HTML : "HTML", EMAIL_PREFERENCE_MOBILE : "Mobile", } + + +PHONE_TYPE_HOME = 'home' +PHONE_TYPE_MOBILE = 'mobile' +PHONE_TYPE_OTHER = 'other' + +PHONE_TYPE = { + PHONE_TYPE_HOME : "Home", + PHONE_TYPE_MOBILE : "Mobile", + PHONE_TYPE_OTHER : "Other", + } From a1d22df20c4844c56b844fbcc96b47ae2cf590c8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 21 Nov 2012 10:05:24 -0800 Subject: [PATCH 32/59] add some __unicode__() methods --- edbob/db/extensions/auth/model.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/edbob/db/extensions/auth/model.py b/edbob/db/extensions/auth/model.py index 5b5f122..a3f36f0 100644 --- a/edbob/db/extensions/auth/model.py +++ b/edbob/db/extensions/auth/model.py @@ -53,8 +53,8 @@ class Permission(Base): def __repr__(self): return "" % (self.role, self.permission) - def __str__(self): - return str(self.permission or '') + def __unicode__(self): + return unicode(self.permission or '') class UserRole(Base): @@ -99,8 +99,8 @@ class Role(Base): def __repr__(self): return "" % self.name - def __str__(self): - return str(self.name or '') + def __unicode__(self): + return unicode(self.name or '') class User(Base): @@ -126,15 +126,16 @@ class User(Base): def __repr__(self): return "" % self.username - def __str__(self): - return str(self.username or '') + def __unicode__(self): + return unicode(self.username or '') @property def display_name(self): """ - Returns the user's ``person.display_name``, if present, otherwise the - ``username``. + Returns :attr:`Person.display_name` if present; otherwise returns + :attr:`username`. """ + if self.person and self.person.display_name: return self.person.display_name return self.username From d8744f3958dcab474a7dd7d1e1d3a99c43f99e8c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 26 Nov 2012 11:15:59 -0800 Subject: [PATCH 33/59] move contact enum to core --- edbob/__init__.py | 1 + edbob/{db/extensions/contact => }/enum.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename edbob/{db/extensions/contact => }/enum.py (96%) diff --git a/edbob/__init__.py b/edbob/__init__.py index 8ab239f..e1574df 100644 --- a/edbob/__init__.py +++ b/edbob/__init__.py @@ -28,6 +28,7 @@ from edbob._version import __version__ +from edbob.enum import * from edbob.core import * from edbob.time import * from edbob.files import * diff --git a/edbob/db/extensions/contact/enum.py b/edbob/enum.py similarity index 96% rename from edbob/db/extensions/contact/enum.py rename to edbob/enum.py index bf09e97..958dd7e 100644 --- a/edbob/db/extensions/contact/enum.py +++ b/edbob/enum.py @@ -23,7 +23,7 @@ ################################################################################ """ -``edbob.db.extensions.contact.enum`` -- Enumerations +``edbob.enum`` -- Enumerations """ From 64ae8e9136d830ffc36702d2009c21007e05a4f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 27 Nov 2012 11:27:31 -0800 Subject: [PATCH 34/59] fix css in progress template --- edbob/pyramid/templates/progress.mako | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edbob/pyramid/templates/progress.mako b/edbob/pyramid/templates/progress.mako index 7626e46..ea83598 100644 --- a/edbob/pyramid/templates/progress.mako +++ b/edbob/pyramid/templates/progress.mako @@ -5,7 +5,8 @@ Working... ${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'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))} + ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}