1
0
Fork 0

feat: add People view; improve CRUD master for SQLAlchemy models

This commit is contained in:
Lance Edgar 2024-08-11 18:21:02 -05:00
parent fc01fa283a
commit 33589f1cd8
10 changed files with 275 additions and 43 deletions

View file

@ -26,4 +26,5 @@
views.common
views.essential
views.master
views.people
views.settings

View file

@ -0,0 +1,6 @@
``wuttaweb.views.people``
===========================
.. automodule:: wuttaweb.views.people
:members:

View file

@ -815,8 +815,9 @@ class Form:
if self.readonly_fields:
schema = self.get_schema()
for field in self.readonly_fields:
del schema[field]
dform.children.remove(dform[field])
if field in schema:
del schema[field]
dform.children.remove(dform[field])
# let deform do real validation
controls = get_form_data(self.request).items()

View file

@ -97,11 +97,41 @@ class MenuHandler(GenericHandler):
is expected for most apps to override it.
The return value should be a list of dicts as described above.
The default logic returns a list of menus obtained from
calling these methods:
* :meth:`make_people_menu()`
* :meth:`make_admin_menu()`
"""
return [
self.make_people_menu(request),
self.make_admin_menu(request),
]
def make_people_menu(self, request, **kwargs):
"""
Generate a typical People menu.
This method provides a semi-sane menu set by default, but it
is expected for most apps to override it.
The return value for this method should be a *single* dict,
which will ultimately be one element of the final list of
dicts as described in :class:`MenuHandler`.
"""
return {
'title': "People",
'type': 'menu',
'items': [
{
'title': "All People",
'route': 'people',
'perm': 'people.list',
},
],
}
def make_admin_menu(self, request, **kwargs):
"""
Generate a typical Admin menu.
@ -111,7 +141,7 @@ class MenuHandler(GenericHandler):
The return value for this method should be a *single* dict,
which will ultimately be one element of the final list of
dicts as described above.
dicts as described in :class:`MenuHandler`.
"""
return {
'title': "Admin",

View file

@ -31,6 +31,8 @@ That will in turn include the following modules:
* :mod:`wuttaweb.views.auth`
* :mod:`wuttaweb.views.common`
* :mod:`wuttaweb.views.settings`
* :mod:`wuttaweb.views.people`
"""
@ -40,6 +42,7 @@ def defaults(config, **kwargs):
config.include(mod('wuttaweb.views.auth'))
config.include(mod('wuttaweb.views.common'))
config.include(mod('wuttaweb.views.settings'))
config.include(mod('wuttaweb.views.people'))
def includeme(config):

View file

