From ba94a015a6cd56d6f09b42824a89b49071ee92a2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 21 Apr 2012 12:45:58 -0500 Subject: [PATCH] initial users/roles/perms admin stuff --- edbob/db/auth.py | 21 +- edbob/db/extensions/auth/model.py | 87 +++++- edbob/db/model.py | 83 ----- edbob/pyramid/__init__.py | 25 +- edbob/pyramid/auth.py | 57 ++++ .../edbob/+package+/commands.py_tmpl | 1 - edbob/pyramid/static/css/edbob.css | 16 +- edbob/pyramid/static/css/perms.css | 17 ++ edbob/pyramid/subscribers.py | 2 +- edbob/pyramid/templates/edbob/login.mako | 2 +- edbob/pyramid/templates/people/base.mako | 2 + edbob/pyramid/templates/people/index.mako | 12 + edbob/pyramid/templates/people/person.mako | 29 ++ edbob/pyramid/templates/roles/base.mako | 2 + edbob/pyramid/templates/roles/index.mako | 10 + edbob/pyramid/templates/roles/role.mako | 15 + edbob/pyramid/templates/users/base.mako | 2 + edbob/pyramid/templates/users/index.mako | 10 + edbob/pyramid/templates/users/user.mako | 10 + edbob/pyramid/views/__init__.py | 30 +- edbob/pyramid/views/auth.py | 14 +- edbob/pyramid/views/people.py | 180 +++++++++++ edbob/pyramid/views/roles.py | 255 ++++++++++++++++ edbob/pyramid/views/users.py | 289 ++++++++++++++++++ 24 files changed, 1061 insertions(+), 110 deletions(-) create mode 100644 edbob/pyramid/auth.py create mode 100644 edbob/pyramid/static/css/perms.css create mode 100644 edbob/pyramid/templates/people/base.mako create mode 100644 edbob/pyramid/templates/people/index.mako create mode 100644 edbob/pyramid/templates/people/person.mako create mode 100644 edbob/pyramid/templates/roles/base.mako create mode 100644 edbob/pyramid/templates/roles/index.mako create mode 100644 edbob/pyramid/templates/roles/role.mako create mode 100644 edbob/pyramid/templates/users/base.mako create mode 100644 edbob/pyramid/templates/users/index.mako create mode 100644 edbob/pyramid/templates/users/user.mako create mode 100644 edbob/pyramid/views/people.py create mode 100644 edbob/pyramid/views/roles.py create mode 100644 edbob/pyramid/views/users.py diff --git a/edbob/db/auth.py b/edbob/db/auth.py index eaa3664..9209013 100644 --- a/edbob/db/auth.py +++ b/edbob/db/auth.py @@ -70,10 +70,10 @@ def administrator_role(session): """ uuid = 'd937fa8a965611dfa0dd001143047286' - admin = session.query(Role).get(uuid) + admin = session.query(edbob.Role).get(uuid) if admin: return admin - admin = Role(uuid=uuid, name='Administrator') + admin = edbob.Role(uuid=uuid, name='Administrator') session.add(admin) return admin @@ -86,15 +86,15 @@ def has_permission(obj, perm): fully-qualified permission name, e.g. ``'users.create'``. """ - if isinstance(obj, User): + if isinstance(obj, edbob.User): roles = obj.roles - elif isinstance(obj, Role): + elif isinstance(obj, edbob.Role): roles = [obj] else: raise TypeError("You must pass either a User or Role for 'obj'; got: %s" % repr(obj)) session = object_session(obj) assert session - admin = get_administrator(session) + admin = administrator_role(session) for role in roles: if role is admin: return True @@ -107,15 +107,18 @@ def has_permission(obj, perm): def init_database(engine, session): """ Initialize the auth system within an ``edbob`` database. + + Currently this only creates an :class:`edbob.User` instance with username + ``'admin'`` (and password the same), and assigns the user to the built-in + administrative role (see :func:`administrator_role()`). """ - # Create 'admin' user with full rights. - admin = edbob.User() - admin.username = 'admin' + admin = edbob.User(username='admin') set_user_password(admin, 'admin') - # admin.roles.append(administrator_role(session)) + admin.roles.append(administrator_role(session)) session.add(admin) session.flush() + print "Created 'admin' user with password 'admin'" def set_user_password(user, password): diff --git a/edbob/db/extensions/auth/model.py b/edbob/db/extensions/auth/model.py index 7bf25fb..61514d2 100644 --- a/edbob/db/extensions/auth/model.py +++ b/edbob/db/extensions/auth/model.py @@ -32,9 +32,10 @@ from sqlalchemy.ext.associationproxy import association_proxy import edbob from edbob.db.model import Base, uuid_column +from edbob.sqlalchemy import getset_factory -__all__ = ['Person', 'User'] +__all__ = ['Person', 'Role', 'User', 'UserRole', 'Permission'] def get_person_display_name(context): @@ -65,6 +66,68 @@ class Person(Base): return str(self.display_name or '') +class Permission(Base): + """ + Represents the fact that a particular :class:`Role` is allowed to do a + particular type of thing. + """ + + __tablename__ = 'permissions' + + role_uuid = Column(String(32), ForeignKey('roles.uuid'), primary_key=True) + permission = Column(String(50), primary_key=True) + + def __repr__(self): + return "" % (self.role, self.permission) + + def __str__(self): + return str(self.permission or '') + + +class UserRole(Base): + """ + Represents the association between a :class:`User` and a :class:`Role`. + """ + + __tablename__ = 'users_roles' + + uuid = uuid_column() + user_uuid = Column(String(32), ForeignKey('users.uuid')) + role_uuid = Column(String(32), ForeignKey('roles.uuid')) + + def __repr__(self): + return "" % (self.user, self.role) + + +class Role(Base): + """ + Represents a role within the system; used to manage permissions. + """ + + __tablename__ = 'roles' + + uuid = uuid_column() + name = Column(String(25), nullable=False, unique=True) + + _permissions = relationship( + Permission, backref='role', + cascade='save-update, merge, delete, delete-orphan') + permissions = association_proxy('_permissions', 'permission', + creator=lambda x: Permission(permission=x), + getset_factory=getset_factory) + + _users = relationship(UserRole, backref='role') + users = association_proxy('_users', 'user', + creator=lambda x: UserRole(user=x), + getset_factory=getset_factory) + + def __repr__(self): + return "" % self.name + + def __str__(self): + return str(self.name or '') + + class User(Base): """ Represents a user of the system. This may or may not correspond to a real @@ -79,12 +142,11 @@ class User(Base): salt = Column(String(29)) person_uuid = Column(String(32), ForeignKey('people.uuid')) - person = relationship(Person, backref='user') - # display_name = association_proxy('person', 'display_name') - - # roles = association_proxy('_roles', 'role', - # creator=lambda x: UserRole(role=x), - # getset_factory=getset_factory) + _roles = relationship(UserRole, backref='user') + roles = association_proxy( + '_roles', 'role', + creator=lambda x: UserRole(role=x), + getset_factory=getset_factory) def __repr__(self): return "" % self.username @@ -101,3 +163,14 @@ class User(Base): if self.person and self.person.display_name: return self.person.display_name return self.username + + +Person.user = relationship( + User, + back_populates='person', + uselist=False) + +User.person = relationship( + Person, + back_populates='user', + uselist=False) diff --git a/edbob/db/model.py b/edbob/db/model.py index 2e3e6de..a077ba0 100644 --- a/edbob/db/model.py +++ b/edbob/db/model.py @@ -29,21 +29,12 @@ from sqlalchemy import Column, String, Text import edbob -# from edbob import Object, get_uuid from edbob.db import Base __all__ = ['ActiveExtension', 'Setting'] -# class ClassWithUuid(Object): -# """ -# Simple mixin class which defines a ``uuid`` column as primary key. -# """ - -# Column('uuid', String(32), primary_key=True, default=get_uuid) - - def uuid_column(*args): """ Convenience function which returns a ``uuid`` column for use as a table's @@ -81,77 +72,3 @@ class Setting(Base): def __repr__(self): return "" % self.name - - -# def get_metadata(*args, **kwargs): -# """ -# Returns the core ``edbob`` schema definition. - -# Note that when :func:`edbob.init()` is called, the ``sqlalchemy.MetaData`` -# instance which is returned from this function will henceforth be available -# as ``edbob.metadata``. However, ``edbob.init()`` may extend -# ``edbob.metadata`` as well, depending on which extensions are activated -# within the primary database. - -# This function then serves two purposes: First, it provides the core -# metadata instance. Secondly, it allows edbob to always know what its core -# schema looks like, as opposed to what's held in the current -# ``edbob.metadata`` instance, which may have been extended locally. (The -# latter use is necessary in order for edbob to properly manage its -# extensions.) - -# All arguments (positional and keyword) are passed directly to the -# ``sqlalchemy.MetaData()`` constructor. -# """ - -# metadata = MetaData(*args, **kwargs) - -# active_extensions = Table( -# 'active_extensions', metadata, -# Column('name', String(50), primary_key=True), -# ) - -# def get_person_display_name(context): -# first_name = context.current_parameters['first_name'] -# last_name = context.current_parameters['last_name'] -# if not (first_name or last_name): -# return None -# return '%(first_name)s %(last_name)s' % locals() - -# people = table_with_uuid( -# 'people', metadata, -# Column('first_name', String(50)), -# Column('last_name', String(50)), -# Column('display_name', String(100), default=get_person_display_name), -# ) - -# permissions = Table( -# 'permissions', metadata, -# Column('role_uuid', String(32), ForeignKey('roles.uuid'), primary_key=True), -# Column('permission', String(50), primary_key=True), -# ) - -# roles = table_with_uuid( -# 'roles', metadata, -# Column('name', String(25), nullable=False, unique=True), -# ) - -# settings = Table( -# 'settings', metadata, -# Column('name', String(255), primary_key=True), -# Column('value', Text), -# ) - -# users = table_with_uuid( -# 'users', metadata, -# Column('username', String(25), nullable=False, unique=True), -# Column('person_uuid', String(32), ForeignKey('people.uuid')), -# ) - -# users_roles = table_with_uuid( -# 'users_roles', metadata, -# Column('user_uuid', String(32), ForeignKey('users.uuid')), -# Column('role_uuid', String(32), ForeignKey('roles.uuid')), -# ) - -# return metadata diff --git a/edbob/pyramid/__init__.py b/edbob/pyramid/__init__.py index 02a62d2..ba60dd0 100644 --- a/edbob/pyramid/__init__.py +++ b/edbob/pyramid/__init__.py @@ -35,10 +35,29 @@ import edbob.db __all__ = ['Session'] Session = scoped_session(edbob.db.Session) -Session.configure(extension=ZopeTransactionExtension()) def includeme(config): + """ + Adds ``edbob``-specific features to the application. Currently this does + two things: + + It adds a ``ZopeTransactionExtension`` instance as an extension to the + SQLAlchemy scoped ``Session`` class. This is necessary for most view code + that ships with ``edbob``, so you will most likely need to specify + ``config.include('edbob.pyramid')`` somewhere in your app config (i.e. your + ``main()`` function). + + The other thing added is the ``edbob`` static view for CSS files etc. + """ + + # Session is extended here instead of at module scope to prevent import + # side-effects. + Session.configure(extension=ZopeTransactionExtension()) + + # Forbidden view is configured here instead of within edbob.pyramid.views + # since it's so "important." + config.add_forbidden_view('edbob.pyramid.views.forbidden') + + # Same goes with the edbob static route; we need that JS. config.include('edbob.pyramid.static') - config.include('edbob.pyramid.subscribers') - # config.include('edbob.pyramid.views') diff --git a/edbob/pyramid/auth.py b/edbob/pyramid/auth.py new file mode 100644 index 0000000..803f45c --- /dev/null +++ b/edbob/pyramid/auth.py @@ -0,0 +1,57 @@ +#!/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.auth`` -- Authentication & Authorization +""" + +from zope.interface import implementer + +from pyramid.interfaces import IAuthorizationPolicy +from pyramid.security import Everyone, Authenticated + +import edbob +from edbob.db.auth import has_permission +from edbob.pyramid import Session + + +# def groupfinder(userid, request): +# q = Session.query(edbob.UserRole) +# q = q.filter(edbob.UserRole.user_uuid == userid) +# return [x.role_uuid for x in q] + + +@implementer(IAuthorizationPolicy) +class EdbobAuthorizationPolicy(object): + + def permits(self, context, principals, permission): + for userid in principals: + if userid not in (Everyone, Authenticated): + user = Session.query(edbob.User).get(userid) + assert user + return has_permission(user, permission) + return False + + def principals_allowed_by_permission(self, context, permission): + raise NotImplementedError diff --git a/edbob/pyramid/scaffolds/edbob/+package+/commands.py_tmpl b/edbob/pyramid/scaffolds/edbob/+package+/commands.py_tmpl index 7ff9d50..0f297c7 100644 --- a/edbob/pyramid/scaffolds/edbob/+package+/commands.py_tmpl +++ b/edbob/pyramid/scaffolds/edbob/+package+/commands.py_tmpl @@ -57,7 +57,6 @@ class InitCommand(commands.Subcommand): # activate_extension('shrubbery') # Okay, on to bootstrapping... - session = Session() # This creates an 'admin' user with 'admin' password. diff --git a/edbob/pyramid/static/css/edbob.css b/edbob/pyramid/static/css/edbob.css index 2e98bf3..d10b9e0 100644 --- a/edbob/pyramid/static/css/edbob.css +++ b/edbob/pyramid/static/css/edbob.css @@ -385,6 +385,7 @@ div.field-couple div.field { } div.field-couple div.field input[type=text], +div.field-couple div.field input[type=password], div.field-couple div.field select { width: 320px; } @@ -393,11 +394,12 @@ div.checkbox { margin: 15px 0px; } -table.fieldset tr { +table.fieldset tbody tr { vertical-align: top; } -table.fieldset td { +table.fieldset tbody td { + height: 30px; padding: 2px; } @@ -406,6 +408,16 @@ table.fieldset td.label { width: 120px; } +table.fieldset tbody td ul { + padding-left: 15px; +} + +table.fieldset tbody td ul li { + line-height: 1em; + margin-bottom: 4px; +} + + /****************************** * Sub-Grids ******************************/ diff --git a/edbob/pyramid/static/css/perms.css b/edbob/pyramid/static/css/perms.css new file mode 100644 index 0000000..764f8eb --- /dev/null +++ b/edbob/pyramid/static/css/perms.css @@ -0,0 +1,17 @@ + +/****************************** + * perms.css + ******************************/ + +div.field-couple.permissions div.field p.group { + font-weight: bold; +} + +div.field-couple.permissions div.field label { + float: none; + font-weight: normal; +} + +div.field-couple.permissions div.field label input { + margin-right: 10px; +} diff --git a/edbob/pyramid/subscribers.py b/edbob/pyramid/subscribers.py index 0bf8527..ae492fd 100644 --- a/edbob/pyramid/subscribers.py +++ b/edbob/pyramid/subscribers.py @@ -69,9 +69,9 @@ def context_found(event): return has_perm request = event.request - request.user = None request.has_perm = has_perm_func(request) + request.user = None uuid = authenticated_userid(request) if uuid: request.user = Session.query(edbob.User).get(uuid) diff --git a/edbob/pyramid/templates/edbob/login.mako b/edbob/pyramid/templates/edbob/login.mako index aeabf37..cd5753d 100644 --- a/edbob/pyramid/templates/edbob/login.mako +++ b/edbob/pyramid/templates/edbob/login.mako @@ -6,7 +6,7 @@ ${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/login.css'))} -${h.image(logo_url, "${self.global_title()} logo", id='login-logo')} +${h.image(logo_url, "${self.global_title()} logo", id='login-logo', **logo_kwargs)}
${h.form('')} diff --git a/edbob/pyramid/templates/people/base.mako b/edbob/pyramid/templates/people/base.mako new file mode 100644 index 0000000..27f7dd9 --- /dev/null +++ b/edbob/pyramid/templates/people/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/edbob/pyramid/templates/people/index.mako b/edbob/pyramid/templates/people/index.mako new file mode 100644 index 0000000..f02a31f --- /dev/null +++ b/edbob/pyramid/templates/people/index.mako @@ -0,0 +1,12 @@ +<%inherit file="/people/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">People + +<%def name="menu()"> + % if request.has_perm('people.create'): +

