diff --git a/docs/api/wuttaweb/index.rst b/docs/api/wuttaweb/index.rst index 273dd66..1410a20 100644 --- a/docs/api/wuttaweb/index.rst +++ b/docs/api/wuttaweb/index.rst @@ -29,5 +29,6 @@ views.essential views.master views.people + views.roles views.settings views.users diff --git a/docs/api/wuttaweb/views.roles.rst b/docs/api/wuttaweb/views.roles.rst new file mode 100644 index 0000000..8770256 --- /dev/null +++ b/docs/api/wuttaweb/views.roles.rst @@ -0,0 +1,6 @@ + +``wuttaweb.views.roles`` +======================== + +.. automodule:: wuttaweb.views.roles + :members: diff --git a/src/wuttaweb/menus.py b/src/wuttaweb/menus.py index 0dfb6f5..fc4581a 100644 --- a/src/wuttaweb/menus.py +++ b/src/wuttaweb/menus.py @@ -152,6 +152,11 @@ class MenuHandler(GenericHandler): 'route': 'users', 'perm': 'users.list', }, + { + 'title': "Roles", + 'route': 'roles', + 'perm': 'roles.list', + }, {'type': 'sep'}, { 'title': "App Info", diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako index e92a660..710d02c 100644 --- a/src/wuttaweb/templates/appinfo/index.mako +++ b/src/wuttaweb/templates/appinfo/index.mako @@ -16,6 +16,9 @@ ${app.get_title()} + + ${config.production()} + diff --git a/src/wuttaweb/views/essential.py b/src/wuttaweb/views/essential.py index 2a77df5..1387a99 100644 --- a/src/wuttaweb/views/essential.py +++ b/src/wuttaweb/views/essential.py @@ -33,6 +33,7 @@ That will in turn include the following modules: * :mod:`wuttaweb.views.common` * :mod:`wuttaweb.views.settings` * :mod:`wuttaweb.views.people` +* :mod:`wuttaweb.views.roles` * :mod:`wuttaweb.views.users` """ @@ -44,6 +45,7 @@ def defaults(config, **kwargs): config.include(mod('wuttaweb.views.common')) config.include(mod('wuttaweb.views.settings')) config.include(mod('wuttaweb.views.people')) + config.include(mod('wuttaweb.views.roles')) config.include(mod('wuttaweb.views.users')) diff --git a/src/wuttaweb/views/roles.py b/src/wuttaweb/views/roles.py new file mode 100644 index 0000000..51111e0 --- /dev/null +++ b/src/wuttaweb/views/roles.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# wuttaweb -- Web App for Wutta Framework +# Copyright © 2024 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Views for roles +""" + +from wuttjamaican.db.model import Role +from wuttaweb.views import MasterView +from wuttaweb.db import Session + + +class RoleView(MasterView): + """ + Master view for roles. + + Notable URLs provided by this class: + + * ``/roles/`` + * ``/roles/new`` + * ``/roles/XXX`` + * ``/roles/XXX/edit`` + * ``/roles/XXX/delete`` + """ + model_class = Role + + grid_columns = [ + 'name', + 'notes', + ] + + # TODO: master should handle this, possibly via configure_form() + def get_query(self, session=None): + """ """ + model = self.app.model + query = super().get_query(session=session) + return query.order_by(model.Role.name) + + def configure_grid(self, g): + """ """ + super().configure_grid(g) + + # name + g.set_link('name') + + def configure_form(self, f): + """ """ + super().configure_form(f) + + # never show these + f.remove('permission_refs', + 'user_refs') + + # name + f.set_validator('name', self.unique_name) + + def unique_name(self, node, value): + """ """ + model = self.app.model + session = Session() + + query = session.query(model.Role)\ + .filter(model.Role.name == value) + + if self.editing: + uuid = self.request.matchdict['uuid'] + query = query.filter(model.Role.uuid != uuid) + + if query.count(): + node.raise_invalid("Name must be unique") + + +def defaults(config, **kwargs): + base = globals() + + RoleView = kwargs.get('RoleView', base['RoleView']) + RoleView.defaults(config) + + +def includeme(config): + defaults(config) diff --git a/tests/views/test_roles.py b/tests/views/test_roles.py new file mode 100644 index 0000000..49c6197 --- /dev/null +++ b/tests/views/test_roles.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8; -*- + +from unittest.mock import patch + +from sqlalchemy import orm + +import colander + +from wuttaweb.views import roles as mod +from tests.util import WebTestCase + + +class TestRoleView(WebTestCase): + + def make_view(self): + return mod.RoleView(self.request) + + def test_get_query(self): + view = self.make_view() + query = view.get_query(session=self.session) + self.assertIsInstance(query, orm.Query) + + def test_configure_grid(self): + model = self.app.model + view = self.make_view() + grid = view.make_grid(model_class=model.Role) + self.assertFalse(grid.is_linked('name')) + view.configure_grid(grid) + self.assertTrue(grid.is_linked('name')) + + def test_configure_form(self): + model = self.app.model + view = self.make_view() + form = view.make_form(model_class=model.Person) + self.assertNotIn('name', form.validators) + view.configure_form(form) + self.assertIsNotNone(form.validators['name']) + + def test_unique_name(self): + model = self.app.model + view = self.make_view() + + role = model.Role(name='Foo') + self.session.add(role) + self.session.commit() + + with patch.object(mod, 'Session', return_value=self.session): + + # invalid if same name in data + node = colander.SchemaNode(colander.String(), name='name') + self.assertRaises(colander.Invalid, view.unique_name, node, 'Foo') + + # but not if name belongs to current role + view.editing = True + self.request.matchdict = {'uuid': role.uuid} + node = colander.SchemaNode(colander.String(), name='name') + self.assertIsNone(view.unique_name(node, 'Foo')) diff --git a/tests/views/test_users.py b/tests/views/test_users.py index a7be394..ea67544 100644 --- a/tests/views/test_users.py +++ b/tests/views/test_users.py @@ -5,16 +5,15 @@ from unittest.mock import patch from sqlalchemy import orm import colander -from pyramid.httpexceptions import HTTPNotFound -from wuttaweb.views import users +from wuttaweb.views import users as mod from tests.util import WebTestCase -class TestPersonView(WebTestCase): +class TestUserView(WebTestCase): def make_view(self): - return users.UserView(self.request) + return mod.UserView(self.request) def test_get_query(self): view = self.make_view() @@ -45,7 +44,7 @@ class TestPersonView(WebTestCase): self.session.add(user) self.session.commit() - with patch.object(users, 'Session', return_value=self.session): + with patch.object(mod, 'Session', return_value=self.session): # invalid if same username in data node = colander.SchemaNode(colander.String(), name='username')