feat: add basic Roles view
can't edit user/role/perm mappings yet, just minimal CRUD
This commit is contained in:
parent
eac3b81918
commit
7ad6a9d5a0
|
@ -29,5 +29,6 @@
|
||||||
views.essential
|
views.essential
|
||||||
views.master
|
views.master
|
||||||
views.people
|
views.people
|
||||||
|
views.roles
|
||||||
views.settings
|
views.settings
|
||||||
views.users
|
views.users
|
||||||
|
|
6
docs/api/wuttaweb/views.roles.rst
Normal file
6
docs/api/wuttaweb/views.roles.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``wuttaweb.views.roles``
|
||||||
|
========================
|
||||||
|
|
||||||
|
.. automodule:: wuttaweb.views.roles
|
||||||
|
:members:
|
|
@ -152,6 +152,11 @@ class MenuHandler(GenericHandler):
|
||||||
'route': 'users',
|
'route': 'users',
|
||||||
'perm': 'users.list',
|
'perm': 'users.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Roles",
|
||||||
|
'route': 'roles',
|
||||||
|
'perm': 'roles.list',
|
||||||
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "App Info",
|
'title': "App Info",
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
<b-field horizontal label="App Title">
|
<b-field horizontal label="App Title">
|
||||||
<span>${app.get_title()}</span>
|
<span>${app.get_title()}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
<b-field horizontal label="Production Mode">
|
||||||
|
<span>${config.production()}</span>
|
||||||
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -33,6 +33,7 @@ That will in turn include the following modules:
|
||||||
* :mod:`wuttaweb.views.common`
|
* :mod:`wuttaweb.views.common`
|
||||||
* :mod:`wuttaweb.views.settings`
|
* :mod:`wuttaweb.views.settings`
|
||||||
* :mod:`wuttaweb.views.people`
|
* :mod:`wuttaweb.views.people`
|
||||||
|
* :mod:`wuttaweb.views.roles`
|
||||||
* :mod:`wuttaweb.views.users`
|
* :mod:`wuttaweb.views.users`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ def defaults(config, **kwargs):
|
||||||
config.include(mod('wuttaweb.views.common'))
|
config.include(mod('wuttaweb.views.common'))
|
||||||
config.include(mod('wuttaweb.views.settings'))
|
config.include(mod('wuttaweb.views.settings'))
|
||||||
config.include(mod('wuttaweb.views.people'))
|
config.include(mod('wuttaweb.views.people'))
|
||||||
|
config.include(mod('wuttaweb.views.roles'))
|
||||||
config.include(mod('wuttaweb.views.users'))
|
config.include(mod('wuttaweb.views.users'))
|
||||||
|
|
||||||
|
|
||||||
|
|
100
src/wuttaweb/views/roles.py
Normal file
100
src/wuttaweb/views/roles.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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)
|
57
tests/views/test_roles.py
Normal file
57
tests/views/test_roles.py
Normal file
|
@ -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'))
|
|
@ -5,16 +5,15 @@ from unittest.mock import patch
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from pyramid.httpexceptions import HTTPNotFound
|
|
||||||
|
|
||||||
from wuttaweb.views import users
|
from wuttaweb.views import users as mod
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestPersonView(WebTestCase):
|
class TestUserView(WebTestCase):
|
||||||
|
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
return users.UserView(self.request)
|
return mod.UserView(self.request)
|
||||||
|
|
||||||
def test_get_query(self):
|
def test_get_query(self):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -45,7 +44,7 @@ class TestPersonView(WebTestCase):
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
self.session.commit()
|
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
|
# invalid if same username in data
|
||||||
node = colander.SchemaNode(colander.String(), name='username')
|
node = colander.SchemaNode(colander.String(), name='username')
|
||||||
|
|
Loading…
Reference in a new issue