${h.link_to("Create a new Person", url('person.new'))}

+ % endif + + +${parent.body()} diff --git a/edbob/pyramid/templates/people/person.mako b/edbob/pyramid/templates/people/person.mako new file mode 100644 index 0000000..c9f0cf5 --- /dev/null +++ b/edbob/pyramid/templates/people/person.mako @@ -0,0 +1,29 @@ +<%inherit file="/people/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="crud_name()">Person + +<%def name="menu()"> +

${h.link_to("Back to People", url('people.list'))}

+ + +${parent.body()} + +% if fieldset.edit: +

User Info

+ % if user: + ${user.render()|n} +
+ +
+ % else: +

This person does not have a user account.

+ ${h.form(url('user.new'))} + ${h.hidden('User--person_uuid', value=fieldset.model.uuid)} + ${h.hidden('User--username')} +
+ ${h.submit('submit', "Create User")} +
+ ${h.end_form()} + % endif +% endif diff --git a/edbob/pyramid/templates/roles/base.mako b/edbob/pyramid/templates/roles/base.mako new file mode 100644 index 0000000..27f7dd9 --- /dev/null +++ b/edbob/pyramid/templates/roles/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/edbob/pyramid/templates/roles/index.mako b/edbob/pyramid/templates/roles/index.mako new file mode 100644 index 0000000..597c8f7 --- /dev/null +++ b/edbob/pyramid/templates/roles/index.mako @@ -0,0 +1,10 @@ +<%inherit file="/roles/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Roles + +<%def name="menu()"> +

${h.link_to("Create a new Role", url('role.new'))}

+ + +${parent.body()} diff --git a/edbob/pyramid/templates/roles/role.mako b/edbob/pyramid/templates/roles/role.mako new file mode 100644 index 0000000..a532b06 --- /dev/null +++ b/edbob/pyramid/templates/roles/role.mako @@ -0,0 +1,15 @@ +<%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/templates/users/base.mako b/edbob/pyramid/templates/users/base.mako new file mode 100644 index 0000000..27f7dd9 --- /dev/null +++ b/edbob/pyramid/templates/users/base.mako @@ -0,0 +1,2 @@ +<%inherit file="/base.mako" /> +${parent.body()} diff --git a/edbob/pyramid/templates/users/index.mako b/edbob/pyramid/templates/users/index.mako new file mode 100644 index 0000000..6494aaf --- /dev/null +++ b/edbob/pyramid/templates/users/index.mako @@ -0,0 +1,10 @@ +<%inherit file="/users/base.mako" /> +<%inherit file="/index.mako" /> + +<%def name="title()">Users + +<%def name="menu()"> +

${h.link_to("Create a new User", url('user.new'))}

+ + +${parent.body()} diff --git a/edbob/pyramid/templates/users/user.mako b/edbob/pyramid/templates/users/user.mako new file mode 100644 index 0000000..73e93e8 --- /dev/null +++ b/edbob/pyramid/templates/users/user.mako @@ -0,0 +1,10 @@ +<%inherit file="/users/base.mako" /> +<%inherit file="/crud.mako" /> + +<%def name="crud_name()">User + +<%def name="menu()"> +

${h.link_to("Back to Users", url('users.list'))}

+ + +${parent.body()} diff --git a/edbob/pyramid/views/__init__.py b/edbob/pyramid/views/__init__.py index e5972a2..236a454 100644 --- a/edbob/pyramid/views/__init__.py +++ b/edbob/pyramid/views/__init__.py @@ -26,6 +26,32 @@ ``edbob.pyramid.views`` -- Views """ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import authenticated_userid -# def includeme(config): -# config.include('edbob.pyramid.views.auth') +from webhelpers.html import literal +from webhelpers.html.tags import link_to + + +def forbidden(request): + """ + The forbidden view. This is triggered whenever access rights are denied + for an otherwise-appropriate view. + """ + + msg = literal("You do not have permission to do that.") + if not authenticated_userid(request): + msg += literal("  (Perhaps you should %s?)" % + link_to("log in", request.route_url('login'))) + request.session.flash(msg) + + url = request.referer + if not url or url == request.current_route_url(): + url = request.route_url('home') + return HTTPFound(location=url) + + +def includeme(config): + config.include('edbob.pyramid.views.auth') + config.include('edbob.pyramid.views.people') + config.include('edbob.pyramid.views.users') diff --git a/edbob/pyramid/views/auth.py b/edbob/pyramid/views/auth.py index f0755c1..11636e5 100644 --- a/edbob/pyramid/views/auth.py +++ b/edbob/pyramid/views/auth.py @@ -78,17 +78,29 @@ def login(context, request): url = edbob.config.get('edbob.pyramid', 'login.logo_url', default=request.static_url('edbob.pyramid:static/img/logo.jpg')) + kwargs = eval(edbob.config.get('edbob.pyramid', 'login.logo_kwargs', + default="dict(width=500)")) - return {'form': FormRenderer(form), 'referer': referer, 'logo_url': url} + return {'form': FormRenderer(form), 'referer': referer, + 'logo_url': url, 'logo_kwargs': kwargs} def logout(context, request): + """ + View responsible for logging out the current user. + + This deletes/invalidates the current session and then redirects to the + login page. + """ + request.session.delete() + request.session.invalidate() headers = forget(request) return HTTPFound(location=request.route_url('login'), headers=headers) def includeme(config): + config.add_route('login', '/login') config.add_view(login, route_name='login', renderer='/login.mako') diff --git a/edbob/pyramid/views/people.py b/edbob/pyramid/views/people.py new file mode 100644 index 0000000..a4c501e --- /dev/null +++ b/edbob/pyramid/views/people.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.people`` -- Person Views +""" + +import transaction +from pyramid.httpexceptions import HTTPFound + +from formalchemy import Field + +import edbob +from edbob.pyramid import filters +from edbob.pyramid import forms +from edbob.pyramid import grids +from edbob.pyramid import Session + + +def filter_map(): + return filters.get_filter_map( + edbob.Person, + ilike=['first_name', 'last_name', 'display_name']) + +def search_config(request, fmap): + return filters.get_search_config( + 'people.list', request, fmap, + include_filter_display_name=True, + filter_type_display_name='lk') + +def search_form(config): + return filters.get_search_form(config) + +def grid_config(request, search, fmap): + return grids.get_grid_config( + 'people.list', request, search, + filter_map=fmap, sort='display_name') + +def sort_map(): + return grids.get_sort_map( + edbob.Person, + ['first_name', 'last_name', 'display_name']) + +def query(config): + smap = sort_map() + q = Session.query(edbob.Person) + q = filters.filter_query(q, config) + q = grids.sort_query(q, config, smap) + return q + + +def people(context, request): + + fmap = filter_map() + config = search_config(request, fmap) + search = search_form(config) + config = grid_config(request, search, fmap) + people = grids.get_pager(query, config) + + g = forms.AlchemyGrid( + edbob.Person, people, config, + gridurl=request.route_url('people.list'), + objurl='person.edit') + + g.configure( + include=[ + g.first_name, + g.last_name, + g.display_name, + ], + readonly=True) + + grid = g.render(class_='clickable people') + return grids.render_grid(request, grid, search) + + +def person_fieldset(person, request): + fs = forms.make_fieldset(person, url=request.route_url, + url_action=request.current_route_url(), + route_name='people.list') + fs.configure( + include=[ + fs.first_name, + fs.last_name, + fs.display_name, + ]) + return fs + + +def new_person(context, request): + + fs = person_fieldset(edbob.Person, request) + if not fs.readonly and request.POST: + fs.rebind(data=request.params) + if fs.validate(): + + with transaction.manager: + fs.sync() + Session.add(fs.model) + Session.flush() + request.session.flash("%s \"%s\" has been %s." % ( + fs.crud_title, fs.get_display_text(), + 'updated' if fs.edit else 'created')) + + return HTTPFound(location=request.route_url('people.list')) + + return {'fieldset': fs, 'crud': True} + + +def edit_person(request): + """ + View for editing a :class:`edbob.Person` instance. + """ + + from edbob.pyramid.views.users import user_fieldset + + uuid = request.matchdict['uuid'] + person = Session.query(edbob.Person).get(uuid) if uuid else None + assert person + + fs = person_fieldset(person, request) + if request.POST: + fs.rebind(data=request.params) + if fs.validate(): + + 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('people.list') + + return HTTPFound(location=home) + + user = fs.model.user + if user: + user = user_fieldset(user, request) + user.readonly = True + del user.person + del user.password + del user.confirm_password + + return {'fieldset': fs, 'crud': True, 'user': user} + + +def includeme(config): + + config.add_route('people.list', '/people') + config.add_view(people, route_name='people.list', renderer='/people/index.mako', + permission='people.list', http_cache=0) + + config.add_route('person.new', '/people/new') + config.add_view(new_person, route_name='person.new', renderer='/people/person.mako', + permission='people.create', http_cache=0) + + config.add_route('person.edit', '/people/{uuid}/edit') + config.add_view(edit_person, route_name='person.edit', renderer='/people/person.mako', + permission='people.edit', http_cache=0) diff --git a/edbob/pyramid/views/roles.py b/edbob/pyramid/views/roles.py new file mode 100644 index 0000000..54c86ca --- /dev/null +++ b/edbob/pyramid/views/roles.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.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 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.pyramid import Session + + +def filter_map(): + return filters.get_filter_map( + edbob.Role, + ilike=['name']) + +def search_config(request, fmap): + return filters.get_search_config( + 'roles.list', request, fmap, + include_filter_name=True, + filter_type_name='lk') + +def search_form(config): + return filters.get_search_form(config) + +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 + + +def roles(request): + + fmap = filter_map() + config = search_config(request, fmap) + search = search_form(config) + config = grid_config(request, search, fmap) + roles = grids.get_pager(query, config) + + g = forms.AlchemyGrid( + edbob.Role, roles, config, + gridurl=request.route_url('roles.list'), + objurl='role.edit') + + g.configure( + include=[ + g.name, + ], + readonly=True) + + grid = g.render(class_='clickable roles') + return grids.render_grid(request, grid, search) + + +class PermissionsField(Field): + + def sync(self): + if not self.is_readonly(): + role = self.model + role.permissions = self.renderer.deserialize() + + +class PermissionsFieldRenderer(FieldRenderer): + + 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') + + fs.append(PermissionsField('permissions', + renderer=PermissionsFieldRenderer)) + + fs.configure( + include=[ + fs.name, + fs.permissions, + ]) + + if not fs.edit: + del fs.permissions + + return fs + + +def new_role(request): + + fs = role_fieldset(edbob.Role, request) + if request.POST: + fs.rebind(data=request.params) + if fs.validate(): + + 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') + + 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 includeme(config): + + config.add_route('roles.list', '/roles') + config.add_view(roles, route_name='roles.list', renderer='/roles/index.mako', + permission='roles.list', http_cache=0) + + 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.edit', '/roles/{uuid}/edit') + config.add_view(edit_role, route_name='role.edit', renderer='/roles/role.mako', + permission='roles.edit', http_cache=0) diff --git a/edbob/pyramid/views/users.py b/edbob/pyramid/views/users.py new file mode 100644 index 0000000..ff647e8 --- /dev/null +++ b/edbob/pyramid/views/users.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +################################################################################ +# +# edbob -- Pythonic Software Framework +# Copyright © 2010-2012 Lance Edgar +# +# This file is part of edbob. +# +# edbob is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# edbob is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with edbob. If not, see . +# +################################################################################ + +""" +``edbob.pyramid.views.users`` -- User Views +""" + +import transaction +from pyramid.httpexceptions import HTTPFound + +import formalchemy +from formalchemy.fields import SelectFieldRenderer +from webhelpers.html import literal +from webhelpers.html.tags import hidden, link_to, password + +import edbob +from edbob.db.auth import set_user_password +from edbob.pyramid import filters +from edbob.pyramid import forms +from edbob.pyramid import grids +from edbob.pyramid import Session + + +def filter_map(): + return filters.get_filter_map( + edbob.User, + ilike=['username'], + person=filters.filter_ilike(edbob.Person.display_name)) + +def search_config(request, fmap): + return filters.get_search_config( + 'users.list', request, fmap, + include_filter_username=True, + filter_type_username='lk') + +def search_form(config): + return filters.get_search_form(config) + +def grid_config(request, search, fmap): + return grids.get_grid_config( + 'users.list', request, search, + filter_map=fmap, sort='username') + +def sort_map(): + return grids.get_sort_map( + edbob.User, ['username'], + person=grids.sorter(edbob.Person.display_name)) + +def query(config): + jmap = {'person': lambda q: q.outerjoin(edbob.Person)} + smap = sort_map() + q = Session.query(edbob.User) + q = filters.filter_query(q, config, jmap) + q = grids.sort_query(q, config, smap, jmap) + return q + + +def users(context, request): + + fmap = filter_map() + config = search_config(request, fmap) + search = search_form(config) + config = grid_config(request, search, fmap) + users = grids.get_pager(query, config) + + g = forms.AlchemyGrid( + edbob.User, users, config, + gridurl=request.route_url('users.list'), + objurl='user.edit') + + g.configure( + include=[ + g.username, + g.person, + ], + readonly=True) + + grid = g.render(class_='clickable users') + return grids.render_grid(request, grid, search) + + +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
  • ' % ( + link_to(role.name, + self.request.route_url('role.edit', uuid=role.uuid)))) + res += literal('
