feat: expose Role permissions for editing
This commit is contained in:
parent
97e914c2e0
commit
230e2fd1ab
|
@ -135,6 +135,12 @@ def make_pyramid_config(settings):
|
||||||
pyramid_config.include('pyramid_mako')
|
pyramid_config.include('pyramid_mako')
|
||||||
pyramid_config.include('pyramid_tm')
|
pyramid_config.include('pyramid_tm')
|
||||||
|
|
||||||
|
# add some permissions magic
|
||||||
|
pyramid_config.add_directive('add_wutta_permission_group',
|
||||||
|
'wuttaweb.auth.add_permission_group')
|
||||||
|
pyramid_config.add_directive('add_wutta_permission',
|
||||||
|
'wuttaweb.auth.add_permission')
|
||||||
|
|
||||||
return pyramid_config
|
return pyramid_config
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -148,3 +148,93 @@ class WuttaSecurityPolicy:
|
||||||
auth = app.get_auth_handler()
|
auth = app.get_auth_handler()
|
||||||
user = self.identity(request)
|
user = self.identity(request)
|
||||||
return auth.has_permission(self.db_session, user, permission)
|
return auth.has_permission(self.db_session, user, permission)
|
||||||
|
|
||||||
|
|
||||||
|
def add_permission_group(pyramid_config, key, label=None, overwrite=True):
|
||||||
|
"""
|
||||||
|
Pyramid directive to add a "permission group" to the app's
|
||||||
|
awareness.
|
||||||
|
|
||||||
|
The app must be made aware of all permissions, so they are exposed
|
||||||
|
when editing a
|
||||||
|
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
|
||||||
|
for discovering permissions is in
|
||||||
|
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
|
||||||
|
|
||||||
|
This is usually called from within a master view's
|
||||||
|
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
|
||||||
|
the permission group which applies to the view model.
|
||||||
|
|
||||||
|
A simple example of usage::
|
||||||
|
|
||||||
|
pyramid_config.add_permission_group('widgets', label="Widgets")
|
||||||
|
|
||||||
|
:param key: Unique key for the permission group. In the context
|
||||||
|
of a master view, this will be the same as
|
||||||
|
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
|
||||||
|
|
||||||
|
:param label: Optional label for the permission group. If not
|
||||||
|
specified, it is derived from ``key``.
|
||||||
|
|
||||||
|
:param overwrite: If the permission group was already established,
|
||||||
|
this flag controls whether the group's label should be
|
||||||
|
overwritten (with ``label``).
|
||||||
|
|
||||||
|
See also :func:`add_permission()`.
|
||||||
|
"""
|
||||||
|
config = pyramid_config.get_settings()['wutta_config']
|
||||||
|
app = config.get_app()
|
||||||
|
def action():
|
||||||
|
perms = pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
if overwrite or key not in perms:
|
||||||
|
group = perms.setdefault(key, {'key': key})
|
||||||
|
group['label'] = label or app.make_title(key)
|
||||||
|
pyramid_config.add_settings({'wutta_permissions': perms})
|
||||||
|
pyramid_config.action(None, action)
|
||||||
|
|
||||||
|
|
||||||
|
def add_permission(pyramid_config, groupkey, key, label=None):
|
||||||
|
"""
|
||||||
|
Pyramid directive to add a single "permission" to the app's
|
||||||
|
awareness.
|
||||||
|
|
||||||
|
The app must be made aware of all permissions, so they are exposed
|
||||||
|
when editing a
|
||||||
|
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
|
||||||
|
for discovering permissions is in
|
||||||
|
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
|
||||||
|
|
||||||
|
This is usually called from within a master view's
|
||||||
|
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
|
||||||
|
"known" permissions based on master view feature flags
|
||||||
|
(:attr:`~wuttaweb.views.master.MasterView.viewable`,
|
||||||
|
:attr:`~wuttaweb.views.master.MasterView.editable`, etc.).
|
||||||
|
|
||||||
|
A simple example of usage::
|
||||||
|
|
||||||
|
pyramid_config.add_permission('widgets', 'widgets.polish',
|
||||||
|
label="Polish all the widgets")
|
||||||
|
|
||||||
|
:param key: Unique key for the permission group. In the context
|
||||||
|
of a master view, this will be the same as
|
||||||
|
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
|
||||||
|
|
||||||
|
:param key: Unique key for the permission. This should be the
|
||||||
|
"complete" permission name which includes the permission
|
||||||
|
prefix.
|
||||||
|
|
||||||
|
:param label: Optional label for the permission. If not
|
||||||
|
specified, it is derived from ``key``.
|
||||||
|
|
||||||
|
See also :func:`add_permission_group()`.
|
||||||
|
"""
|
||||||
|
config = pyramid_config.get_settings()['wutta_config']
|
||||||
|
app = config.get_app()
|
||||||
|
def action():
|
||||||
|
perms = pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
group = perms.setdefault(groupkey, {'key': groupkey})
|
||||||
|
group.setdefault('label', app.make_title(groupkey))
|
||||||
|
perm = group.setdefault('perms', {}).setdefault(key, {'key': key})
|
||||||
|
perm['label'] = label or app.make_title(key)
|
||||||
|
pyramid_config.add_settings({'wutta_permissions': perms})
|
||||||
|
pyramid_config.action(None, action)
|
||||||
|
|
|
@ -259,20 +259,38 @@ class PersonRef(ObjectRef):
|
||||||
return query.order_by(self.model_class.full_name)
|
return query.order_by(self.model_class.full_name)
|
||||||
|
|
||||||
|
|
||||||
class RoleRefs(colander.Set):
|
class WuttaSet(colander.Set):
|
||||||
"""
|
"""
|
||||||
Form schema type for the User
|
Custom schema type for :class:`python:set` fields.
|
||||||
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles`
|
|
||||||
association proxy field.
|
This is a subclass of :class:`colander.Set`, but adds
|
||||||
|
Wutta-related params to the constructor.
|
||||||
|
|
||||||
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
|
:param session: Optional :term:`db session` to use instead of
|
||||||
|
:class:`wuttaweb.db.Session`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, request, session=None, *args, **kwargs):
|
def __init__(self, request, session=None):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__()
|
||||||
self.request = request
|
self.request = request
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
self.session = session or Session()
|
self.session = session or Session()
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRefs(WuttaSet):
|
||||||
|
"""
|
||||||
|
Form schema type for the User
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles`
|
||||||
|
association proxy field.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
|
||||||
|
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role` ``uuid``
|
||||||
|
values for underlying data format.
|
||||||
|
"""
|
||||||
|
|
||||||
def widget_maker(self, **kwargs):
|
def widget_maker(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Constructs a default widget for the field.
|
Constructs a default widget for the field.
|
||||||
|
@ -298,3 +316,42 @@ class RoleRefs(colander.Set):
|
||||||
kwargs['values'] = values
|
kwargs['values'] = values
|
||||||
|
|
||||||
return widgets.RoleRefsWidget(self.request, **kwargs)
|
return widgets.RoleRefsWidget(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Permissions(WuttaSet):
|
||||||
|
"""
|
||||||
|
Form schema type for the Role
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
|
||||||
|
association proxy field.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaSet`. It uses a ``set`` of
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Permission.permission`
|
||||||
|
values for underlying data format.
|
||||||
|
|
||||||
|
:param permissions: Dict with all possible permissions. Should be
|
||||||
|
in the same format as returned by
|
||||||
|
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, request, permissions, *args, **kwargs):
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
self.permissions = permissions
|
||||||
|
|
||||||
|
def widget_maker(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Constructs a default widget for the field.
|
||||||
|
|
||||||
|
:returns: Instance of
|
||||||
|
:class:`~wuttaweb.forms.widgets.PermissionsWidget`.
|
||||||
|
"""
|
||||||
|
kwargs.setdefault('session', self.session)
|
||||||
|
kwargs.setdefault('permissions', self.permissions)
|
||||||
|
|
||||||
|
if 'values' not in kwargs:
|
||||||
|
values = []
|
||||||
|
for gkey, group in self.permissions.items():
|
||||||
|
for pkey, perm in group['perms'].items():
|
||||||
|
values.append((pkey, perm['label']))
|
||||||
|
kwargs['values'] = values
|
||||||
|
|
||||||
|
return widgets.PermissionsWidget(self.request, **kwargs)
|
||||||
|
|
|
@ -103,14 +103,20 @@ class NotesWidget(TextAreaWidget):
|
||||||
readonly_template = 'readonly/notes'
|
readonly_template = 'readonly/notes'
|
||||||
|
|
||||||
|
|
||||||
class RoleRefsWidget(CheckboxChoiceWidget):
|
class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
||||||
"""
|
"""
|
||||||
Widget for use with User
|
Custom widget for :class:`python:set` fields.
|
||||||
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
|
|
||||||
|
|
||||||
This is a subclass of
|
This is a subclass of
|
||||||
:class:`deform:deform.widget.CheckboxChoiceWidget` and uses these
|
:class:`deform:deform.widget.CheckboxChoiceWidget`, but adds
|
||||||
Deform templates:
|
Wutta-related params to the constructor.
|
||||||
|
|
||||||
|
:param request: Current :term:`request` object.
|
||||||
|
|
||||||
|
:param session: Optional :term:`db session` to use instead of
|
||||||
|
:class:`wuttaweb.db.Session`.
|
||||||
|
|
||||||
|
It uses these Deform templates:
|
||||||
|
|
||||||
* ``checkbox_choice``
|
* ``checkbox_choice``
|
||||||
* ``readonly/checkbox_choice``
|
* ``readonly/checkbox_choice``
|
||||||
|
@ -123,6 +129,15 @@ class RoleRefsWidget(CheckboxChoiceWidget):
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
self.session = session or Session()
|
self.session = session or Session()
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRefsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
"""
|
||||||
|
Widget for use with User
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaCheckboxChoiceWidget`.
|
||||||
|
"""
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
def serialize(self, field, cstruct, **kw):
|
||||||
""" """
|
""" """
|
||||||
# special logic when field is editable
|
# special logic when field is editable
|
||||||
|
@ -143,3 +158,32 @@ class RoleRefsWidget(CheckboxChoiceWidget):
|
||||||
|
|
||||||
# default logic from here
|
# default logic from here
|
||||||
return super().serialize(field, cstruct, **kw)
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsWidget(WuttaCheckboxChoiceWidget):
|
||||||
|
"""
|
||||||
|
Widget for use with Role
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.Role.permissions`
|
||||||
|
field.
|
||||||
|
|
||||||
|
This is a subclass of :class:`WuttaCheckboxChoiceWidget`. It uses
|
||||||
|
these Deform templates:
|
||||||
|
|
||||||
|
* ``permissions``
|
||||||
|
* ``readonly/permissions``
|
||||||
|
"""
|
||||||
|
template = 'permissions'
|
||||||
|
readonly_template = 'readonly/permissions'
|
||||||
|
|
||||||
|
def serialize(self, field, cstruct, **kw):
|
||||||
|
""" """
|
||||||
|
kw.setdefault('permissions', self.permissions)
|
||||||
|
|
||||||
|
if 'values' not in kw:
|
||||||
|
values = []
|
||||||
|
for gkey, group in self.permissions.items():
|
||||||
|
for pkey, perm in group['perms'].items():
|
||||||
|
values.append((pkey, perm['label']))
|
||||||
|
kw['values'] = values
|
||||||
|
|
||||||
|
return super().serialize(field, cstruct, **kw)
|
||||||
|
|
23
src/wuttaweb/templates/deform/permissions.pt
Normal file
23
src/wuttaweb/templates/deform/permissions.pt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<div>
|
||||||
|
${field.start_sequence()}
|
||||||
|
<tal:loop tal:repeat="groupkey sorted(permissions, key=lambda k: permissions[k]['label'].lower())">
|
||||||
|
<div class="card block"
|
||||||
|
tal:define="perms permissions[groupkey]['perms'];">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">${permissions[groupkey]['label']}</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<tal:loop tal:repeat="key sorted(perms, key=lambda p: perms[p]['label'].lower())">
|
||||||
|
<b-checkbox name="checkbox"
|
||||||
|
native-value="${key}"
|
||||||
|
tal:attributes=":value 'true' if key in cstruct else 'false';">
|
||||||
|
${perms[key]['label']}
|
||||||
|
</b-checkbox>
|
||||||
|
</tal:loop>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</tal:loop>
|
||||||
|
${field.end_sequence()}
|
||||||
|
</div>
|
18
src/wuttaweb/templates/deform/readonly/permissions.pt
Normal file
18
src/wuttaweb/templates/deform/readonly/permissions.pt
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<div>
|
||||||
|
<tal:loop tal:repeat="groupkey sorted(permissions, key=lambda k: permissions[k]['label'].lower())">
|
||||||
|
<div class="card block"
|
||||||
|
tal:condition="any([key in cstruct for key in permissions[groupkey]['perms']])"
|
||||||
|
tal:define="perms permissions[groupkey]['perms'];">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">${permissions[groupkey]['label']}</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
|
<tal:loop tal:repeat="key sorted(perms, key=lambda p: perms[p]['label'].lower())">
|
||||||
|
<span tal:condition="key in cstruct">${perms[key]['label']}</span>
|
||||||
|
</tal:loop>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</tal:loop>
|
||||||
|
</div>
|
|
@ -139,6 +139,14 @@ class MasterView(View):
|
||||||
Code should not access this directly but instead call
|
Code should not access this directly but instead call
|
||||||
:meth:`get_route_prefix()`.
|
:meth:`get_route_prefix()`.
|
||||||
|
|
||||||
|
.. attribute:: permission_prefix
|
||||||
|
|
||||||
|
Optional override for the view's permission prefix,
|
||||||
|
e.g. ``'wutta_widgets'``.
|
||||||
|
|
||||||
|
Code should not access this directly but instead call
|
||||||
|
:meth:`get_permission_prefix()`.
|
||||||
|
|
||||||
.. attribute:: url_prefix
|
.. attribute:: url_prefix
|
||||||
|
|
||||||
Optional override for the view's URL prefix,
|
Optional override for the view's URL prefix,
|
||||||
|
@ -1442,6 +1450,29 @@ class MasterView(View):
|
||||||
model_name = cls.get_model_name_normalized()
|
model_name = cls.get_model_name_normalized()
|
||||||
return f'{model_name}s'
|
return f'{model_name}s'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_permission_prefix(cls):
|
||||||
|
"""
|
||||||
|
Returns the "permission prefix" for the master view. This
|
||||||
|
prefix is used for all permissions defined by the view class.
|
||||||
|
|
||||||
|
For instance if permission prefix is ``'widgets'`` then a view
|
||||||
|
might have these permissions:
|
||||||
|
|
||||||
|
* ``'widgets.list'``
|
||||||
|
* ``'widgets.create'``
|
||||||
|
* ``'widgets.edit'``
|
||||||
|
* ``'widgets.delete'``
|
||||||
|
|
||||||
|
The default logic will call :meth:`get_route_prefix()` and use
|
||||||
|
that value as-is. A subclass may override by assigning
|
||||||
|
:attr:`permission_prefix`.
|
||||||
|
"""
|
||||||
|
if hasattr(cls, 'permission_prefix'):
|
||||||
|
return cls.permission_prefix
|
||||||
|
|
||||||
|
return cls.get_route_prefix()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_url_prefix(cls):
|
def get_url_prefix(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1581,13 +1612,24 @@ class MasterView(View):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _defaults(cls, config):
|
def _defaults(cls, config):
|
||||||
route_prefix = cls.get_route_prefix()
|
route_prefix = cls.get_route_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
url_prefix = cls.get_url_prefix()
|
url_prefix = cls.get_url_prefix()
|
||||||
|
model_title = cls.get_model_title()
|
||||||
|
model_title_plural = cls.get_model_title_plural()
|
||||||
|
|
||||||
|
# permission group
|
||||||
|
config.add_wutta_permission_group(permission_prefix,
|
||||||
|
model_title_plural,
|
||||||
|
overwrite=False)
|
||||||
|
|
||||||
# index
|
# index
|
||||||
if cls.listable:
|
if cls.listable:
|
||||||
config.add_route(route_prefix, f'{url_prefix}/')
|
config.add_route(route_prefix, f'{url_prefix}/')
|
||||||
config.add_view(cls, attr='index',
|
config.add_view(cls, attr='index',
|
||||||
route_name=route_prefix)
|
route_name=route_prefix)
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.list',
|
||||||
|
f"Browse / search {model_title_plural}")
|
||||||
|
|
||||||
# create
|
# create
|
||||||
if cls.creatable:
|
if cls.creatable:
|
||||||
|
@ -1595,6 +1637,9 @@ class MasterView(View):
|
||||||
f'{url_prefix}/new')
|
f'{url_prefix}/new')
|
||||||
config.add_view(cls, attr='create',
|
config.add_view(cls, attr='create',
|
||||||
route_name=f'{route_prefix}.create')
|
route_name=f'{route_prefix}.create')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.create',
|
||||||
|
f"Create new {model_title}")
|
||||||
|
|
||||||
# view
|
# view
|
||||||
if cls.viewable:
|
if cls.viewable:
|
||||||
|
@ -1602,6 +1647,9 @@ class MasterView(View):
|
||||||
config.add_route(f'{route_prefix}.view', instance_url_prefix)
|
config.add_route(f'{route_prefix}.view', instance_url_prefix)
|
||||||
config.add_view(cls, attr='view',
|
config.add_view(cls, attr='view',
|
||||||
route_name=f'{route_prefix}.view')
|
route_name=f'{route_prefix}.view')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.view',
|
||||||
|
f"View {model_title}")
|
||||||
|
|
||||||
# edit
|
# edit
|
||||||
if cls.editable:
|
if cls.editable:
|
||||||
|
@ -1610,6 +1658,9 @@ class MasterView(View):
|
||||||
f'{instance_url_prefix}/edit')
|
f'{instance_url_prefix}/edit')
|
||||||
config.add_view(cls, attr='edit',
|
config.add_view(cls, attr='edit',
|
||||||
route_name=f'{route_prefix}.edit')
|
route_name=f'{route_prefix}.edit')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.edit',
|
||||||
|
f"Edit {model_title}")
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
if cls.deletable:
|
if cls.deletable:
|
||||||
|
@ -1618,6 +1669,9 @@ class MasterView(View):
|
||||||
f'{instance_url_prefix}/delete')
|
f'{instance_url_prefix}/delete')
|
||||||
config.add_view(cls, attr='delete',
|
config.add_view(cls, attr='delete',
|
||||||
route_name=f'{route_prefix}.delete')
|
route_name=f'{route_prefix}.delete')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.delete',
|
||||||
|
f"Delete {model_title}")
|
||||||
|
|
||||||
# configure
|
# configure
|
||||||
if cls.configurable:
|
if cls.configurable:
|
||||||
|
@ -1625,3 +1679,6 @@ class MasterView(View):
|
||||||
f'{url_prefix}/configure')
|
f'{url_prefix}/configure')
|
||||||
config.add_view(cls, attr='configure',
|
config.add_view(cls, attr='configure',
|
||||||
route_name=f'{route_prefix}.configure')
|
route_name=f'{route_prefix}.configure')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.configure',
|
||||||
|
f"Configure {model_title_plural}")
|
||||||
|
|
|
@ -32,6 +32,8 @@ class PersonView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for people.
|
Master view for people.
|
||||||
|
|
||||||
|
Default route prefix is ``people``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/people/``
|
* ``/people/``
|
||||||
|
|
|
@ -28,12 +28,15 @@ from wuttjamaican.db.model import Role
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets
|
||||||
|
from wuttaweb.forms.schema import Permissions
|
||||||
|
|
||||||
|
|
||||||
class RoleView(MasterView):
|
class RoleView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for roles.
|
Master view for roles.
|
||||||
|
|
||||||
|
Default route prefix is ``roles``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/roles/``
|
* ``/roles/``
|
||||||
|
@ -69,6 +72,7 @@ class RoleView(MasterView):
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
role = f.model_instance
|
||||||
|
|
||||||
# never show these
|
# never show these
|
||||||
f.remove('permission_refs',
|
f.remove('permission_refs',
|
||||||
|
@ -80,6 +84,13 @@ class RoleView(MasterView):
|
||||||
# notes
|
# notes
|
||||||
f.set_widget('notes', widgets.NotesWidget())
|
f.set_widget('notes', widgets.NotesWidget())
|
||||||
|
|
||||||
|
# permissions
|
||||||
|
f.append('permissions')
|
||||||
|
self.wutta_permissions = self.get_available_permissions()
|
||||||
|
f.set_node('permissions', Permissions(self.request, permissions=self.wutta_permissions))
|
||||||
|
if not self.creating:
|
||||||
|
f.set_default('permissions', list(role.permissions))
|
||||||
|
|
||||||
def unique_name(self, node, value):
|
def unique_name(self, node, value):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -95,6 +106,113 @@ class RoleView(MasterView):
|
||||||
if query.count():
|
if query.count():
|
||||||
node.raise_invalid("Name must be unique")
|
node.raise_invalid("Name must be unique")
|
||||||
|
|
||||||
|
def get_available_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns all "available" permissions. This is used when
|
||||||
|
viewing or editing a role; the result is passed into the
|
||||||
|
:class:`~wuttaweb.forms.schema.Permissions` field schema.
|
||||||
|
|
||||||
|
The app itself must be made aware of each permission, in order
|
||||||
|
for them to found by this method. This is done via
|
||||||
|
:func:`~wuttaweb.auth.add_permission_group()` and
|
||||||
|
:func:`~wuttaweb.auth.add_permission()`.
|
||||||
|
|
||||||
|
When in "view" (readonly) mode, this method will return the
|
||||||
|
full set of known permissions.
|
||||||
|
|
||||||
|
However in "edit" mode, it will prune the set to remove any
|
||||||
|
permissions which the current user does not also have. The
|
||||||
|
idea here is to allow "many" users to manage roles, but ensure
|
||||||
|
they cannot "break out" of their own role by assigning extra
|
||||||
|
permissions to it.
|
||||||
|
|
||||||
|
The permissions returned will also be grouped, and each single
|
||||||
|
permission is also represented as a simple dict, e.g.::
|
||||||
|
|
||||||
|
{
|
||||||
|
'books': {
|
||||||
|
'key': 'books',
|
||||||
|
'label': "Books",
|
||||||
|
'perms': {
|
||||||
|
'books.list': {
|
||||||
|
'key': 'books.list',
|
||||||
|
'label': "Browse / search Books",
|
||||||
|
},
|
||||||
|
'books.view': {
|
||||||
|
'key': 'books.view',
|
||||||
|
'label': "View Book",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'widgets': {
|
||||||
|
'key': 'widgets',
|
||||||
|
'label': "Widgets",
|
||||||
|
'perms': {
|
||||||
|
'widgets.list': {
|
||||||
|
'key': 'widgets.list',
|
||||||
|
'label': "Browse / search Widgets",
|
||||||
|
},
|
||||||
|
'widgets.view': {
|
||||||
|
'key': 'widgets.view',
|
||||||
|
'label': "View Widget",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get all known permissions from settings cache
|
||||||
|
permissions = self.request.registry.settings.get('wutta_permissions', {})
|
||||||
|
|
||||||
|
# when viewing, we allow all permissions to be exposed for all users
|
||||||
|
if self.viewing:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# admin user gets to manage all permissions
|
||||||
|
if self.request.is_admin:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# non-admin user can only see permissions they're granted
|
||||||
|
available = {}
|
||||||
|
for gkey, group in permissions.items():
|
||||||
|
for pkey, perm in group['perms'].items():
|
||||||
|
if self.request.has_perm(pkey):
|
||||||
|
if gkey not in available:
|
||||||
|
available[gkey] = {
|
||||||
|
'key': gkey,
|
||||||
|
'label': group['label'],
|
||||||
|
'perms': {},
|
||||||
|
}
|
||||||
|
available[gkey]['perms'][pkey] = perm
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
|
def objectify(self, form):
|
||||||
|
""" """
|
||||||
|
# normal logic first
|
||||||
|
role = super().objectify(form)
|
||||||
|
|
||||||
|
# update permissions for role
|
||||||
|
self.update_permissions(role, form)
|
||||||
|
|
||||||
|
return role
|
||||||
|
|
||||||
|
def update_permissions(self, role, form):
|
||||||
|
""" """
|
||||||
|
if 'permissions' not in form.validated:
|
||||||
|
return
|
||||||
|
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
available = self.wutta_permissions
|
||||||
|
permissions = form.validated['permissions']
|
||||||
|
|
||||||
|
for gkey, group in available.items():
|
||||||
|
for pkey, perm in group['perms'].items():
|
||||||
|
if pkey in permissions:
|
||||||
|
auth.grant_permission(role, pkey)
|
||||||
|
else:
|
||||||
|
auth.revoke_permission(role, pkey)
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
@ -35,6 +35,8 @@ class AppInfoView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the core app info, to show/edit config etc.
|
Master view for the core app info, to show/edit config etc.
|
||||||
|
|
||||||
|
Default route prefix is ``appinfo``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/appinfo/``
|
* ``/appinfo/``
|
||||||
|
@ -137,6 +139,8 @@ class SettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the "raw" settings table.
|
Master view for the "raw" settings table.
|
||||||
|
|
||||||
|
Default route prefix is ``settings``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/settings/``
|
* ``/settings/``
|
||||||
|
|
|
@ -36,6 +36,8 @@ class UserView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for users.
|
Master view for users.
|
||||||
|
|
||||||
|
Default route prefix is ``users``.
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
Notable URLs provided by this class:
|
||||||
|
|
||||||
* ``/users/``
|
* ``/users/``
|
||||||
|
|
|
@ -221,3 +221,34 @@ class TestRoleRefs(DataTestCase):
|
||||||
self.assertEqual(len(widget.values), 2)
|
self.assertEqual(len(widget.values), 2)
|
||||||
self.assertEqual(widget.values[0][1], "Administrator")
|
self.assertEqual(widget.values[0][1], "Administrator")
|
||||||
self.assertEqual(widget.values[1][1], "Blokes")
|
self.assertEqual(widget.values[1][1], "Blokes")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPermissions(DataTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_db()
|
||||||
|
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||||
|
|
||||||
|
def test_widget_maker(self):
|
||||||
|
|
||||||
|
# no supported permissions
|
||||||
|
permissions = {}
|
||||||
|
typ = mod.Permissions(self.request, permissions)
|
||||||
|
widget = typ.widget_maker()
|
||||||
|
self.assertEqual(len(widget.values), 0)
|
||||||
|
|
||||||
|
# supported permissions are morphed to values
|
||||||
|
permissions = {
|
||||||
|
'widgets': {
|
||||||
|
'label': "Widgets",
|
||||||
|
'perms': {
|
||||||
|
'widgets.polish': {
|
||||||
|
'label': "Polish the widgets",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
typ = mod.Permissions(self.request, permissions)
|
||||||
|
widget = typ.widget_maker()
|
||||||
|
self.assertEqual(len(widget.values), 1)
|
||||||
|
self.assertEqual(widget.values[0], ('widgets.polish', "Polish the widgets"))
|
||||||
|
|
|
@ -5,7 +5,7 @@ import deform
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
from wuttaweb.forms import widgets as mod
|
from wuttaweb.forms import widgets as mod
|
||||||
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
from wuttaweb.forms.schema import PersonRef, RoleRefs, Permissions
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,3 +75,41 @@ class TestRoleRefsWidget(WebTestCase):
|
||||||
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||||
self.assertIn(admin.uuid, html)
|
self.assertIn(admin.uuid, html)
|
||||||
self.assertIn(blokes.uuid, html)
|
self.assertIn(blokes.uuid, html)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPermissionsWidget(WebTestCase):
|
||||||
|
|
||||||
|
def make_field(self, node, **kwargs):
|
||||||
|
# TODO: not sure why default renderer is in use even though
|
||||||
|
# pyramid_deform was included in setup? but this works..
|
||||||
|
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||||
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
permissions = {
|
||||||
|
'widgets': {
|
||||||
|
'label': "Widgets",
|
||||||
|
'perms': {
|
||||||
|
'widgets.polish': {
|
||||||
|
'label': "Polish the widgets",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# nb. we let the field construct the widget via our type
|
||||||
|
node = colander.SchemaNode(Permissions(self.request, permissions, session=self.session))
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = field.widget
|
||||||
|
|
||||||
|
# readonly output does *not* include the perm by default
|
||||||
|
html = widget.serialize(field, set(), readonly=True)
|
||||||
|
self.assertNotIn("Polish the widgets", html)
|
||||||
|
|
||||||
|
# readonly output includes the perm if set
|
||||||
|
html = widget.serialize(field, {'widgets.polish'}, readonly=True)
|
||||||
|
self.assertIn("Polish the widgets", html)
|
||||||
|
|
||||||
|
# editable output always includes the perm
|
||||||
|
html = widget.serialize(field, set())
|
||||||
|
self.assertIn("Polish the widgets", html)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from pyramid import testing
|
||||||
|
|
||||||
from wuttjamaican.conf import WuttaConfig
|
from wuttjamaican.conf import WuttaConfig
|
||||||
from wuttaweb import auth as mod
|
from wuttaweb import auth as mod
|
||||||
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestLoginUser(TestCase):
|
class TestLoginUser(TestCase):
|
||||||
|
@ -143,3 +144,26 @@ class TestWuttaSecurityPolicy(TestCase):
|
||||||
self.assertFalse(self.policy.permits(self.request, None, 'some-root-perm'))
|
self.assertFalse(self.policy.permits(self.request, None, 'some-root-perm'))
|
||||||
self.request.is_root = True
|
self.request.is_root = True
|
||||||
self.assertTrue(self.policy.permits(self.request, None, 'some-root-perm'))
|
self.assertTrue(self.policy.permits(self.request, None, 'some-root-perm'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddPermissionGroup(WebTestCase):
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
self.assertNotIn('widgets', permissions)
|
||||||
|
self.pyramid_config.add_wutta_permission_group('widgets')
|
||||||
|
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
self.assertIn('widgets', permissions)
|
||||||
|
self.assertEqual(permissions['widgets']['label'], "Widgets")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddPermission(WebTestCase):
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
self.assertNotIn('widgets', permissions)
|
||||||
|
self.pyramid_config.add_wutta_permission('widgets', 'widgets.polish')
|
||||||
|
permissions = self.pyramid_config.get_settings().get('wutta_permissions', {})
|
||||||
|
self.assertIn('widgets', permissions)
|
||||||
|
self.assertEqual(permissions['widgets']['label'], "Widgets")
|
||||||
|
self.assertIn('widgets.polish', permissions['widgets']['perms'])
|
||||||
|
|
|
@ -56,10 +56,13 @@ class WebTestCase(DataTestCase):
|
||||||
# init web
|
# init web
|
||||||
self.pyramid_config.include('pyramid_deform')
|
self.pyramid_config.include('pyramid_deform')
|
||||||
self.pyramid_config.include('pyramid_mako')
|
self.pyramid_config.include('pyramid_mako')
|
||||||
self.pyramid_config.include('wuttaweb.static')
|
self.pyramid_config.add_directive('add_wutta_permission_group',
|
||||||
self.pyramid_config.include('wuttaweb.views.essential')
|
'wuttaweb.auth.add_permission_group')
|
||||||
|
self.pyramid_config.add_directive('add_wutta_permission',
|
||||||
|
'wuttaweb.auth.add_permission')
|
||||||
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
self.pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
|
||||||
'pyramid.events.BeforeRender')
|
'pyramid.events.BeforeRender')
|
||||||
|
self.pyramid_config.include('wuttaweb.static')
|
||||||
|
|
||||||
# setup new request w/ anonymous user
|
# setup new request w/ anonymous user
|
||||||
event = MagicMock(request=self.request)
|
event = MagicMock(request=self.request)
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest import TestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
from pyramid import testing
|
|
||||||
|
|
||||||
|
|
||||||
class TestIncludeMe(TestCase):
|
class TestIncludeMe(WebTestCase):
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
with testing.testConfig() as pyramid_config:
|
# just ensure no error happens when included..
|
||||||
|
self.pyramid_config.include('wuttaweb.views')
|
||||||
# just ensure no error happens when included..
|
|
||||||
pyramid_config.include('wuttaweb.views')
|
|
||||||
|
|
|
@ -162,6 +162,24 @@ class TestMasterView(WebTestCase):
|
||||||
model_class=MyModel):
|
model_class=MyModel):
|
||||||
self.assertEqual(master.MasterView.get_route_prefix(), 'trucks')
|
self.assertEqual(master.MasterView.get_route_prefix(), 'trucks')
|
||||||
|
|
||||||
|
def test_get_permission_prefix(self):
|
||||||
|
|
||||||
|
# error by default (since no model class)
|
||||||
|
self.assertRaises(AttributeError, master.MasterView.get_permission_prefix)
|
||||||
|
|
||||||
|
# subclass may specify permission prefix
|
||||||
|
with patch.object(master.MasterView, 'permission_prefix', new='widgets', create=True):
|
||||||
|
self.assertEqual(master.MasterView.get_permission_prefix(), 'widgets')
|
||||||
|
|
||||||
|
# subclass may specify route prefix
|
||||||
|
with patch.object(master.MasterView, 'route_prefix', new='widgets', create=True):
|
||||||
|
self.assertEqual(master.MasterView.get_permission_prefix(), 'widgets')
|
||||||
|
|
||||||
|
# or it may specify model class
|
||||||
|
Truck = MagicMock(__name__='Truck')
|
||||||
|
with patch.object(master.MasterView, 'model_class', new=Truck, create=True):
|
||||||
|
self.assertEqual(master.MasterView.get_permission_prefix(), 'trucks')
|
||||||
|
|
||||||
def test_get_url_prefix(self):
|
def test_get_url_prefix(self):
|
||||||
|
|
||||||
# error by default (since no model class)
|
# error by default (since no model class)
|
||||||
|
@ -315,6 +333,9 @@ class TestMasterView(WebTestCase):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def test_render_to_response(self):
|
def test_render_to_response(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('appinfo', '/appinfo/')
|
||||||
|
|
||||||
def widgets(request): return {}
|
def widgets(request): return {}
|
||||||
self.pyramid_config.add_route('widgets', '/widgets/')
|
self.pyramid_config.add_route('widgets', '/widgets/')
|
||||||
|
@ -539,28 +560,38 @@ class TestMasterView(WebTestCase):
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('settings.create', '/settings/new')
|
||||||
|
self.pyramid_config.add_route('settings.view', '/settings/{name}')
|
||||||
|
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
|
||||||
|
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
|
||||||
|
|
||||||
# sanity/coverage check using /settings/
|
# sanity/coverage check using /settings/
|
||||||
master.MasterView.model_name = 'Setting'
|
with patch.multiple(master.MasterView, create=True,
|
||||||
master.MasterView.model_key = 'name'
|
model_name='Setting',
|
||||||
master.MasterView.grid_columns = ['name', 'value']
|
model_key='name',
|
||||||
view = master.MasterView(self.request)
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
response = view.index()
|
grid_columns=['name', 'value']):
|
||||||
# then again with data, to include view action url
|
view = master.MasterView(self.request)
|
||||||
data = [{'name': 'foo', 'value': 'bar'}]
|
|
||||||
with patch.object(view, 'get_grid_data', return_value=data):
|
|
||||||
response = view.index()
|
response = view.index()
|
||||||
del master.MasterView.model_name
|
|
||||||
del master.MasterView.model_key
|
# then again with data, to include view action url
|
||||||
del master.MasterView.grid_columns
|
data = [{'name': 'foo', 'value': 'bar'}]
|
||||||
|
with patch.object(view, 'get_grid_data', return_value=data):
|
||||||
|
response = view.index()
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('settings.view', '/settings/{name}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# sanity/coverage check using /settings/new
|
# sanity/coverage check using /settings/new
|
||||||
with patch.multiple(master.MasterView, create=True,
|
with patch.multiple(master.MasterView, create=True,
|
||||||
model_name='Setting',
|
model_name='Setting',
|
||||||
model_key='name',
|
model_key='name',
|
||||||
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
form_fields=['name', 'value']):
|
form_fields=['name', 'value']):
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
|
|
||||||
|
@ -604,6 +635,11 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'fraggle')
|
||||||
|
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('settings.create', '/settings/new')
|
||||||
|
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
|
||||||
|
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
|
||||||
|
|
||||||
# sanity/coverage check using /settings/XXX
|
# sanity/coverage check using /settings/XXX
|
||||||
setting = {'name': 'foo.bar', 'value': 'baz'}
|
setting = {'name': 'foo.bar', 'value': 'baz'}
|
||||||
|
@ -611,6 +647,7 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.multiple(master.MasterView, create=True,
|
with patch.multiple(master.MasterView, create=True,
|
||||||
model_name='Setting',
|
model_name='Setting',
|
||||||
model_key='name',
|
model_key='name',
|
||||||
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
grid_columns=['name', 'value'],
|
grid_columns=['name', 'value'],
|
||||||
form_fields=['name', 'value']):
|
form_fields=['name', 'value']):
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
|
@ -618,6 +655,11 @@ class TestMasterView(WebTestCase):
|
||||||
response = view.view()
|
response = view.view()
|
||||||
|
|
||||||
def test_edit(self):
|
def test_edit(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('settings.create', '/settings/new')
|
||||||
|
self.pyramid_config.add_route('settings.view', '/settings/{name}')
|
||||||
|
self.pyramid_config.add_route('settings.delete', '/settings/{name}/delete')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
@ -634,6 +676,7 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.multiple(master.MasterView, create=True,
|
with patch.multiple(master.MasterView, create=True,
|
||||||
model_name='Setting',
|
model_name='Setting',
|
||||||
model_key='name',
|
model_key='name',
|
||||||
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
form_fields=['name', 'value']):
|
form_fields=['name', 'value']):
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
with patch.object(view, 'get_instance', new=get_instance):
|
with patch.object(view, 'get_instance', new=get_instance):
|
||||||
|
@ -675,6 +718,11 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('settings.create', '/settings/new')
|
||||||
|
self.pyramid_config.add_route('settings.view', '/settings/{name}')
|
||||||
|
self.pyramid_config.add_route('settings.edit', '/settings/{name}/edit')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
@ -692,6 +740,7 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.multiple(master.MasterView, create=True,
|
with patch.multiple(master.MasterView, create=True,
|
||||||
model_name='Setting',
|
model_name='Setting',
|
||||||
model_key='name',
|
model_key='name',
|
||||||
|
get_index_url=MagicMock(return_value='/settings/'),
|
||||||
form_fields=['name', 'value']):
|
form_fields=['name', 'value']):
|
||||||
view = master.MasterView(self.request)
|
view = master.MasterView(self.request)
|
||||||
with patch.object(view, 'get_instance', new=get_instance):
|
with patch.object(view, 'get_instance', new=get_instance):
|
||||||
|
@ -730,6 +779,8 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
def test_configure(self):
|
def test_configure(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# mock settings
|
# mock settings
|
||||||
|
@ -750,6 +801,7 @@ class TestMasterView(WebTestCase):
|
||||||
route_prefix='appinfo',
|
route_prefix='appinfo',
|
||||||
template_prefix='/appinfo',
|
template_prefix='/appinfo',
|
||||||
creatable=False,
|
creatable=False,
|
||||||
|
get_index_url=MagicMock(return_value='/appinfo/'),
|
||||||
configure_get_simple_settings=MagicMock(return_value=settings)):
|
configure_get_simple_settings=MagicMock(return_value=settings)):
|
||||||
|
|
||||||
# get the form page
|
# get the form page
|
||||||
|
|
|
@ -15,6 +15,9 @@ class TestRoleView(WebTestCase):
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
return mod.RoleView(self.request)
|
return mod.RoleView(self.request)
|
||||||
|
|
||||||
|
def test_includeme(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.roles')
|
||||||
|
|
||||||
def test_get_query(self):
|
def test_get_query(self):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
query = view.get_query(session=self.session)
|
query = view.get_query(session=self.session)
|
||||||
|
@ -30,8 +33,9 @@ class TestRoleView(WebTestCase):
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
role = model.Role(name="Foo")
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
form = view.make_form(model_class=model.Person)
|
form = view.make_form(model_instance=role)
|
||||||
self.assertNotIn('name', form.validators)
|
self.assertNotIn('name', form.validators)
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertIsNotNone(form.validators['name'])
|
self.assertIsNotNone(form.validators['name'])
|
||||||
|
@ -55,3 +59,132 @@ class TestRoleView(WebTestCase):
|
||||||
self.request.matchdict = {'uuid': role.uuid}
|
self.request.matchdict = {'uuid': role.uuid}
|
||||||
node = colander.SchemaNode(colander.String(), name='name')
|
node = colander.SchemaNode(colander.String(), name='name')
|
||||||
self.assertIsNone(view.unique_name(node, 'Foo'))
|
self.assertIsNone(view.unique_name(node, 'Foo'))
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
return {
|
||||||
|
'widgets': {
|
||||||
|
'label': "Widgets",
|
||||||
|
'perms': {
|
||||||
|
'widgets.list': {
|
||||||
|
'label': "List widgets",
|
||||||
|
},
|
||||||
|
'widgets.polish': {
|
||||||
|
'label': "Polish the widgets",
|
||||||
|
},
|
||||||
|
'widgets.view': {
|
||||||
|
'label': "View widget",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_available_permissions(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
auth.grant_permission(blokes, 'widgets.list')
|
||||||
|
self.session.add(blokes)
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
barney.roles.append(blokes)
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
|
view = self.make_view()
|
||||||
|
all_perms = self.get_permissions()
|
||||||
|
self.request.registry.settings['wutta_permissions'] = all_perms
|
||||||
|
|
||||||
|
def has_perm(perm):
|
||||||
|
if perm == 'widgets.list':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
with patch.object(self.request, 'has_perm', new=has_perm, create=True):
|
||||||
|
|
||||||
|
# sanity check; current request has 1 perm
|
||||||
|
self.assertTrue(self.request.has_perm('widgets.list'))
|
||||||
|
self.assertFalse(self.request.has_perm('widgets.polish'))
|
||||||
|
self.assertFalse(self.request.has_perm('widgets.view'))
|
||||||
|
|
||||||
|
# when editing, user sees only the 1 perm
|
||||||
|
with patch.object(view, 'editing', new=True):
|
||||||
|
perms = view.get_available_permissions()
|
||||||
|
self.assertEqual(list(perms), ['widgets'])
|
||||||
|
self.assertEqual(list(perms['widgets']['perms']), ['widgets.list'])
|
||||||
|
|
||||||
|
# but when viewing, same user sees all perms
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
perms = view.get_available_permissions()
|
||||||
|
self.assertEqual(list(perms), ['widgets'])
|
||||||
|
self.assertEqual(list(perms['widgets']['perms']),
|
||||||
|
['widgets.list', 'widgets.polish', 'widgets.view'])
|
||||||
|
|
||||||
|
# also, when admin user is editing, sees all perms
|
||||||
|
self.request.is_admin = True
|
||||||
|
with patch.object(view, 'editing', new=True):
|
||||||
|
perms = view.get_available_permissions()
|
||||||
|
self.assertEqual(list(perms), ['widgets'])
|
||||||
|
self.assertEqual(list(perms['widgets']['perms']),
|
||||||
|
['widgets.list', 'widgets.polish', 'widgets.view'])
|
||||||
|
|
||||||
|
def test_objectify(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
self.session.add(blokes)
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
barney.roles.append(blokes)
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
|
view = self.make_view()
|
||||||
|
permissions = self.get_permissions()
|
||||||
|
|
||||||
|
# sanity check, role has just 1 perm
|
||||||
|
auth.grant_permission(blokes, 'widgets.list')
|
||||||
|
self.session.commit()
|
||||||
|
self.assertEqual(blokes.permissions, ['widgets.list'])
|
||||||
|
|
||||||
|
# form can update role perms
|
||||||
|
view.editing = True
|
||||||
|
self.request.matchdict = {'uuid': blokes.uuid}
|
||||||
|
with patch.object(view, 'get_available_permissions', return_value=permissions):
|
||||||
|
form = view.make_model_form(model_instance=blokes)
|
||||||
|
form.validated = {'name': 'Blokes',
|
||||||
|
'permissions': {'widgets.list', 'widgets.polish', 'widgets.view'}}
|
||||||
|
role = view.objectify(form)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertIs(role, blokes)
|
||||||
|
self.assertEqual(blokes.permissions, ['widgets.list', 'widgets.polish', 'widgets.view'])
|
||||||
|
|
||||||
|
def test_update_permissions(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
auth.grant_permission(blokes, 'widgets.list')
|
||||||
|
self.session.add(blokes)
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
barney.roles.append(blokes)
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
|
view = self.make_view()
|
||||||
|
permissions = self.get_permissions()
|
||||||
|
|
||||||
|
with patch.object(view, 'get_available_permissions', return_value=permissions):
|
||||||
|
|
||||||
|
# no error if data is missing perms
|
||||||
|
form = view.make_model_form(model_instance=blokes)
|
||||||
|
form.validated = {'name': 'BloX'}
|
||||||
|
role = view.objectify(form)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertIs(role, blokes)
|
||||||
|
self.assertEqual(blokes.name, 'BloX')
|
||||||
|
|
||||||
|
# sanity check, role has just 1 perm
|
||||||
|
self.assertEqual(blokes.permissions, ['widgets.list'])
|
||||||
|
|
||||||
|
# role perms are updated
|
||||||
|
form = view.make_model_form(model_instance=blokes)
|
||||||
|
form.validated = {'name': 'Blokes',
|
||||||
|
'permissions': {'widgets.polish', 'widgets.view'}}
|
||||||
|
role = view.objectify(form)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertIs(role, blokes)
|
||||||
|
self.assertEqual(blokes.permissions, ['widgets.polish', 'widgets.view'])
|
||||||
|
|
|
@ -10,6 +10,10 @@ from tests.util import WebTestCase
|
||||||
|
|
||||||
class TestAppInfoView(WebTestCase):
|
class TestAppInfoView(WebTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_web()
|
||||||
|
self.pyramid_config.include('wuttaweb.views.essential')
|
||||||
|
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
return settings.AppInfoView(self.request)
|
return settings.AppInfoView(self.request)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue