From 8d3e47443660583919fd1915d64d4b8efecfd5c9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Nov 2012 19:11:22 -0800 Subject: [PATCH 01/41] 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 02/41] 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 03/41] 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 04/41] 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 05/41] 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 06/41] 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 07/41] 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 08/41] 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 09/41] 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 10/41] 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 11/41] 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 12/41] 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 13/41] 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 14/41] 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 15/41] 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 16/41] 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'))}