') + return res + + +def RolesFieldRenderer(request): + return type('RolesFieldRenderer', (_RolesFieldRenderer,), {'request': request}) + + +class RolesField(formalchemy.Field): + + def __init__(self, name, **kwargs): + kwargs.setdefault('value', self.get_value) + kwargs.setdefault('options', self.get_options()) + kwargs.setdefault('multiple', True) + super(RolesField, self).__init__(name, **kwargs) + + def get_value(self, user): + return [x.uuid for x in user.roles] + + def get_options(self): + q = Session.query(edbob.Role.name, edbob.Role.uuid) + q = q.order_by(edbob.Role.name) + return q.all() + + def sync(self): + if not self.is_readonly(): + user = self.model + roles = Session.query(edbob.Role) + data = self.renderer.deserialize() + user.roles = [roles.get(x) for x in data] + + +class _ProtectedPersonRenderer(formalchemy.FieldRenderer): + + def render_readonly(self, **kwargs): + res = str(self.person) + res += hidden('User--person_uuid', + value=self.field.parent.person_uuid.value) + return res + + +def ProtectedPersonRenderer(uuid): + person = Session.query(edbob.Person).get(uuid) + assert person + return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,), + {'person': person}) + + +class _LinkedPersonRenderer(formalchemy.FieldRenderer): + + def render_readonly(self, **kwargs): + return link_to(str(self.raw_value), + self.request.route_url('person.edit', uuid=self.value)) + + +def LinkedPersonRenderer(request): + return type('LinkedPersonRenderer', (_LinkedPersonRenderer,), {'request': request}) + + +class PasswordFieldRenderer(formalchemy.PasswordFieldRenderer): + + def render(self, **kwargs): + return password(self.name, value='', maxlength=self.length, **kwargs) + + +def passwords_match(value, field): + if field.parent.confirm_password.value != value: + raise formalchemy.ValidationError("Passwords do not match") + return value + + +class PasswordField(formalchemy.Field): + + def __init__(self, *args, **kwargs): + kwargs.setdefault('value', lambda x: x.password) + kwargs.setdefault('renderer', PasswordFieldRenderer) + kwargs.setdefault('validate', passwords_match) + super(PasswordField, self).__init__(*args, **kwargs) + + def sync(self): + if not self.is_readonly(): + password = self.renderer.deserialize() + if password: + set_user_password(self.model, password) + + +def user_fieldset(user, request): + fs = forms.make_fieldset(user, url=request.route_url, + url_action=request.current_route_url(), + route_name='users.list') + + fs.append(PasswordField('password')) + fs.append(formalchemy.Field('confirm_password', + renderer=PasswordFieldRenderer)) + + fs.append(RolesField( + 'roles', renderer=RolesFieldRenderer(request))) + + fs.configure( + include=[ + fs.person, + fs.username, + fs.password.label("Set Password"), + fs.confirm_password, + fs.roles, + ]) + + if isinstance(user, edbob.User) and user.person: + fs.person.set(readonly=True, + renderer=LinkedPersonRenderer(request)) + + return fs + + +def new_user(request): + """ + View for creating a new :class:`edbob.User` instance. + """ + + fs = user_fieldset(edbob.User, 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('users.list') + + return HTTPFound(location=home) + + if fs.person_uuid.value: + fs.person.set(readonly=True, + renderer=ProtectedPersonRenderer(fs.person_uuid.value)) + + return {'fieldset': fs, 'crud': True} + + +def edit_user(request): + uuid = request.matchdict['uuid'] + user = Session.query(edbob.User).get(uuid) if uuid else None + assert user + + fs = user_fieldset(user, 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('users.list') + + return HTTPFound(location=home) + + return {'fieldset': fs, 'crud': True} + + +def includeme(config): + + config.add_route('users.list', '/users') + config.add_view(users, route_name='users.list', renderer='/users/index.mako', + permission='users.list', http_cache=0) + + config.add_route('user.new', '/users/new') + config.add_view(new_user, route_name='user.new', renderer='/users/user.mako', + permission='users.create', http_cache=0) + + config.add_route('user.edit', '/users/{uuid}/edit') + config.add_view(edit_user, route_name='user.edit', renderer='/users/user.mako', + permission='users.edit', http_cache=0)