WuttaWeb 0.20.0__tar.gz → 0.20.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/CHANGELOG.md +18 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/PKG-INFO +2 -2
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/pyproject.toml +5 -2
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/base.py +33 -5
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/filters.py +29 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/handler.py +75 -7
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/appinfo/configure.mako +17 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/base.mako +1 -1
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/grids/vue_template.mako +3 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/master.py +40 -12
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/settings.py +12 -4
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/grids/test_base.py +27 -1
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/grids/test_filters.py +24 -0
- wuttaweb-0.20.2/tests/test_handler.py +149 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_master.py +65 -34
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_reports.py +7 -2
- wuttaweb-0.20.0/tests/test_handler.py +0 -76
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/.gitignore +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/COPYING.txt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/README.md +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/Makefile +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/_static/.keepme +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.app.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.auth.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.cli.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.cli.webapp.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.conf.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.continuum.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.db.sess.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.emails.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.base.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.schema.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.forms.widgets.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.base.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.filters.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.grids.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.handler.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.helpers.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.menus.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.progress.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.static.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.subscribers.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.util.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.auth.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.base.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.batch.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.common.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.email.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.essential.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.master.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.people.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.progress.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.reports.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.roles.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.settings.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.upgrades.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/api/wuttaweb.views.users.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/conf.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/glossary.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/index.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/make.bat +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/cli/builtin.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/cli/index.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/base.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/index.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/lookup.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/docs/narr/templates/overview.rst +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/_version.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/app.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/auth.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/cli/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/cli/webapp.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/conf.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/continuum.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/db/sess.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/email-templates/feedback.html.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/email-templates/feedback.txt.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/emails.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/base.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/schema.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/forms/widgets.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/grids/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/helpers.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/menus.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/progress.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/favicon.ico +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/logo.png +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/static/img/testing.png +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/subscribers.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/appinfo/index.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/auth/change_password.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/auth/login.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/base_meta.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/batch/view.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/configure.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checkbox.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checkbox_choice.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/checked_password.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/dateinput.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/datetimeinput.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/moneyinput.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/password.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/permissions.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/checkbox.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/email_recips.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/filedownload.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/notes.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/objectref.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/permissions.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/readonly/rolerefs.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/select.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/textarea.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/deform/textinput.pt +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/email/settings/view.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/forbidden.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/form.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/forms/vue_template.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/grids/table_element.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/home.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/configure.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/create.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/delete.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/edit.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/form.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/index.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/master/view.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/notfound.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/page.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/people/view_profile.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/progress.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/reports/view.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/setup.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrade.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrades/configure.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/upgrades/view.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/templates/wutta-components.mako +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/testing.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/util.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/auth.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/base.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/batch.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/common.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/email.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/essential.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/people.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/progress.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/reports.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/roles.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/upgrades.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/src/wuttaweb/views/users.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tasks.py +0 -0
- {wuttaweb-0.20.0/tests/views → wuttaweb-0.20.2/tests}/__init__.py +0 -0
- {wuttaweb-0.20.0/tests/grids → wuttaweb-0.20.2/tests/cli}/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/cli/test_webapp.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/db/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/db/test_continuum.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_base.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_schema.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/forms/test_widgets.py +0 -0
- {wuttaweb-0.20.0/tests/cli → wuttaweb-0.20.2/tests/grids}/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_fontawesome_svg_core.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_free_solid_svg_icons.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga_bulma.css +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_oruga_bulma.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_vue.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/bb_vue_fontawesome.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/buefy.css +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/buefy.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/fontawesome.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/vue.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/libcache/vue_resource.js +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_app.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_auth.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_emails.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_helpers.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_menus.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_progress.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_static.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_subscribers.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/test_util.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/util.py +0 -0
- {wuttaweb-0.20.0/tests → wuttaweb-0.20.2/tests/views}/__init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test___init__.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_auth.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_base.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_batch.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_common.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_email.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_essential.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_people.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_progress.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_roles.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_settings.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_upgrades.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tests/views/test_users.py +0 -0
- {wuttaweb-0.20.0 → wuttaweb-0.20.2}/tox.ini +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to wuttaweb will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v0.20.2 (2025-01-14)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- improve support for composite `model_key` in MasterView
|
|
13
|
+
- let content header text be a bit longer
|
|
14
|
+
- add optional `target` attr for GridAction
|
|
15
|
+
- add `render_date()` method for grids
|
|
16
|
+
|
|
17
|
+
## v0.20.1 (2025-01-13)
|
|
18
|
+
|
|
19
|
+
### Fix
|
|
20
|
+
|
|
21
|
+
- expose setting to choose menu handler, in appinfo/configure
|
|
22
|
+
- use prop key instead of column name, for master view model key
|
|
23
|
+
- add grid filters specific to numeric, integer types
|
|
24
|
+
- use default value for config settings
|
|
25
|
+
|
|
8
26
|
## v0.20.0 (2025-01-11)
|
|
9
27
|
|
|
10
28
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: WuttaWeb
|
|
3
|
-
Version: 0.20.
|
|
3
|
+
Version: 0.20.2
|
|
4
4
|
Summary: Web App for Wutta Framework
|
|
5
5
|
Project-URL: Homepage, https://wuttaproject.org/
|
|
6
6
|
Project-URL: Repository, https://forgejo.wuttaproject.org/wutta/wuttaweb
|
|
@@ -39,7 +39,7 @@ Requires-Dist: pyramid-tm
|
|
|
39
39
|
Requires-Dist: pyramid>=2
|
|
40
40
|
Requires-Dist: waitress
|
|
41
41
|
Requires-Dist: webhelpers2
|
|
42
|
-
Requires-Dist: wuttjamaican[db]>=0.20.
|
|
42
|
+
Requires-Dist: wuttjamaican[db]>=0.20.1
|
|
43
43
|
Requires-Dist: zope-sqlalchemy>=1.5
|
|
44
44
|
Provides-Extra: continuum
|
|
45
45
|
Requires-Dist: wutta-continuum; extra == 'continuum'
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "WuttaWeb"
|
|
9
|
-
version = "0.20.
|
|
9
|
+
version = "0.20.2"
|
|
10
10
|
description = "Web App for Wutta Framework"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
|
|
@@ -44,7 +44,7 @@ dependencies = [
|
|
|
44
44
|
"pyramid_tm",
|
|
45
45
|
"waitress",
|
|
46
46
|
"WebHelpers2",
|
|
47
|
-
"WuttJamaican[db]>=0.20.
|
|
47
|
+
"WuttJamaican[db]>=0.20.1",
|
|
48
48
|
"zope.sqlalchemy>=1.5",
|
|
49
49
|
]
|
|
50
50
|
|
|
@@ -70,6 +70,9 @@ wuttaweb = "wuttaweb.conf:WuttaWebConfigExtension"
|
|
|
70
70
|
[project.entry-points."wutta.typer_imports"]
|
|
71
71
|
wuttaweb = "wuttaweb.cli"
|
|
72
72
|
|
|
73
|
+
[project.entry-points."wutta.web.menus"]
|
|
74
|
+
wuttaweb = "wuttaweb.menus:MenuHandler"
|
|
75
|
+
|
|
73
76
|
|
|
74
77
|
[project.urls]
|
|
75
78
|
Homepage = "https://wuttaproject.org/"
|
|
@@ -602,6 +602,7 @@ class Grid:
|
|
|
602
602
|
* ``'batch_id'`` -> :meth:`render_batch_id()`
|
|
603
603
|
* ``'boolean'`` -> :meth:`render_boolean()`
|
|
604
604
|
* ``'currency'`` -> :meth:`render_currency()`
|
|
605
|
+
* ``'date'`` -> :meth:`render_date()`
|
|
605
606
|
* ``'datetime'`` -> :meth:`render_datetime()`
|
|
606
607
|
* ``'quantity'`` -> :meth:`render_quantity()`
|
|
607
608
|
|
|
@@ -611,6 +612,7 @@ class Grid:
|
|
|
611
612
|
'batch_id': self.render_batch_id,
|
|
612
613
|
'boolean': self.render_boolean,
|
|
613
614
|
'currency': self.render_currency,
|
|
615
|
+
'date': self.render_date,
|
|
614
616
|
'datetime': self.render_datetime,
|
|
615
617
|
'quantity': self.render_quantity,
|
|
616
618
|
}
|
|
@@ -631,11 +633,13 @@ class Grid:
|
|
|
631
633
|
data type implies a default renderer. This is only possible
|
|
632
634
|
if :attr:`model_class` is set to a SQLAlchemy mapped class.
|
|
633
635
|
|
|
634
|
-
This only looks for a
|
|
636
|
+
This only looks for a few data types, and configures as
|
|
635
637
|
follows:
|
|
636
638
|
|
|
637
639
|
* :class:`sqlalchemy:sqlalchemy.types.Boolean` ->
|
|
638
640
|
:meth:`render_boolean()`
|
|
641
|
+
* :class:`sqlalchemy:sqlalchemy.types.Date` ->
|
|
642
|
+
:meth:`render_date()`
|
|
639
643
|
* :class:`sqlalchemy:sqlalchemy.types.DateTime` ->
|
|
640
644
|
:meth:`render_datetime()`
|
|
641
645
|
"""
|
|
@@ -651,7 +655,9 @@ class Grid:
|
|
|
651
655
|
prop = getattr(attr, 'prop', None)
|
|
652
656
|
if prop and isinstance(prop, orm.ColumnProperty):
|
|
653
657
|
column = prop.columns[0]
|
|
654
|
-
if isinstance(column.type, sa.
|
|
658
|
+
if isinstance(column.type, sa.Date):
|
|
659
|
+
self.set_renderer(key, self.render_date)
|
|
660
|
+
elif isinstance(column.type, sa.DateTime):
|
|
655
661
|
self.set_renderer(key, self.render_datetime)
|
|
656
662
|
elif isinstance(column.type, sa.Boolean):
|
|
657
663
|
self.set_renderer(key, self.render_boolean)
|
|
@@ -1820,7 +1826,7 @@ class Grid:
|
|
|
1820
1826
|
This may be used automatically per
|
|
1821
1827
|
:meth:`set_default_renderers()` or you can use it explicitly::
|
|
1822
1828
|
|
|
1823
|
-
grid.set_renderer('foo',
|
|
1829
|
+
grid.set_renderer('foo', 'boolean')
|
|
1824
1830
|
"""
|
|
1825
1831
|
return self.app.render_boolean(value)
|
|
1826
1832
|
|
|
@@ -1839,6 +1845,22 @@ class Grid:
|
|
|
1839
1845
|
"""
|
|
1840
1846
|
return self.app.render_currency(value, **kwargs)
|
|
1841
1847
|
|
|
1848
|
+
def render_date(self, obj, key, value):
|
|
1849
|
+
"""
|
|
1850
|
+
Column renderer for :class:`python:datetime.date` values.
|
|
1851
|
+
|
|
1852
|
+
This calls
|
|
1853
|
+
:meth:`~wuttjamaican:wuttjamaican.app.AppHandler.render_date()`
|
|
1854
|
+
for the return value.
|
|
1855
|
+
|
|
1856
|
+
This may be used automatically per
|
|
1857
|
+
:meth:`set_default_renderers()` or you can use it explicitly::
|
|
1858
|
+
|
|
1859
|
+
grid.set_renderer('foo', 'date')
|
|
1860
|
+
"""
|
|
1861
|
+
dt = getattr(obj, key)
|
|
1862
|
+
return self.app.render_date(dt)
|
|
1863
|
+
|
|
1842
1864
|
def render_datetime(self, obj, key, value):
|
|
1843
1865
|
"""
|
|
1844
1866
|
Column renderer for :class:`python:datetime.datetime` values.
|
|
@@ -1850,7 +1872,7 @@ class Grid:
|
|
|
1850
1872
|
This may be used automatically per
|
|
1851
1873
|
:meth:`set_default_renderers()` or you can use it explicitly::
|
|
1852
1874
|
|
|
1853
|
-
grid.set_renderer('foo',
|
|
1875
|
+
grid.set_renderer('foo', 'datetime')
|
|
1854
1876
|
"""
|
|
1855
1877
|
dt = getattr(obj, key)
|
|
1856
1878
|
return self.app.render_datetime(dt)
|
|
@@ -1865,7 +1887,7 @@ class Grid:
|
|
|
1865
1887
|
|
|
1866
1888
|
This is not used automatically but you can use it explicitly::
|
|
1867
1889
|
|
|
1868
|
-
grid.set_renderer('foo',
|
|
1890
|
+
grid.set_renderer('foo', 'quantity')
|
|
1869
1891
|
"""
|
|
1870
1892
|
return self.app.render_quantity(value)
|
|
1871
1893
|
|
|
@@ -2258,6 +2280,10 @@ class GridAction:
|
|
|
2258
2280
|
|
|
2259
2281
|
See also :meth:`get_url()`.
|
|
2260
2282
|
|
|
2283
|
+
.. attribute:: target
|
|
2284
|
+
|
|
2285
|
+
Optional ``target`` attribute for the ``<a>`` tag.
|
|
2286
|
+
|
|
2261
2287
|
.. attribute:: icon
|
|
2262
2288
|
|
|
2263
2289
|
Name of icon to be shown for the action link.
|
|
@@ -2275,6 +2301,7 @@ class GridAction:
|
|
|
2275
2301
|
key,
|
|
2276
2302
|
label=None,
|
|
2277
2303
|
url=None,
|
|
2304
|
+
target=None,
|
|
2278
2305
|
icon=None,
|
|
2279
2306
|
link_class=None,
|
|
2280
2307
|
):
|
|
@@ -2283,6 +2310,7 @@ class GridAction:
|
|
|
2283
2310
|
self.app = self.config.get_app()
|
|
2284
2311
|
self.key = key
|
|
2285
2312
|
self.url = url
|
|
2313
|
+
self.target = target
|
|
2286
2314
|
self.label = label or self.app.make_title(key)
|
|
2287
2315
|
self.icon = icon or key
|
|
2288
2316
|
self.link_class = link_class or ''
|
|
@@ -465,6 +465,33 @@ class StringAlchemyFilter(AlchemyFilter):
|
|
|
465
465
|
sa.and_(*criteria)))
|
|
466
466
|
|
|
467
467
|
|
|
468
|
+
class NumericAlchemyFilter(AlchemyFilter):
|
|
469
|
+
"""
|
|
470
|
+
SQLAlchemy filter option for a numeric data column.
|
|
471
|
+
|
|
472
|
+
Subclass of :class:`AlchemyFilter`.
|
|
473
|
+
"""
|
|
474
|
+
default_verbs = ['equal', 'not_equal',
|
|
475
|
+
'greater_than', 'greater_equal',
|
|
476
|
+
'less_than', 'less_equal']
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class IntegerAlchemyFilter(NumericAlchemyFilter):
|
|
480
|
+
"""
|
|
481
|
+
SQLAlchemy filter option for an integer data column.
|
|
482
|
+
|
|
483
|
+
Subclass of :class:`NumericAlchemyFilter`.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
def coerce_value(self, value):
|
|
487
|
+
""" """
|
|
488
|
+
if value:
|
|
489
|
+
try:
|
|
490
|
+
return int(value)
|
|
491
|
+
except:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
|
|
468
495
|
class BooleanAlchemyFilter(AlchemyFilter):
|
|
469
496
|
"""
|
|
470
497
|
SQLAlchemy filter option for a boolean data column.
|
|
@@ -568,6 +595,8 @@ default_sqlalchemy_filters = {
|
|
|
568
595
|
None: AlchemyFilter,
|
|
569
596
|
sa.String: StringAlchemyFilter,
|
|
570
597
|
sa.Text: StringAlchemyFilter,
|
|
598
|
+
sa.Numeric: NumericAlchemyFilter,
|
|
599
|
+
sa.Integer: IntegerAlchemyFilter,
|
|
571
600
|
sa.Boolean: BooleanAlchemyFilter,
|
|
572
601
|
sa.Date: DateAlchemyFilter,
|
|
573
602
|
}
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
Web Handler
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
+
import warnings
|
|
28
|
+
|
|
27
29
|
from wuttjamaican.app import GenericHandler
|
|
30
|
+
from wuttjamaican.util import load_entry_points
|
|
28
31
|
|
|
29
32
|
from wuttaweb import static, forms, grids
|
|
30
33
|
|
|
@@ -106,22 +109,87 @@ class WebHandler(GenericHandler):
|
|
|
106
109
|
|
|
107
110
|
def get_menu_handler(self, **kwargs):
|
|
108
111
|
"""
|
|
109
|
-
Get the configured
|
|
112
|
+
Get the configured :term:`menu handler` for the web app.
|
|
110
113
|
|
|
111
114
|
Specify a custom handler in your config file like this:
|
|
112
115
|
|
|
113
116
|
.. code-block:: ini
|
|
114
117
|
|
|
115
118
|
[wutta.web]
|
|
116
|
-
menus.
|
|
119
|
+
menus.handler.spec = poser.web.menus:PoserMenuHandler
|
|
117
120
|
|
|
118
121
|
:returns: Instance of :class:`~wuttaweb.menus.MenuHandler`.
|
|
119
122
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
spec = self.config.get(f'{self.appname}.web.menus.handler.spec')
|
|
124
|
+
if not spec:
|
|
125
|
+
spec = self.config.get(f'{self.appname}.web.menus.handler_spec')
|
|
126
|
+
if spec:
|
|
127
|
+
warnings.warn(f"setting '{self.appname}.web.menus.handler_spec' is deprecated; "
|
|
128
|
+
f"please use '{self.appname}.web.menus.handler_spec' instead",
|
|
129
|
+
DeprecationWarning)
|
|
130
|
+
else:
|
|
131
|
+
spec = self.config.get(f'{self.appname}.web.menus.handler.default_spec',
|
|
132
|
+
default='wuttaweb.menus:MenuHandler')
|
|
133
|
+
factory = self.app.load_object(spec)
|
|
134
|
+
return factory(self.config)
|
|
135
|
+
|
|
136
|
+
def get_menu_handler_specs(self, default=None):
|
|
137
|
+
"""
|
|
138
|
+
Get the :term:`spec` strings for all available :term:`menu
|
|
139
|
+
handlers <menu handler>`. See also
|
|
140
|
+
:meth:`get_menu_handler()`.
|
|
141
|
+
|
|
142
|
+
:param default: Default spec string(s) to include, even if not
|
|
143
|
+
registered. Can be a string or list of strings.
|
|
144
|
+
|
|
145
|
+
:returns: List of menu handler spec strings.
|
|
146
|
+
|
|
147
|
+
This will gather available spec strings from the following:
|
|
148
|
+
|
|
149
|
+
First, the ``default`` as provided by caller.
|
|
150
|
+
|
|
151
|
+
Second, the default spec from config, if set; for example:
|
|
152
|
+
|
|
153
|
+
.. code-block:: ini
|
|
154
|
+
|
|
155
|
+
[wutta.web]
|
|
156
|
+
menus.handler.default_spec = poser.web.menus:PoserMenuHandler
|
|
157
|
+
|
|
158
|
+
Third, each spec registered via entry points. For instance in
|
|
159
|
+
``pyproject.toml``:
|
|
160
|
+
|
|
161
|
+
.. code-block:: toml
|
|
162
|
+
|
|
163
|
+
[project.entry-points."wutta.web.menus"]
|
|
164
|
+
poser = "poser.web.menus:PoserMenuHandler"
|
|
165
|
+
|
|
166
|
+
The final list will be "sorted" according to the above, with
|
|
167
|
+
the latter registered handlers being sorted alphabetically.
|
|
168
|
+
"""
|
|
169
|
+
handlers = []
|
|
170
|
+
|
|
171
|
+
# defaults from caller
|
|
172
|
+
if isinstance(default, str):
|
|
173
|
+
handlers.append(default)
|
|
174
|
+
elif default:
|
|
175
|
+
handlers.extend(default)
|
|
176
|
+
|
|
177
|
+
# configured default, if applicable
|
|
178
|
+
default = self.config.get(f'{self.config.appname}.web.menus.handler.default_spec')
|
|
179
|
+
if default and default not in handlers:
|
|
180
|
+
handlers.append(default)
|
|
181
|
+
|
|
182
|
+
# registered via entry points
|
|
183
|
+
registered = []
|
|
184
|
+
for Handler in load_entry_points(f'{self.appname}.web.menus').values():
|
|
185
|
+
spec = Handler.get_spec()
|
|
186
|
+
if spec not in handlers:
|
|
187
|
+
registered.append(spec)
|
|
188
|
+
if registered:
|
|
189
|
+
registered.sort()
|
|
190
|
+
handlers.extend(registered)
|
|
191
|
+
|
|
192
|
+
return handlers
|
|
125
193
|
|
|
126
194
|
def make_form(self, request, **kwargs):
|
|
127
195
|
"""
|
|
@@ -41,6 +41,21 @@
|
|
|
41
41
|
</b-checkbox>
|
|
42
42
|
</b-field>
|
|
43
43
|
|
|
44
|
+
<b-field label="Menu Handler">
|
|
45
|
+
<input type="hidden"
|
|
46
|
+
name="${app.appname}.web.menus.handler.spec"
|
|
47
|
+
:value="simpleSettings['${app.appname}.web.menus.handler.spec']" />
|
|
48
|
+
<b-select v-model="simpleSettings['${app.appname}.web.menus.handler.spec']"
|
|
49
|
+
@input="settingsNeedSaved = true">
|
|
50
|
+
<option :value="null">(use default)</option>
|
|
51
|
+
<option v-for="handler in menuHandlers"
|
|
52
|
+
:key="handler.spec"
|
|
53
|
+
:value="handler.spec">
|
|
54
|
+
{{ handler.spec }}
|
|
55
|
+
</option>
|
|
56
|
+
</b-select>
|
|
57
|
+
</b-field>
|
|
58
|
+
|
|
44
59
|
</div>
|
|
45
60
|
|
|
46
61
|
<h3 class="block is-size-3">User/Auth</h3>
|
|
@@ -241,6 +256,8 @@
|
|
|
241
256
|
${parent.modify_vue_vars()}
|
|
242
257
|
<script>
|
|
243
258
|
|
|
259
|
+
ThisPageData.menuHandlers = ${json.dumps(menu_handlers)|n}
|
|
260
|
+
|
|
244
261
|
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
|
|
245
262
|
|
|
246
263
|
ThisPageData.editWebLibraryShowDialog = false
|
|
@@ -180,6 +180,9 @@
|
|
|
180
180
|
% for action in grid.actions:
|
|
181
181
|
<a v-if="props.row._action_url_${action.key}"
|
|
182
182
|
:href="props.row._action_url_${action.key}"
|
|
183
|
+
% if action.target:
|
|
184
|
+
target="${action.target}"
|
|
185
|
+
% endif
|
|
183
186
|
class="${action.link_class}">
|
|
184
187
|
${action.render_icon_and_label()}
|
|
185
188
|
</a>
|
|
@@ -1224,7 +1224,7 @@ class MasterView(View):
|
|
|
1224
1224
|
elif simple.get('type') is bool:
|
|
1225
1225
|
value = self.config.get_bool(name, default=simple.get('default', False))
|
|
1226
1226
|
else:
|
|
1227
|
-
value = self.config.get(name)
|
|
1227
|
+
value = self.config.get(name, default=simple.get('default'))
|
|
1228
1228
|
|
|
1229
1229
|
normalized[name] = value
|
|
1230
1230
|
|
|
@@ -2176,6 +2176,28 @@ class MasterView(View):
|
|
|
2176
2176
|
"""
|
|
2177
2177
|
return str(instance) or "(no title)"
|
|
2178
2178
|
|
|
2179
|
+
def get_action_route_kwargs(self, obj):
|
|
2180
|
+
"""
|
|
2181
|
+
Get a dict of route kwargs for the given object.
|
|
2182
|
+
|
|
2183
|
+
This is called from :meth:`get_action_url()` and must return
|
|
2184
|
+
kwargs suitable for use with ``request.route_url()``.
|
|
2185
|
+
|
|
2186
|
+
In practice this should return a dict which has keys for each
|
|
2187
|
+
field from :meth:`get_model_key()` and values which come from
|
|
2188
|
+
the object.
|
|
2189
|
+
|
|
2190
|
+
:param obj: Model instance object.
|
|
2191
|
+
|
|
2192
|
+
:returns: The dict of route kwargs for the object.
|
|
2193
|
+
"""
|
|
2194
|
+
try:
|
|
2195
|
+
return dict([(key, obj[key])
|
|
2196
|
+
for key in self.get_model_key()])
|
|
2197
|
+
except TypeError:
|
|
2198
|
+
return dict([(key, getattr(obj, key))
|
|
2199
|
+
for key in self.get_model_key()])
|
|
2200
|
+
|
|
2179
2201
|
def get_action_url(self, action, obj, **kwargs):
|
|
2180
2202
|
"""
|
|
2181
2203
|
Generate an "action" URL for the given model instance.
|
|
@@ -2183,22 +2205,21 @@ class MasterView(View):
|
|
|
2183
2205
|
This is a shortcut which generates a route name based on
|
|
2184
2206
|
:meth:`get_route_prefix()` and the ``action`` param.
|
|
2185
2207
|
|
|
2186
|
-
It
|
|
2187
|
-
|
|
2208
|
+
It calls :meth:`get_action_route_kwargs()` and then passes
|
|
2209
|
+
those along with route name to ``request.route_url()``, and
|
|
2210
|
+
returns the result.
|
|
2188
2211
|
|
|
2189
2212
|
:param action: String name for the action, which corresponds
|
|
2190
2213
|
to part of some named route, e.g. ``'view'`` or ``'edit'``.
|
|
2191
2214
|
|
|
2192
2215
|
:param obj: Model instance object.
|
|
2216
|
+
|
|
2217
|
+
:param \**kwargs: Additional kwargs to be passed to
|
|
2218
|
+
``request.route_url()``, if needed.
|
|
2193
2219
|
"""
|
|
2194
|
-
|
|
2195
|
-
try:
|
|
2196
|
-
kw = dict([(key, obj[key])
|
|
2197
|
-
for key in self.get_model_key()])
|
|
2198
|
-
except TypeError:
|
|
2199
|
-
kw = dict([(key, getattr(obj, key))
|
|
2200
|
-
for key in self.get_model_key()])
|
|
2220
|
+
kw = self.get_action_route_kwargs(obj)
|
|
2201
2221
|
kw.update(kwargs)
|
|
2222
|
+
route_prefix = self.get_route_prefix()
|
|
2202
2223
|
return self.request.route_url(f'{route_prefix}.{action}', **kw)
|
|
2203
2224
|
|
|
2204
2225
|
def get_action_url_view(self, obj, i):
|
|
@@ -2707,6 +2728,9 @@ class MasterView(View):
|
|
|
2707
2728
|
represents a Wutta-based SQLAlchemy model, the return value
|
|
2708
2729
|
for this method is: ``('uuid',)``
|
|
2709
2730
|
|
|
2731
|
+
Any class mapped via SQLAlchemy should be supported
|
|
2732
|
+
automatically, the keys are determined from class inspection.
|
|
2733
|
+
|
|
2710
2734
|
But there is no "sane" default for other scenarios, in which
|
|
2711
2735
|
case subclass should define :attr:`model_key`. If the model
|
|
2712
2736
|
key cannot be determined, raises ``AttributeError``.
|
|
@@ -2721,8 +2745,12 @@ class MasterView(View):
|
|
|
2721
2745
|
|
|
2722
2746
|
model_class = cls.get_model_class()
|
|
2723
2747
|
if model_class:
|
|
2724
|
-
|
|
2725
|
-
|
|
2748
|
+
# nb. we want the primary key but must avoid column names
|
|
2749
|
+
# in case mapped class uses different prop keys
|
|
2750
|
+
inspector = sa.inspect(model_class)
|
|
2751
|
+
keys = [col.name for col in inspector.primary_key]
|
|
2752
|
+
return tuple([prop.key for prop in inspector.column_attrs
|
|
2753
|
+
if all([col.name in keys for col in prop.columns])])
|
|
2726
2754
|
|
|
2727
2755
|
raise AttributeError(f"you must define model_key for view class: {cls}")
|
|
2728
2756
|
|
|
@@ -129,6 +129,10 @@ class AppInfoView(MasterView):
|
|
|
129
129
|
{'name': f'{self.config.appname}.node_title'},
|
|
130
130
|
{'name': f'{self.config.appname}.production',
|
|
131
131
|
'type': bool},
|
|
132
|
+
{'name': f'{self.config.appname}.web.menus.handler.spec'},
|
|
133
|
+
# nb. this is deprecated; we define so it is auto-deleted
|
|
134
|
+
# when we replace with newer setting
|
|
135
|
+
{'name': f'{self.config.appname}.web.menus.handler_spec'},
|
|
132
136
|
|
|
133
137
|
# user/auth
|
|
134
138
|
{'name': 'wuttaweb.home_redirect_to_login',
|
|
@@ -164,11 +168,15 @@ class AppInfoView(MasterView):
|
|
|
164
168
|
|
|
165
169
|
def configure_get_context(self, **kwargs):
|
|
166
170
|
""" """
|
|
167
|
-
|
|
168
|
-
# normal context
|
|
169
171
|
context = super().configure_get_context(**kwargs)
|
|
170
172
|
|
|
171
|
-
#
|
|
173
|
+
# add registered menu handlers
|
|
174
|
+
web = self.app.get_web_handler()
|
|
175
|
+
handlers = web.get_menu_handler_specs()
|
|
176
|
+
handlers = [{'spec': spec} for spec in handlers]
|
|
177
|
+
context['menu_handlers'] = handlers
|
|
178
|
+
|
|
179
|
+
# add `weblibs` to context, based on config values
|
|
172
180
|
weblibs = self.get_weblibs()
|
|
173
181
|
for key in weblibs:
|
|
174
182
|
title = weblibs[key]
|
|
@@ -192,8 +200,8 @@ class AppInfoView(MasterView):
|
|
|
192
200
|
'live_url': get_liburl(self.request, key,
|
|
193
201
|
prefix=self.weblib_config_prefix),
|
|
194
202
|
}
|
|
195
|
-
|
|
196
203
|
context['weblibs'] = list(weblibs.values())
|
|
204
|
+
|
|
197
205
|
return context
|
|
198
206
|
|
|
199
207
|
|
|
@@ -213,7 +213,7 @@ class TestGrid(WebTestCase):
|
|
|
213
213
|
obj = MagicMock(foo=42.00)
|
|
214
214
|
self.assertEqual(grid.renderers['foo'](obj, 'foo', 42.00), '42')
|
|
215
215
|
|
|
216
|
-
def
|
|
216
|
+
def test_set_default_renderers(self):
|
|
217
217
|
model = self.app.model
|
|
218
218
|
|
|
219
219
|
# no defaults for "plain" schema
|
|
@@ -249,6 +249,18 @@ class TestGrid(WebTestCase):
|
|
|
249
249
|
self.assertIn('executing', grid.renderers)
|
|
250
250
|
self.assertIs(grid.renderers['executing'], myrender)
|
|
251
251
|
|
|
252
|
+
# nb. as of writing we have no Date columns in default schema,
|
|
253
|
+
# so must invent one to test that type
|
|
254
|
+
class SomeFoolery(model.Base):
|
|
255
|
+
__tablename__ = 'somefoolery'
|
|
256
|
+
id = sa.Column(sa.Integer(), primary_key=True)
|
|
257
|
+
created = sa.Column(sa.Date())
|
|
258
|
+
|
|
259
|
+
# renderer set for date mapped field
|
|
260
|
+
grid = self.make_grid(model_class=SomeFoolery)
|
|
261
|
+
self.assertIn('created', grid.renderers)
|
|
262
|
+
self.assertIsNot(grid.renderers['created'], myrender)
|
|
263
|
+
|
|
252
264
|
def test_linked_columns(self):
|
|
253
265
|
grid = self.make_grid(columns=['foo', 'bar'])
|
|
254
266
|
self.assertEqual(grid.linked_columns, [])
|
|
@@ -1415,6 +1427,20 @@ class TestGrid(WebTestCase):
|
|
|
1415
1427
|
# zero is *not* empty string (with this renderer)
|
|
1416
1428
|
self.assertEqual(grid.render_quantity(obj, 'foo', 0), "0")
|
|
1417
1429
|
|
|
1430
|
+
def test_render_date(self):
|
|
1431
|
+
grid = self.make_grid(columns=['foo', 'bar'])
|
|
1432
|
+
|
|
1433
|
+
# null
|
|
1434
|
+
obj = MagicMock(dt=None)
|
|
1435
|
+
result = grid.render_date(obj, 'dt', None)
|
|
1436
|
+
self.assertIsNone(result)
|
|
1437
|
+
|
|
1438
|
+
# typical
|
|
1439
|
+
dt = datetime.date(2025, 1, 13)
|
|
1440
|
+
obj = MagicMock(dt=dt)
|
|
1441
|
+
result = grid.render_date(obj, 'dt', str(dt))
|
|
1442
|
+
self.assertEqual(result, '2025-01-13')
|
|
1443
|
+
|
|
1418
1444
|
def test_render_datetime(self):
|
|
1419
1445
|
grid = self.make_grid(columns=['foo', 'bar'])
|
|
1420
1446
|
|
|
@@ -326,6 +326,30 @@ class TestStringAlchemyFilter(WebTestCase):
|
|
|
326
326
|
self.assertEqual(filtered_query.count(), 6)
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
class TestIntegerAlchemyFilter(WebTestCase):
|
|
330
|
+
|
|
331
|
+
def make_filter(self, model_property, **kwargs):
|
|
332
|
+
factory = kwargs.pop('factory', mod.IntegerAlchemyFilter)
|
|
333
|
+
kwargs['model_property'] = model_property
|
|
334
|
+
return factory(self.request, model_property.key, **kwargs)
|
|
335
|
+
|
|
336
|
+
def test_coerce_value(self):
|
|
337
|
+
model = self.app.model
|
|
338
|
+
filtr = self.make_filter(model.Upgrade.exit_code)
|
|
339
|
+
|
|
340
|
+
# null
|
|
341
|
+
self.assertIsNone(filtr.coerce_value(None))
|
|
342
|
+
self.assertIsNone(filtr.coerce_value(''))
|
|
343
|
+
|
|
344
|
+
# typical
|
|
345
|
+
self.assertEqual(filtr.coerce_value('42'), 42)
|
|
346
|
+
self.assertEqual(filtr.coerce_value('-42'), -42)
|
|
347
|
+
|
|
348
|
+
# invalid
|
|
349
|
+
self.assertIsNone(filtr.coerce_value('42.12'))
|
|
350
|
+
self.assertIsNone(filtr.coerce_value('bogus'))
|
|
351
|
+
|
|
352
|
+
|
|
329
353
|
class TestBooleanAlchemyFilter(WebTestCase):
|
|
330
354
|
|
|
331
355
|
def setUp(self):
|