feat: add basic support for wutta-continuum
and i mean *basic* - so far.. eventually will expose version history for viewing etc. unfortunately got carried away and reorganized the api docs a little while i was at it..
This commit is contained in:
parent
24f5ee1dcc
commit
71728718d8
|
@ -1,10 +0,0 @@
|
|||
|
||||
Package API
|
||||
===========
|
||||
|
||||
This is the "raw" API documentation for the ``wuttaweb`` package.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
wuttaweb/index
|
6
docs/api/wuttaweb.conf.rst
Normal file
6
docs/api/wuttaweb.conf.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.conf``
|
||||
=================
|
||||
|
||||
.. automodule:: wuttaweb.conf
|
||||
:members:
|
6
docs/api/wuttaweb.db.continuum.rst
Normal file
6
docs/api/wuttaweb.db.continuum.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.db.continuum``
|
||||
=========================
|
||||
|
||||
.. automodule:: wuttaweb.db.continuum
|
||||
:members:
|
6
docs/api/wuttaweb.db.sess.rst
Normal file
6
docs/api/wuttaweb.db.sess.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb.db.sess``
|
||||
====================
|
||||
|
||||
.. automodule:: wuttaweb.db.sess
|
||||
:members:
|
6
docs/api/wuttaweb.rst
Normal file
6
docs/api/wuttaweb.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
``wuttaweb``
|
||||
============
|
||||
|
||||
.. automodule:: wuttaweb
|
||||
:members:
|
|
@ -1,38 +0,0 @@
|
|||
|
||||
``wuttaweb``
|
||||
============
|
||||
|
||||
.. automodule:: wuttaweb
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
app
|
||||
auth
|
||||
db
|
||||
forms
|
||||
forms.base
|
||||
forms.schema
|
||||
forms.widgets
|
||||
grids
|
||||
grids.base
|
||||
grids.filters
|
||||
handler
|
||||
helpers
|
||||
menus
|
||||
progress
|
||||
static
|
||||
subscribers
|
||||
util
|
||||
views
|
||||
views.auth
|
||||
views.base
|
||||
views.common
|
||||
views.essential
|
||||
views.master
|
||||
views.people
|
||||
views.progress
|
||||
views.roles
|
||||
views.settings
|
||||
views.upgrades
|
||||
views.users
|
|
@ -34,6 +34,7 @@ intersphinx_mapping = {
|
|||
'rattail-manual': ('https://rattailproject.org/docs/rattail-manual/', None),
|
||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
||||
'wutta-continuum': ('https://rattailproject.org/docs/wutta-continuum/', None),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,13 +11,53 @@ project.
|
|||
|
||||
.. _test coverage: https://buildbot.rattailproject.org/coverage/wuttaweb/
|
||||
|
||||
However as you can see..the API should be fairly well documented but
|
||||
the narrative docs are pretty scant. That will eventually change.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
:caption: Contents:
|
||||
:maxdepth: 2
|
||||
:caption: Documentation:
|
||||
|
||||
glossary
|
||||
narr/index
|
||||
api/index
|
||||
narr/templates/index
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Package API:
|
||||
|
||||
api/wuttaweb
|
||||
api/wuttaweb.app
|
||||
api/wuttaweb.auth
|
||||
api/wuttaweb.conf
|
||||
api/wuttaweb.db
|
||||
api/wuttaweb.db.continuum
|
||||
api/wuttaweb.db.sess
|
||||
api/wuttaweb.forms
|
||||
api/wuttaweb.forms.base
|
||||
api/wuttaweb.forms.schema
|
||||
api/wuttaweb.forms.widgets
|
||||
api/wuttaweb.grids
|
||||
api/wuttaweb.grids.base
|
||||
api/wuttaweb.grids.filters
|
||||
api/wuttaweb.handler
|
||||
api/wuttaweb.helpers
|
||||
api/wuttaweb.menus
|
||||
api/wuttaweb.progress
|
||||
api/wuttaweb.static
|
||||
api/wuttaweb.subscribers
|
||||
api/wuttaweb.util
|
||||
api/wuttaweb.views
|
||||
api/wuttaweb.views.auth
|
||||
api/wuttaweb.views.base
|
||||
api/wuttaweb.views.common
|
||||
api/wuttaweb.views.essential
|
||||
api/wuttaweb.views.master
|
||||
api/wuttaweb.views.people
|
||||
api/wuttaweb.views.progress
|
||||
api/wuttaweb.views.roles
|
||||
api/wuttaweb.views.settings
|
||||
api/wuttaweb.views.upgrades
|
||||
api/wuttaweb.views.users
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
templates/index
|
|
@ -42,12 +42,13 @@ dependencies = [
|
|||
"pyramid_tm",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
"WuttJamaican[db,email]>=0.13.0",
|
||||
"WuttJamaican[db,email]>=0.13.2",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
||||
[project.optional-dependencies]
|
||||
continuum = ["Wutta-Continuum"]
|
||||
docs = ["Sphinx", "furo"]
|
||||
tests = ["pytest-cov", "tox"]
|
||||
|
||||
|
@ -60,6 +61,10 @@ main = "wuttaweb.app:main"
|
|||
wuttaweb = "wuttaweb.app:WebAppProvider"
|
||||
|
||||
|
||||
[project.entry-points."wutta.config.extensions"]
|
||||
wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension"
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://wuttaproject.org/"
|
||||
Repository = "https://forgejo.wuttaproject.org/wutta/wuttaweb"
|
||||
|
|
|
@ -98,7 +98,8 @@ class WuttaSecurityPolicy:
|
|||
for current user
|
||||
|
||||
:param db_session: Optional :term:`db session` to use, instead of
|
||||
:class:`wuttaweb.db.Session`. Probably only useful for tests.
|
||||
:class:`wuttaweb.db.sess.Session`. Probably only useful for
|
||||
tests.
|
||||
"""
|
||||
|
||||
def __init__(self, db_session=None):
|
||||
|
|
43
src/wuttaweb/conf.py
Normal file
43
src/wuttaweb/conf.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Config Extension
|
||||
"""
|
||||
|
||||
from wuttjamaican.conf import WuttaConfigExtension
|
||||
|
||||
|
||||
class WuttaWebConfigExtension(WuttaConfigExtension):
|
||||
"""
|
||||
Config extension for WuttaWeb.
|
||||
|
||||
This sets the default plugin for SQLAlchemy-Continuum. Which is
|
||||
only relevant if Wutta-Continuum is installed and enabled. For
|
||||
more info see :doc:`wutta-continuum:index`.
|
||||
"""
|
||||
key = 'wuttaweb'
|
||||
|
||||
def configure(self, config):
|
||||
""" """
|
||||
config.setdefault('wutta_continuum.wutta_plugin_spec',
|
||||
'wuttaweb.db.continuum:WuttaWebContinuumPlugin')
|
31
src/wuttaweb/db/__init__.py
Normal file
31
src/wuttaweb/db/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Database Utilities
|
||||
|
||||
The following are available from this ``wuttaweb.db`` namespace:
|
||||
|
||||
* :class:`~wuttaweb.db.sess.Session`
|
||||
"""
|
||||
|
||||
from .sess import Session
|
56
src/wuttaweb/db/continuum.py
Normal file
56
src/wuttaweb/db/continuum.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
SQLAlchemy-Continuum Plugin
|
||||
"""
|
||||
|
||||
from pyramid.threadlocal import get_current_request
|
||||
|
||||
try:
|
||||
from wutta_continuum.conf import WuttaContinuumPlugin
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
|
||||
class WuttaWebContinuumPlugin(WuttaContinuumPlugin):
|
||||
"""
|
||||
SQLAlchemy-Continuum manager plugin for WuttaWeb.
|
||||
|
||||
This tries to use the current request to obtain user and IP
|
||||
address for the transaction.
|
||||
"""
|
||||
|
||||
# TODO: should find a better way, threadlocals are bad?
|
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/api/threadlocal.html#pyramid.threadlocal.get_current_request
|
||||
|
||||
def get_remote_addr(self, uow, session):
|
||||
""" """
|
||||
request = get_current_request()
|
||||
if request:
|
||||
return request.client_addr
|
||||
|
||||
def get_user_id(self, uow, session):
|
||||
""" """
|
||||
request = get_current_request()
|
||||
if request and request.user:
|
||||
return request.user.uuid
|
|
@ -128,7 +128,7 @@ class WuttaSet(colander.Set):
|
|||
:param request: Current :term:`request` object.
|
||||
|
||||
:param session: Optional :term:`db session` to use instead of
|
||||
:class:`wuttaweb.db.Session`.
|
||||
:class:`wuttaweb.db.sess.Session`.
|
||||
"""
|
||||
|
||||
def __init__(self, request, session=None):
|
||||
|
|
|
@ -99,8 +99,11 @@ class ObjectRefWidget(SelectWidget):
|
|||
""" """
|
||||
values = super().get_template_values(field, cstruct, kw)
|
||||
|
||||
if 'url' not in values and self.url and field.schema.model_instance:
|
||||
values['url'] = self.url(field.schema.model_instance)
|
||||
# add url, only if rendering readonly
|
||||
readonly = kw.get('readonly', self.readonly)
|
||||
if readonly:
|
||||
if 'url' not in values and self.url and field.schema.model_instance:
|
||||
values['url'] = self.url(field.schema.model_instance)
|
||||
|
||||
return values
|
||||
|
||||
|
@ -134,7 +137,7 @@ class WuttaCheckboxChoiceWidget(CheckboxChoiceWidget):
|
|||
:param request: Current :term:`request` object.
|
||||
|
||||
:param session: Optional :term:`db session` to use instead of
|
||||
:class:`wuttaweb.db.Session`.
|
||||
:class:`wuttaweb.db.sess.Session`.
|
||||
|
||||
It uses these Deform templates:
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ def new_request_set_user(
|
|||
from database, instead of :func:`default_user_getter()`.
|
||||
|
||||
:param db_session: Optional :term:`db session` to use,
|
||||
instead of :class:`wuttaweb.db.Session`.
|
||||
instead of :class:`wuttaweb.db.sess.Session`.
|
||||
|
||||
This will add to the request object:
|
||||
|
||||
|
|
|
@ -491,10 +491,16 @@ def get_model_fields(config, model_class=None):
|
|||
try:
|
||||
mapper = sa.inspect(model_class)
|
||||
except sa.exc.NoInspectionAvailable:
|
||||
pass
|
||||
else:
|
||||
fields = [prop.key for prop in mapper.iterate_properties]
|
||||
return fields
|
||||
return
|
||||
|
||||
fields = [prop.key for prop in mapper.iterate_properties]
|
||||
|
||||
# nb. we never want the continuum 'versions' prop
|
||||
app = config.get_app()
|
||||
if app.continuum_is_enabled() and 'versions' in fields:
|
||||
fields.remove('versions')
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def make_json_safe(value, key=None, warn=True):
|
||||
|
|
0
tests/db/__init__.py
Normal file
0
tests/db/__init__.py
Normal file
38
tests/db/test_continuum.py
Normal file
38
tests/db/test_continuum.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.util import WebTestCase
|
||||
|
||||
from wuttaweb.db import continuum as mod
|
||||
|
||||
|
||||
class TestWuttaWebContinuumPlugin(WebTestCase):
|
||||
|
||||
def setUp(self):
|
||||
if not hasattr(mod, 'WuttaWebContinuumPlugin'):
|
||||
pytest.skip("test not relevant without sqlalchemy-continuum")
|
||||
self.setup_web()
|
||||
|
||||
def make_plugin(self):
|
||||
return mod.WuttaWebContinuumPlugin()
|
||||
|
||||
def test_get_remote_addr(self):
|
||||
plugin = self.make_plugin()
|
||||
|
||||
with patch.object(mod, 'get_current_request', return_value=None):
|
||||
self.assertIsNone(plugin.get_remote_addr(None, self.session))
|
||||
|
||||
self.request.client_addr = '127.0.0.1'
|
||||
self.assertEqual(plugin.get_remote_addr(None, self.session), '127.0.0.1')
|
||||
|
||||
def test_get_user_id(self):
|
||||
plugin = self.make_plugin()
|
||||
|
||||
with patch.object(mod, 'get_current_request', return_value=None):
|
||||
self.assertIsNone(plugin.get_user_id(None, self.session))
|
||||
|
||||
self.request.user = MagicMock(uuid='some-random-uuid')
|
||||
self.assertEqual(plugin.get_user_id(None, self.session), 'some-random-uuid')
|
|
@ -19,6 +19,9 @@ class TestObjectRefWidget(WebTestCase):
|
|||
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||
return deform.Field(node, **kwargs)
|
||||
|
||||
def make_widget(self, **kwargs):
|
||||
return mod.ObjectRefWidget(self.request, **kwargs)
|
||||
|
||||
def test_serialize(self):
|
||||
model = self.app.model
|
||||
person = model.Person(full_name="Betty Boop")
|
||||
|
@ -27,7 +30,7 @@ class TestObjectRefWidget(WebTestCase):
|
|||
|
||||
# standard (editable)
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
widget = mod.ObjectRefWidget(self.request)
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid)
|
||||
self.assertIn('<b-select ', html)
|
||||
|
@ -35,7 +38,7 @@ class TestObjectRefWidget(WebTestCase):
|
|||
# readonly
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
node.model_instance = person
|
||||
widget = mod.ObjectRefWidget(self.request)
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
|
@ -44,13 +47,26 @@ class TestObjectRefWidget(WebTestCase):
|
|||
# with hyperlink
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
node.model_instance = person
|
||||
widget = mod.ObjectRefWidget(self.request, url=lambda p: '/foo')
|
||||
widget = self.make_widget(url=lambda p: '/foo')
|
||||
field = self.make_field(node)
|
||||
html = widget.serialize(field, person.uuid, readonly=True)
|
||||
self.assertIn('Betty Boop', html)
|
||||
self.assertIn('<a', html)
|
||||
self.assertIn('href="/foo"', html)
|
||||
|
||||
def test_get_template_values(self):
|
||||
model = self.app.model
|
||||
person = model.Person(full_name="Betty Boop")
|
||||
self.session.add(person)
|
||||
self.session.commit()
|
||||
|
||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||
widget = self.make_widget()
|
||||
field = self.make_field(node)
|
||||
values = widget.get_template_values(field, person.uuid, {})
|
||||
self.assertIn('cstruct', values)
|
||||
self.assertNotIn('url', values)
|
||||
|
||||
|
||||
class TestFileDownloadWidget(WebTestCase):
|
||||
|
||||
|
|
|
@ -480,6 +480,27 @@ class TestGetModelFields(TestCase):
|
|||
fields = mod.get_model_fields(self.config, model.Setting)
|
||||
self.assertEqual(fields, ['name', 'value'])
|
||||
|
||||
def test_avoid_versions(self):
|
||||
model = self.app.model
|
||||
|
||||
mapper = MagicMock(iterate_properties = [
|
||||
MagicMock(key='uuid'),
|
||||
MagicMock(key='full_name'),
|
||||
MagicMock(key='first_name'),
|
||||
MagicMock(key='middle_name'),
|
||||
MagicMock(key='last_name'),
|
||||
MagicMock(key='versions'),
|
||||
])
|
||||
|
||||
with patch.object(mod, 'sa') as sa:
|
||||
sa.inspect.return_value = mapper
|
||||
|
||||
with patch.object(self.app, 'continuum_is_enabled', return_value=True):
|
||||
fields = mod.get_model_fields(self.config, model.Person)
|
||||
# nb. no versions field
|
||||
self.assertEqual(set(fields), set(['uuid', 'full_name', 'first_name',
|
||||
'middle_name', 'last_name']))
|
||||
|
||||
|
||||
class TestGetCsrfToken(TestCase):
|
||||
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -1,11 +1,14 @@
|
|||
|
||||
[tox]
|
||||
envlist = py38, py39, py310, py311
|
||||
envlist = py38, py39, py310, py311, nox
|
||||
|
||||
[testenv]
|
||||
extras = tests
|
||||
extras = continuum,tests
|
||||
commands = pytest {posargs}
|
||||
|
||||
[testenv:nox]
|
||||
extras = tests
|
||||
|
||||
[testenv:coverage]
|
||||
basepython = python3.11
|
||||
commands = pytest --cov=wuttaweb --cov-report=html --cov-fail-under=100
|
||||
|
|
Loading…
Reference in a new issue