@ -30,7 +30,7 @@ from sqlalchemy import orm
from pyramid.renderers import render_to_response
from wuttaweb.views import View
from wuttaweb.util import get_form_data
from wuttaweb.util import get_form_data, get_model_fields
from wuttaweb.db import Session
@ -292,6 +292,7 @@ class MasterView(View):
if form.validate():
obj = self.create_save_form(form)
Session.flush()
return self.redirect(self.get_action_url('view', obj))
context = {
@ -910,7 +911,8 @@ class MasterView(View):
kwargs['columns'] = self.get_grid_columns()
if 'data' not in kwargs:
kwargs['data'] = self.get_grid_data(session=session)
kwargs['data'] = self.get_grid_data(columns=kwargs['columns'],
session=session)
if 'actions' not in kwargs:
actions = []
@ -961,7 +963,7 @@ class MasterView(View):
if hasattr(self, 'grid_columns'):
return self.grid_columns
def get_grid_data(self, session=None):
def get_grid_data(self, columns=None, session=None):
"""
Returns the grid data for the :meth:`index()` view.
@ -974,8 +976,27 @@ class MasterView(View):
empty list. Subclass should override as needed.
"""
query = self.get_query(session=session)
if query is not None:
return query.all()
if query:
data = query.all()
# determine which columns are relevant for data set
if not columns:
columns = self.get_grid_columns()
if not columns:
model_class = self.get_model_class()
if model_class:
columns = get_model_fields(self.config, model_class)
if not columns:
raise ValueError("cannot determine columns for the grid")
columns = set(columns)
columns.update(self.get_model_key())
# prune data fields for which no column is defined
for i, record in enumerate(data):
data[i]= dict([(key, record[key])
for key in columns])
return data
return []
@ -1131,7 +1152,6 @@ class MasterView(View):
kwargs['model_instance'] = model_instance
# if 'fields' not in kwargs:
if not kwargs.get('fields'):
fields = self.get_form_fields()
if fields:
@ -1181,8 +1201,13 @@ class MasterView(View):
already be "complete" and ready to use as-is, but this method
can further modify it based on request details etc.
"""
model_keys = self.get_model_key()
if 'uuid' in form:
form.fields.remove('uuid')
if self.editing:
for key in self.get_model_key():
for key in model_keys:
form.set_readonly(key)
def objectify(self, form):

View file

@ -0,0 +1,95 @@
# -*- 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 people
"""
from wuttjamaican.db.model import Person
from wuttaweb.views import MasterView
class PersonView(MasterView):
"""
Master view for people.
Notable URLs provided by this class:
* ``/people/``
* ``/people/new``
* ``/people/XXX``
* ``/people/XXX/edit``
* ``/people/XXX/delete``
"""
model_class = Person
model_title_plural = "People"
route_prefix = 'people'
grid_columns = [
'full_name',
'first_name',
'middle_name',
'last_name',
]
# 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.Person.full_name)
def configure_grid(self, g):
""" """
super().configure_grid(g)
# full_name
g.set_link('full_name')
# TODO: master should handle this?
def configure_form(self, f):
""" """
super().configure_form(f)
# first_name
f.set_required('first_name', False)
# middle_name
f.set_required('middle_name', False)
# last_name
f.set_required('last_name', False)
# users
if 'users' in f:
f.fields.remove('users')
def defaults(config, **kwargs):
base = globals()
PersonView = kwargs.get('PersonView', base['PersonView'])
PersonView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -26,12 +26,9 @@ Views for app settings
from collections import OrderedDict
import colander
from wuttjamaican.db.model import Setting
from wuttaweb.views import MasterView
from wuttaweb.util import get_libver, get_liburl
from wuttaweb.db import Session
class AppInfoView(MasterView):

View file

@ -36,9 +36,9 @@ class TestMasterView(WebTestCase):
# subclass may specify
MyModel = MagicMock()
master.MasterView.model_class = MyModel
self.assertIs(master.MasterView.get_model_class(), MyModel)
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertIs(master.MasterView.get_model_class(), MyModel)
def test_get_model_name(self):
@ -52,9 +52,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Blaster')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_name(), 'Blaster')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_model_name(), 'Blaster')
def test_get_model_name_normalized(self):
@ -73,9 +73,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_name_normalized(), 'dinosaur')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_model_name_normalized(), 'dinosaur')
def test_get_model_title(self):
@ -94,9 +94,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_title(), "Dinosaur")
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_model_title(), "Dinosaur")
def test_get_model_title_plural(self):
@ -120,9 +120,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_model_title_plural(), "Dinosaurs")
def test_get_model_key(self):
@ -156,9 +156,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Truck')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_route_prefix(), 'trucks')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_route_prefix(), 'trucks')
def test_get_url_prefix(self):
@ -187,9 +187,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_url_prefix(), '/machines')
def test_get_instance_url_prefix(self):
@ -242,9 +242,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_template_prefix(), '/machines')
def test_get_grid_key(self):
@ -273,9 +273,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Machine')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_grid_key(), 'machines')
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_grid_key(), 'machines')
def test_get_config_title(self):
@ -304,9 +304,9 @@ class TestMasterView(WebTestCase):
# or it may specify model class
MyModel = MagicMock(__name__='Dinosaur')
master.MasterView.model_class = MyModel
self.assertEqual(master.MasterView.get_config_title(), "Dinosaurs")
del master.MasterView.model_class
with patch.multiple(master.MasterView, create=True,
model_class=MyModel):
self.assertEqual(master.MasterView.get_config_title(), "Dinosaurs")
##############################
# support methods
@ -365,6 +365,28 @@ class TestMasterView(WebTestCase):
grid = view.make_model_grid(session=self.session)
self.assertIs(grid.model_class, model.Setting)
def test_get_grid_data(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
self.session.commit()
# basic logic with Setting model
with patch.multiple(master.MasterView, create=True,
model_class=model.Setting):
view = master.MasterView(self.request)
data = view.get_grid_data(session=self.session)
self.assertEqual(len(data), 1)
self.assertEqual(data[0], {'name': 'foo', 'value': 'bar'})
# error if model not known
view = master.MasterView(self.request)
self.assertFalse(hasattr(master.MasterView, 'model_class'))
def get_query(session=None):
session = session or self.session
return session.query(model.Setting)
with patch.object(view, 'get_query', new=get_query):
self.assertRaises(ValueError, view.get_grid_data, session=self.session)
def test_get_instance(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')
@ -408,6 +430,19 @@ class TestMasterView(WebTestCase):
form = view.make_model_form()
self.assertIs(form.model_class, model.Setting)
def test_configure_form(self):
model = self.app.model
# uuid field is pruned
with patch.multiple(master.MasterView, create=True,
model_class=model.Setting):
view = master.MasterView(self.request)
form = view.make_form(model_class=model.Setting,
fields=['uuid', 'name', 'value'])
self.assertIn('uuid', form.fields)
view.configure_form(form)
self.assertNotIn('uuid', form.fields)
def test_objectify(self):
model = self.app.model
self.app.save_setting(self.session, 'foo', 'bar')

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
from sqlalchemy import orm
from pyramid.httpexceptions import HTTPNotFound
from wuttaweb.views import people
from tests.views.utils import WebTestCase
class TestPersonView(WebTestCase):
def make_view(self):
return people.PersonView(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.Setting)
self.assertEqual(grid.linked_columns, [])
view.configure_grid(grid)
self.assertIn('full_name', grid.linked_columns)
def test_configure_form(self):
model = self.app.model
view = self.make_view()
form = view.make_form(model_class=model.Person)
form.set_fields(form.get_model_fields())
self.assertEqual(form.required_fields, {})
view.configure_form(form)
self.assertTrue(form.required_fields)
self.assertFalse(form.required_fields['middle_name'])