diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5196306..57295c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,23 +5,6 @@ All notable changes to wuttaweb will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## v0.27.0 (2025-12-31)
-
-### Feat
-
-- add wizard for generating new master view code
-- add basic MasterView to show all registered master views
-- add MasterView registry/discovery mechanism
-
-### Fix
-
-- show db backend (dialect name) on App Info page
-- prevent whitespace wrap for tool panel header
-- render datetimes with tooltip showing time delta from now
-- fallback to default continuum plugin logic, when no request
-- flush session when creating new object via MasterView
-- fix page title for Alembic Dashboard
-
## v0.26.0 (2025-12-28)
### Feat
diff --git a/docs/api/wuttaweb.views.views.rst b/docs/api/wuttaweb.views.views.rst
deleted file mode 100644
index ad26770..0000000
--- a/docs/api/wuttaweb.views.views.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-
-``wuttaweb.views.views``
-========================
-
-.. automodule:: wuttaweb.views.views
- :members:
diff --git a/docs/index.rst b/docs/index.rst
index c7bed0b..f910cc9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -74,7 +74,6 @@ the narrative docs are pretty scant. That will eventually change.
api/wuttaweb.views.tables
api/wuttaweb.views.upgrades
api/wuttaweb.views.users
- api/wuttaweb.views.views
Indices and tables
diff --git a/pyproject.toml b/pyproject.toml
index ec934fe..5e85363 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "WuttaWeb"
-version = "0.27.0"
+version = "0.26.0"
description = "Web App for Wutta Framework"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}]
@@ -45,7 +45,7 @@ dependencies = [
"SQLAlchemy-Utils",
"waitress",
"WebHelpers2",
- "WuttJamaican[db]>=0.28.1",
+ "WuttJamaican[db]>=0.28.0",
"zope.sqlalchemy>=1.5",
]
diff --git a/src/wuttaweb/app.py b/src/wuttaweb/app.py
index d6749b7..75d00f2 100644
--- a/src/wuttaweb/app.py
+++ b/src/wuttaweb/app.py
@@ -164,11 +164,6 @@ def make_pyramid_config(settings):
)
pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
- # add some more config magic
- pyramid_config.add_directive(
- "add_wutta_master_view", "wuttaweb.conf.add_master_view"
- )
-
return pyramid_config
diff --git a/src/wuttaweb/code-templates/new-master-view.mako b/src/wuttaweb/code-templates/new-master-view.mako
deleted file mode 100644
index 01fb4b7..0000000
--- a/src/wuttaweb/code-templates/new-master-view.mako
+++ /dev/null
@@ -1,116 +0,0 @@
-## -*- coding: utf-8; mode: python; -*-
-# -*- coding: utf-8; -*-
-"""
-Master view for ${model_title_plural}
-"""
-
-% if model_option == "model_class":
-from ${model_module} import ${model_name}
-% endif
-
-from wuttaweb.views import MasterView
-
-
-class ${class_name}(MasterView):
- """
- Master view for ${model_title_plural}
- """
- % if model_option == "model_class":
- model_class = ${model_name}
- % else:
- model_name = "${model_name}"
- % endif
- model_title = "${model_title}"
- model_title_plural = "${model_title_plural}"
-
- route_prefix = "${route_prefix}"
- % if permission_prefix != route_prefix:
- permission_prefix = "${permission_prefix}"
- % endif
- url_prefix = "${url_prefix}"
- % if template_prefix != url_prefix:
- template_prefix = "${template_prefix}"
- % endif
-
- % if not listable:
- listable = False
- % endif
- creatable = ${creatable}
- % if not viewable:
- viewable = ${viewable}
- % endif
- editable = ${editable}
- deletable = ${deletable}
-
- % if listable and model_option == "model_name":
- filterable = False
- sort_on_backend = False
- paginate_on_backend = False
- % endif
-
- % if grid_columns:
- grid_columns = [
- % for field in grid_columns:
- "${field}",
- % endfor
- ]
- % elif model_option == "model_name":
- # TODO: must specify grid columns before the list view will work:
- # grid_columns = [
- # "foo",
- # "bar",
- # ]
- % endif
-
- % if form_fields:
- form_fields = [
- % for field in form_fields:
- "${field}",
- % endfor
- ]
- % elif model_option == "model_name":
- # TODO: must specify form fields before create/view/edit/delete will work:
- # form_fields = [
- # "foo",
- # "bar",
- # ]
- % endif
-
- % if listable and model_option == "model_name":
- def get_grid_data(self, columns=None, session=None):
- data = []
-
- # TODO: you should return whatever data is needed for the grid.
- # it is expected to be a list of dicts, with keys corresponding
- # to grid columns.
- #
- # data = [
- # {"foo": 1, "bar": "abc"},
- # {"foo": 2, "bar": "def"},
- # ]
-
- return data
- % endif
-
- % if listable:
- def configure_grid(self, grid):
- g = grid
- super().configure_grid(g)
-
- # TODO: tweak grid however you need here
- #
- # g.set_label("foo", "FOO")
- # g.set_link("foo")
- # g.set_renderer("foo", self.render_special_field)
- % endif
-
-
-def defaults(config, **kwargs):
- base = globals()
-
- ${class_name} = kwargs.get('${class_name}', base['${class_name}'])
- ${class_name}.defaults(config)
-
-
-def includeme(config):
- defaults(config)
diff --git a/src/wuttaweb/conf.py b/src/wuttaweb/conf.py
index 61c038d..6d33fa6 100644
--- a/src/wuttaweb/conf.py
+++ b/src/wuttaweb/conf.py
@@ -31,8 +31,7 @@ class WuttaWebConfigExtension(WuttaConfigExtension):
"""
Config extension for WuttaWeb.
- This sets the default plugin used for SQLAlchemy-Continuum, to
- :class:`~wuttaweb.db.continuum.WuttaWebContinuumPlugin`. Which is
+ 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`.
"""
@@ -45,45 +44,3 @@ class WuttaWebConfigExtension(WuttaConfigExtension):
"wutta_continuum.wutta_plugin_spec",
"wuttaweb.db.continuum:WuttaWebContinuumPlugin",
)
-
-
-def add_master_view(config, master):
- """
- Pyramid directive to add the given ``MasterView`` subclass to the
- app's registry.
-
- This allows the app to dynamically present certain options for
- admin features etc.
-
- This is normally called automatically for all master views, within
- the :meth:`~wuttaweb.views.master.MasterView.defaults()` method.
-
- Should you need to call this yourself, do not call it directly but
- instead make a similar call via the Pyramid config object::
-
- pyramid_config.add_wutta_master_view(PoserWidgetView)
-
- :param config: Reference to the Pyramid config object.
-
- :param master: Reference to a
- :class:`~wuttaweb.views.master.MasterView` subclass.
-
- This function is involved in app startup; once that phase is
- complete you can inspect the master views like so::
-
- master_views = request.registry.settings["wuttaweb_master_views"]
-
- # find master views for given model class
- user_views = master_views.get(model.User, [])
-
- # some master views are registered by model name instead (if no class)
- email_views = master_views.get("email_setting", [])
- """
- key = master.get_model_class() or master.get_model_name()
-
- def action():
- master_views = config.get_settings().get("wuttaweb_master_views", {})
- master_views.setdefault(key, []).append(master)
- config.add_settings({"wuttaweb_master_views": master_views})
-
- config.action(None, action)
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index e7ba16d..72c4d79 100644
--- a/src/wuttaweb/forms/widgets.py
+++ b/src/wuttaweb/forms/widgets.py
@@ -274,7 +274,7 @@ class WuttaDateTimeWidget(DateTimeInputWidget):
if not cstruct:
return ""
dt = datetime.datetime.fromisoformat(cstruct)
- return self.app.render_datetime(dt, html=True)
+ return self.app.render_datetime(dt)
return super().serialize(field, cstruct, **kw)
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index 63829a4..76149f6 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -2041,7 +2041,7 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
grid.set_renderer('foo', 'datetime')
"""
dt = getattr(obj, key)
- return self.app.render_datetime(dt, html=True)
+ return self.app.render_datetime(dt)
def render_enum(self, obj, key, value, enum=None):
"""
diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako
index 54bcb10..137d8b1 100644
--- a/src/wuttaweb/templates/appinfo/index.mako
+++ b/src/wuttaweb/templates/appinfo/index.mako
@@ -19,9 +19,6 @@
${app.get_node_title()}
-
- ${config.appdb_engine.dialect.name}
-
${app.get_timezone_name()}
@@ -37,14 +34,6 @@
- % if request.has_perm("master_views.list"):
-
- % endif
-
% if request.has_perm("app_tables.list"):
%def>
diff --git a/src/wuttaweb/templates/form.mako b/src/wuttaweb/templates/form.mako
index 8a8ae48..a5ba1f4 100644
--- a/src/wuttaweb/templates/form.mako
+++ b/src/wuttaweb/templates/form.mako
@@ -49,10 +49,6 @@
<%def name="make_vue_components()">
${parent.make_vue_components()}
- ${self.make_vue_components_form()}
-%def>
-
-<%def name="make_vue_components_form()">
% if form is not Undefined:
${form.render_vue_finalize()}
% endif
diff --git a/src/wuttaweb/templates/tables/app/index.mako b/src/wuttaweb/templates/tables/app/index.mako
index e9a6c0a..cda3623 100644
--- a/src/wuttaweb/templates/tables/app/index.mako
+++ b/src/wuttaweb/templates/tables/app/index.mako
@@ -20,14 +20,6 @@
once />
% endif
- % if request.has_perm("master_views.list"):
-
- % endif
-
${parent.page_content()}
%def>
diff --git a/src/wuttaweb/templates/views/master/configure.mako b/src/wuttaweb/templates/views/master/configure.mako
deleted file mode 100644
index b354613..0000000
--- a/src/wuttaweb/templates/views/master/configure.mako
+++ /dev/null
@@ -1,31 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/configure.mako" />
-
-<%def name="form_content()">
-
- Basics
-
-
-
-
-
-
-
-
-
-
-%def>
-
-<%def name="modify_vue_vars()">
- ${parent.modify_vue_vars()}
-
-%def>
diff --git a/src/wuttaweb/templates/views/master/create.mako b/src/wuttaweb/templates/views/master/create.mako
deleted file mode 100644
index 4de295b..0000000
--- a/src/wuttaweb/templates/views/master/create.mako
+++ /dev/null
@@ -1,846 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/master/create.mako" />
-
-<%def name="extra_styles()">
- ${parent.extra_styles()}
-
-%def>
-
-## nb. no need for standard form here
-<%def name="render_vue_template_form()">%def>
-<%def name="make_vue_components_form()">%def>
-
-<%def name="page_content()">
-
-
-
-
-
- Choose Model
-
-
- You can choose a particular model, or just enter a name if the
- view needs to work with something outside the app database.
-
-
-
-
-
-
- Choose model from app database
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Provide just a model name
-
-
-
-
-
-
-
-
-
-
-
-
- This name will be used to suggest defaults for other class attributes.
-
-
-
- It is best to use a "singular Python variable name" style;
- for instance these are real examples:
-
-
-
- - app_table
- - email_setting
- - master_view
-
-
-
-
-
-
-
- Model looks good
-
-
- Skip
-
-
-
-
-
-
-
-
-
- Enter Details
-
-
-
-
- {{ modelName }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- List
- Create
- View
- Edit
- Delete
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Back
-
-
- Details look good
-
-
- Skip
-
-
-
-
-
-
-
- Write View
-
-
- This will create a new Python module with your view class definition.
-
-
-
-
-
-
-
- {{ className }}
-
-
-
- {{ modelClass || modelName }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ viewModuleDir }}
- /
-
-
-
-
-
- Overwrite file if it exists
-
-
-
-
-
-
-
-
-
- Back
-
-
- {{ writingViewFile ? "Working, please wait..." : "Write view class to file" }}
-
-
- Skip
-
-
-
-
-
-
- Confirm Route
-
-
-
-
- Code was generated to file:
-
-
-
-
- Review and modify code to your liking, then include the new
- view/module in your view config.
-
-
-
- Typical view config might be at:
-
-
-
-
- The view config should contain something like:
-
-
-
def includeme(config):
-
- # ..various things..
-
- config.include("{{ viewModulePath }}")
-
-
- Once you've done all that, the web app must be
- restarted. This may happen automatically depending on your
- setup. Test the route status below.
-
-
-
-
-
-
-
- At this point your new view/route should be present in the app. Test below.
-
-
-
-
-
-
-
-
-
-
-
-
- check not yet attempted
-
-
- checking route...
-
-
- {{ routeChecked }} found in app routes
-
-
- {{ routeChecked }} not found in app routes
-
-
-
-
-
-
-
-
-
-
-
- Check for Route
-
-
-
-
-
-
-
-
-
-
- Back
-
-
- Route looks good
-
-
- Skip
-
-
-
-
-
-
- Add to Menu
-
-
- You probably want to add a menu entry for the view, but it's optional.
-
-
-
- Edit the menu file:
-
-
-
-
- Add this entry wherever you like:
-
-
- {
- "title": "{{ modelTitlePlural }}",
- "route": "{{ routePrefix }}",
- "perm": "{{ permissionPrefix }}.list",
-}
-
-
- Occasionally an entry like this might also be useful:
-
-
- {
- "title": "New {{ modelTitle }}",
- "route": "{{ routePrefix }}.create",
- "perm": "{{ permissionPrefix }}.create",
-}
-
-
-
- Back
-
-
- Menu looks good
-
-
- Skip
-
-
-
-
-
-
- Grant Access
-
-
- You can grant access to each CRUD route, for any role(s) you like.
-
-
-
-
-
-
List {{ modelTitlePlural }}
-
-
- {{ role.name }}
-
-
-
-
-
-
Create {{ modelTitle }}
-
-
- {{ role.name }}
-
-
-
-
-
-
View {{ modelTitle }}
-
-
- {{ role.name }}
-
-
-
-
-
-
Edit {{ modelTitle }}
-
-
- {{ role.name }}
-
-
-
-
-
-
Delete {{ modelTitle }}
-
-
- {{ role.name }}
-
-
-
-
-
-
-
- Back
-
-
- {{ applyingPermissions ? "Working, please wait..." : "Apply these permissions" }}
-
-
- Skip
-
-
-
-
-
-
- Commit Code
-
-
- Hope you're having a great day.
-
-
-
- Don't forget to commit code changes to your source repo.
-
-
-
-
- Back
-
-
-
-
-
-%def>
-
-<%def name="modify_vue_vars()">
- ${parent.modify_vue_vars()}
-
-%def>
diff --git a/src/wuttaweb/templates/views/master/index.mako b/src/wuttaweb/templates/views/master/index.mako
deleted file mode 100644
index 7d4cd4d..0000000
--- a/src/wuttaweb/templates/views/master/index.mako
+++ /dev/null
@@ -1,17 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/master/index.mako" />
-
-<%def name="page_content()">
-
-
- % if request.has_perm("app_tables.list"):
-
- % endif
-
-
- ${parent.page_content()}
-%def>
diff --git a/src/wuttaweb/testing.py b/src/wuttaweb/testing.py
index 2d56066..b9986aa 100644
--- a/src/wuttaweb/testing.py
+++ b/src/wuttaweb/testing.py
@@ -75,9 +75,6 @@ class WebTestCase(DataTestCase):
self.pyramid_config.add_directive(
"add_wutta_permission", "wuttaweb.auth.add_permission"
)
- self.pyramid_config.add_directive(
- "add_wutta_master_view", "wuttaweb.conf.add_master_view"
- )
self.pyramid_config.add_subscriber(
"wuttaweb.subscribers.before_render", "pyramid.events.BeforeRender"
)
diff --git a/src/wuttaweb/views/essential.py b/src/wuttaweb/views/essential.py
index 9a839b2..3bc1bf5 100644
--- a/src/wuttaweb/views/essential.py
+++ b/src/wuttaweb/views/essential.py
@@ -40,7 +40,6 @@ That will in turn include the following modules:
* :mod:`wuttaweb.views.upgrades`
* :mod:`wuttaweb.views.tables`
* :mod:`wuttaweb.views.alembic`
-* :mod:`wuttaweb.views.views`
You can also selectively override some modules while keeping most
defaults.
@@ -78,7 +77,6 @@ def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
config.include(mod("wuttaweb.views.upgrades"))
config.include(mod("wuttaweb.views.tables"))
config.include(mod("wuttaweb.views.alembic"))
- config.include(mod("wuttaweb.views.views"))
def includeme(config): # pylint: disable=missing-function-docstring
diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py
index 2875f7c..f1920ed 100644
--- a/src/wuttaweb/views/master.py
+++ b/src/wuttaweb/views/master.py
@@ -25,6 +25,7 @@ Base Logic for Master Views
"""
# pylint: disable=too-many-lines
+import datetime
import logging
import os
import threading
@@ -1279,6 +1280,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
# issued_at
g.set_label("issued_at", "Changed")
+ g.set_renderer("issued_at", self.render_issued_at)
g.set_link("issued_at")
g.set_sort_defaults("issued_at", "desc")
@@ -1388,7 +1390,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
"instance_title": instance_title,
"instance_url": self.get_action_url("versions", instance),
"transaction": txn,
- "changed": self.app.render_datetime(txn.issued_at, html=True),
+ "changed": self.render_issued_at(txn, None, None),
"version_diffs": version_diffs,
"show_prev_next": True,
"prev_url": prev_url,
@@ -1419,6 +1421,14 @@ class MasterView(View): # pylint: disable=too-many-public-methods
.all()
)
+ def render_issued_at( # pylint: disable=missing-function-docstring,unused-argument
+ self, txn, key, value
+ ):
+ dt = txn.issued_at
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
+ dt = dt.astimezone(None)
+ return self.app.render_datetime(dt)
+
##############################
# autocomplete methods
##############################
@@ -2015,17 +2025,22 @@ class MasterView(View): # pylint: disable=too-many-public-methods
fmt = f"${{:0,.{scale}f}}"
return fmt.format(value)
- def grid_render_datetime( # pylint: disable=empty-docstring
- self, record, key, value, fmt=None
- ):
- """ """
- warnings.warn(
- "MasterView.grid_render_datetime() is deprecated; "
- "please use app.render_datetime() directly instead",
- DeprecationWarning,
- stacklevel=2,
- )
+ def grid_render_datetime(self, record, key, value, fmt=None):
+ """
+ Custom grid value renderer for
+ :class:`~python:datetime.datetime` fields.
+ :param fmt: Optional format string to use instead of the
+ default: ``'%Y-%m-%d %I:%M:%S %p'``
+
+ To use this feature for your grid::
+
+ grid.set_renderer('my_datetime_field', self.grid_render_datetime)
+
+ # you can also override format
+ grid.set_renderer('my_datetime_field', self.grid_render_datetime,
+ fmt='%Y-%m-%d %H:%M:%S')
+ """
# nb. get new value since the one provided will just be a
# (json-safe) *string* if the original type was datetime
value = record[key]
@@ -3840,9 +3855,6 @@ class MasterView(View): # pylint: disable=too-many-public-methods
model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
- # add to master view registry
- config.add_wutta_master_view(cls)
-
# permission group
config.add_wutta_permission_group(
permission_prefix, model_title_plural, overwrite=False
diff --git a/src/wuttaweb/views/reports.py b/src/wuttaweb/views/reports.py
index 1f91012..90ff204 100644
--- a/src/wuttaweb/views/reports.py
+++ b/src/wuttaweb/views/reports.py
@@ -48,7 +48,6 @@ class ReportView(MasterView): # pylint: disable=abstract-method
* ``/reports/XXX``
"""
- model_name = "report"
model_title = "Report"
model_key = "report_key"
filterable = False
diff --git a/src/wuttaweb/views/tables.py b/src/wuttaweb/views/tables.py
index e08a57c..611a97a 100644
--- a/src/wuttaweb/views/tables.py
+++ b/src/wuttaweb/views/tables.py
@@ -25,7 +25,6 @@ Table Views
"""
import os
-import sys
from alembic import command as alembic_command
from sqlalchemy_utils import get_mapper
@@ -45,12 +44,11 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
"""
Master view showing all tables in the :term:`app database`.
- Default route prefix is ``app_tables``.
+ Default route prefix is ``tables``.
Notable URLs provided by this class:
- * ``/tables/app/``
- * ``/tables/app/XXX``
+ * ``/tables/``
"""
# pylint: disable=duplicate-code
@@ -70,8 +68,6 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
labels = {
"name": "Table Name",
- "module_name": "Module",
- "module_file": "File",
}
grid_columns = [
@@ -85,11 +81,7 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
form_fields = [
"name",
"schema",
- "model_name",
- "description",
# "row_count",
- "module_name",
- "module_file",
]
has_rows = True
@@ -109,31 +101,6 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
"description",
]
- def normalize_table(self, table): # pylint: disable=missing-function-docstring
- record = {
- "name": table.name,
- "schema": table.schema or "",
- # "row_count": 42,
- }
-
- try:
- cls = get_mapper(table).class_
- except ValueError:
- pass
- else:
- record.update(
- {
- "model_class": cls,
- "model_name": cls.__name__,
- "model_name_dotted": f"{cls.__module__}.{cls.__name__}",
- "description": (cls.__doc__ or "").strip(),
- "module_name": cls.__module__,
- "module_file": sys.modules[cls.__module__].__file__,
- }
- )
-
- return record
-
def get_grid_data( # pylint: disable=empty-docstring
self, columns=None, session=None
):
@@ -142,7 +109,13 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
data = []
for table in model.Base.metadata.tables.values():
- data.append(self.normalize_table(table))
+ data.append(
+ {
+ "name": table.name,
+ "schema": table.schema or "",
+ # "row_count": 42,
+ }
+ )
return data
@@ -170,11 +143,12 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
name = self.request.matchdict["name"]
table = model.Base.metadata.tables[name]
-
- # nb. sometimes need the real table reference later when
- # dealing with an instance view
- data = self.normalize_table(table)
- data["table"] = table
+ data = {
+ "name": table.name,
+ "schema": table.schema or "",
+ # "row_count": 42,
+ "table": table,
+ }
self.__dict__["_cached_instance"] = data
@@ -184,57 +158,6 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
""" """
return instance["name"]
- def configure_form(self, form): # pylint: disable=empty-docstring
- """ """
- f = form
- super().configure_form(f)
-
- # description
- f.set_widget("description", "notes")
-
- def get_xref_buttons(self, obj):
- """
- By default this returns a list of buttons for each
- :class:`~wuttaweb.views.master.MasterView` subclass registered
- in the app for the current table model. Also a button to make
- a new Master View class, if permissions allow.
-
- See also parent method docs,
- :meth:`~wuttaweb.views.master.MasterView.get_xref_buttons()`
- """
- table = obj
- buttons = []
-
- # nb. we do not omit any buttons due to lack of permission
- # here. all buttons are shown for anyone seeing this page.
- # this is for sake of clarity so admin users are aware of what
- # is *possible* within the app etc.
- master_views = self.request.registry.settings.get("wuttaweb_master_views", {})
- model_views = master_views.get(table["model_class"], [])
- for view in model_views:
- buttons.append(
- self.make_button(
- view.get_model_title_plural(),
- primary=True,
- url=self.request.route_url(view.get_route_prefix()),
- icon_left="eye",
- )
- )
-
- # only add "new master view" button if user has perm
- if self.request.has_perm("master_views.create"):
- # nb. separate slightly from others
- buttons.append(HTML.tag("br"))
- buttons.append(
- self.make_button(
- "New Master View",
- url=self.request.route_url("master_views.create"),
- icon_left="plus",
- )
- )
-
- return buttons
-
def get_row_grid_data(self, obj): # pylint: disable=empty-docstring
""" """
table = obj
@@ -443,7 +366,6 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
cls._apptable_defaults(config)
cls._defaults(config)
- # pylint: disable=duplicate-code
@classmethod
def _apptable_defaults(cls, config):
route_prefix = cls.get_route_prefix()
@@ -470,8 +392,6 @@ class AppTableView(MasterView): # pylint: disable=abstract-method
permission=f"{permission_prefix}.create",
)
- # pylint: enable=duplicate-code
-
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
base = globals()
diff --git a/src/wuttaweb/views/upgrades.py b/src/wuttaweb/views/upgrades.py
index 34b2ed8..a4868b2 100644
--- a/src/wuttaweb/views/upgrades.py
+++ b/src/wuttaweb/views/upgrades.py
@@ -81,6 +81,9 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
# description
g.set_link("description")
+ # created
+ g.set_renderer("created", self.grid_render_datetime)
+
# created_by
g.set_link("created_by")
Creator = orm.aliased(model.User) # pylint: disable=invalid-name
@@ -93,6 +96,9 @@ class UpgradeView(MasterView): # pylint: disable=abstract-method
# status
g.set_renderer("status", self.grid_render_enum, enum=enum.UpgradeStatus)
+ # executed
+ g.set_renderer("executed", self.grid_render_datetime)
+
# executed_by
g.set_link("executed_by")
Executor = orm.aliased(model.User) # pylint: disable=invalid-name
diff --git a/src/wuttaweb/views/views.py b/src/wuttaweb/views/views.py
deleted file mode 100644
index 40e2d85..0000000
--- a/src/wuttaweb/views/views.py
+++ /dev/null
@@ -1,469 +0,0 @@
-# -*- coding: utf-8; -*-
-################################################################################
-#
-# wuttaweb -- Web App for Wutta Framework
-# Copyright © 2024-2025 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 of Views
-"""
-
-import importlib
-import logging
-import os
-import re
-import sys
-
-from mako.lookup import TemplateLookup
-
-from wuttaweb.views import MasterView
-from wuttaweb.util import get_model_fields
-
-
-log = logging.getLogger(__name__)
-
-
-class MasterViewView(MasterView): # pylint: disable=abstract-method
- """
- Master view which shows a list of all master views found in the
- app registry.
-
- Route prefix is ``master_views``; notable URLs provided by this
- class include:
-
- * ``/views/master/``
- """
-
- model_name = "master_view"
- model_title = "Master View"
- model_title_plural = "Master Views"
- url_prefix = "/views/master"
-
- filterable = False
- sortable = True
- sort_on_backend = False
- paginated = True
- paginate_on_backend = False
-
- creatable = True
- viewable = False # nb. it has a pseudo-view action instead
- editable = False
- deletable = False
- configurable = True
-
- labels = {
- "model_title_plural": "Title",
- "url_prefix": "URL Prefix",
- }
-
- grid_columns = [
- "model_title_plural",
- "model_name",
- "route_prefix",
- "url_prefix",
- ]
-
- sort_defaults = "model_title_plural"
-
- def get_grid_data( # pylint: disable=empty-docstring
- self, columns=None, session=None
- ):
- """ """
- data = []
-
- # nb. we do not omit any views due to lack of permission here.
- # all views are shown for anyone seeing this page. this is
- # for sake of clarity so admin users are aware of what is
- # *possible* within the app etc.
- master_views = self.request.registry.settings.get("wuttaweb_master_views", {})
- for model_views in master_views.values():
- for view in model_views:
- data.append(
- {
- "model_title_plural": view.get_model_title_plural(),
- "model_name": view.get_model_name(),
- "route_prefix": view.get_route_prefix(),
- "url_prefix": view.get_url_prefix(),
- }
- )
-
- return data
-
- def configure_grid(self, grid): # pylint: disable=empty-docstring
- """ """
- g = grid
- super().configure_grid(g)
-
- # nb. show more views by default
- g.pagesize = 50
-
- # nb. add "pseudo" View action
- def viewurl(view, i): # pylint: disable=unused-argument
- return self.request.route_url(view["route_prefix"])
-
- g.add_action("view", icon="eye", url=viewurl)
-
- # model_title_plural
- g.set_link("model_title_plural")
- g.set_searchable("model_title_plural")
-
- # model_name
- g.set_searchable("model_name")
-
- # route_prefix
- g.set_searchable("route_prefix")
-
- # url_prefix
- g.set_link("url_prefix")
- g.set_searchable("url_prefix")
-
- def get_template_context(self, context): # pylint: disable=empty-docstring
- """ """
- if self.creating:
- model = self.app.model
- session = self.Session()
-
- # app models
- app_models = []
- for name in dir(model):
- obj = getattr(model, name)
- if (
- isinstance(obj, type)
- and issubclass(obj, model.Base)
- and obj is not model.Base
- ):
- app_models.append(name)
- context["app_models"] = sorted(app_models)
-
- # view module location
- view_locations = self.get_view_module_options()
- modpath = self.config.get("wuttaweb.master_views.default_module_dir")
- if modpath not in view_locations:
- modpath = None
- if not modpath and len(view_locations) == 1:
- modpath = view_locations[0]
- context["view_module_dirs"] = view_locations
- context["view_module_dir"] = modpath
-
- # menu handler path
- web = self.app.get_web_handler()
- menu = web.get_menu_handler()
- context["menu_path"] = sys.modules[menu.__class__.__module__].__file__
-
- # roles for access
- roles = self.get_roles_for_access(session)
- context["roles"] = [
- {"uuid": role.uuid.hex, "name": role.name} for role in roles
- ]
- context["listing_roles"] = {role.uuid.hex: False for role in roles}
- context["creating_roles"] = {role.uuid.hex: False for role in roles}
- context["viewing_roles"] = {role.uuid.hex: False for role in roles}
- context["editing_roles"] = {role.uuid.hex: False for role in roles}
- context["deleting_roles"] = {role.uuid.hex: False for role in roles}
-
- return context
-
- def get_roles_for_access( # pylint: disable=missing-function-docstring
- self, session
- ):
- model = self.app.model
- auth = self.app.get_auth_handler()
- admin = auth.get_role_administrator(session)
- return (
- session.query(model.Role)
- .filter(model.Role.uuid != admin.uuid)
- .order_by(model.Role.name)
- .all()
- )
-
- def get_view_module_options(self): # pylint: disable=missing-function-docstring
- modules = set()
- master_views = self.request.registry.settings.get("wuttaweb_master_views", {})
- for model_views in master_views.values():
- for view in model_views:
- parent = ".".join(view.__module__.split(".")[:-1])
- modules.add(parent)
- return sorted(modules)
-
- def wizard_action(self): # pylint: disable=too-many-return-statements
- """
- AJAX view to handle various actions for the "new master view" wizard.
- """
- data = self.request.json_body
- action = data.get("action", "").strip()
- try:
- # nb. cannot use match/case statement until python 3.10, but this
- # project technically still supports python 3.8
- if action == "suggest_details":
- return self.suggest_details(data)
- if action == "write_view_file":
- return self.write_view_file(data)
- if action == "check_route":
- return self.check_route(data)
- if action == "apply_permissions":
- return self.apply_permissions(data)
- if action == "":
- return {"error": "Must specify the action to perform."}
- return {"error": f"Unknown action requested: {action}"}
-
- except Exception as err: # pylint: disable=broad-exception-caught
- log.exception("new master view wizard action failed: %s", action)
- return {"error": f"Unexpected error occurred: {err}"}
-
- def suggest_details( # pylint: disable=missing-function-docstring,too-many-locals
- self, data
- ):
- model = self.app.model
- model_name = data["model_name"]
-
- def make_normal(match):
- return "_" + match.group(1).lower()
-
- # normal is like: poser_widget
- normal = re.sub(r"([A-Z])", make_normal, model_name)
- normal = normal.lstrip("_")
-
- def make_title(match):
- return " " + match.group(1).upper()
-
- # title is like: Poser Widget
- title = re.sub(r"(?:^|_)([a-z])", make_title, normal)
- title = title.lstrip(" ")
-
- model_title = title
- model_title_plural = title + "s"
-
- def make_camel(match):
- return match.group(1).upper()
-
- # camel is like: PoserWidget
- camel = re.sub(r"(?:^|_)([a-z])", make_camel, normal)
-
- # fields are unknown without model class
- grid_columns = []
- form_fields = []
-
- if data["model_option"] == "model_class":
- model_class = getattr(model, model_name)
-
- # get model title from model class, if possible
- if hasattr(model_class, "__wutta_hint__"):
- model_title = model_class.__wutta_hint__.get("model_title", model_title)
- model_title_plural = model_class.__wutta_hint__.get(
- "model_title_plural", model_title + "s"
- )
-
- # get columns/fields from model class
- grid_columns = get_model_fields(self.config, model_class)
- form_fields = grid_columns
-
- # plural is like: poser_widgets
- plural = re.sub(r"(?:^| )([A-Z])", make_normal, model_title_plural)
- plural = plural.lstrip("_")
-
- route_prefix = plural
- url_prefix = "/" + (plural).replace("_", "-")
-
- return {
- "class_file_name": plural + ".py",
- "class_name": camel + "View",
- "model_name": model_name,
- "model_title": model_title,
- "model_title_plural": model_title_plural,
- "route_prefix": route_prefix,
- "permission_prefix": route_prefix,
- "url_prefix": url_prefix,
- "template_prefix": url_prefix,
- "grid_columns": "\n".join(grid_columns),
- "form_fields": "\n".join(form_fields),
- }
-
- def write_view_file(self, data): # pylint: disable=missing-function-docstring
- model = self.app.model
-
- # sort out the destination file path
- modpath = data["view_location"]
- if modpath:
- mod = importlib.import_module(modpath)
- file_path = os.path.join(
- os.path.dirname(mod.__file__), data["view_file_name"]
- )
- else:
- file_path = data["view_file_path"]
-
- # confirm file is writable
- if os.path.exists(file_path):
- if data["overwrite"]:
- os.remove(file_path)
- else:
- return {"error": "File already exists"}
-
- # guess its dotted module path
- modname, ext = os.path.splitext( # pylint: disable=unused-variable
- os.path.basename(file_path)
- )
- if modpath:
- modpath = f"{modpath}.{modname}"
- else:
- modpath = f"poser.web.views.{modname}"
-
- # inject module for class if needed
- if data["model_option"] == "model_class":
- model_class = getattr(model, data["model_name"])
- data["model_module"] = model_class.__module__
-
- # TODO: make templates dir configurable?
- view_templates = TemplateLookup(
- directories=[self.app.resource_path("wuttaweb:code-templates")]
- )
-
- # render template to file
- template = view_templates.get_template("/new-master-view.mako")
- content = template.render(**data)
- with open(file_path, "wt", encoding="utf_8") as f:
- f.write(content)
-
- return {
- "view_file_path": file_path,
- "view_module_path": modpath,
- "view_config_path": os.path.join(os.path.dirname(file_path), "__init__.py"),
- }
-
- def check_route(self, data): # pylint: disable=missing-function-docstring
- try:
- url = self.request.route_url(data["route"])
- path = self.request.route_path(data["route"])
- except Exception as err: # pylint: disable=broad-exception-caught
- return {"problem": self.app.render_error(err)}
-
- return {"url": url, "path": path}
-
- def apply_permissions( # pylint: disable=missing-function-docstring,too-many-branches
- self, data
- ):
- session = self.Session()
- auth = self.app.get_auth_handler()
- roles = self.get_roles_for_access(session)
- permission_prefix = data["permission_prefix"]
-
- if "listing_roles" in data:
- listing = data["listing_roles"]
- for role in roles:
- if listing.get(role.uuid.hex):
- auth.grant_permission(role, f"{permission_prefix}.list")
- else:
- auth.revoke_permission(role, f"{permission_prefix}.list")
-
- if "creating_roles" in data:
- creating = data["creating_roles"]
- for role in roles:
- if creating.get(role.uuid.hex):
- auth.grant_permission(role, f"{permission_prefix}.create")
- else:
- auth.revoke_permission(role, f"{permission_prefix}.create")
-
- if "viewing_roles" in data:
- viewing = data["viewing_roles"]
- for role in roles:
- if viewing.get(role.uuid.hex):
- auth.grant_permission(role, f"{permission_prefix}.view")
- else:
- auth.revoke_permission(role, f"{permission_prefix}.view")
-
- if "editing_roles" in data:
- editing = data["editing_roles"]
- for role in roles:
- if editing.get(role.uuid.hex):
- auth.grant_permission(role, f"{permission_prefix}.edit")
- else:
- auth.revoke_permission(role, f"{permission_prefix}.edit")
-
- if "deleting_roles" in data:
- deleting = data["deleting_roles"]
- for role in roles:
- if deleting.get(role.uuid.hex):
- auth.grant_permission(role, f"{permission_prefix}.delete")
- else:
- auth.revoke_permission(role, f"{permission_prefix}.delete")
-
- return {}
-
- def configure_get_simple_settings(self): # pylint: disable=empty-docstring
- """ """
- return [
- {"name": "wuttaweb.master_views.default_module_dir"},
- ]
-
- def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
- self, **kwargs
- ):
- """ """
- context = super().configure_get_context(**kwargs)
-
- context["view_module_locations"] = self.get_view_module_options()
-
- return context
-
- @classmethod
- def defaults(cls, config): # pylint: disable=empty-docstring
- """ """
- cls._masterview_defaults(config)
- cls._defaults(config)
-
- # pylint: disable=duplicate-code
- @classmethod
- def _masterview_defaults(cls, config):
- route_prefix = cls.get_route_prefix()
- permission_prefix = cls.get_permission_prefix()
- model_title_plural = cls.get_model_title_plural()
- url_prefix = cls.get_url_prefix()
-
- # fix permission group
- config.add_wutta_permission_group(
- permission_prefix, model_title_plural, overwrite=False
- )
-
- # wizard actions
- config.add_route(
- f"{route_prefix}.wizard_action",
- f"{url_prefix}/new/wizard-action",
- request_method="POST",
- )
- config.add_view(
- cls,
- attr="wizard_action",
- route_name=f"{route_prefix}.wizard_action",
- renderer="json",
- permission=f"{permission_prefix}.create",
- )
-
- # pylint: enable=duplicate-code
-
-
-def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
- base = globals()
-
- MasterViewView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
- "MasterViewView", base["MasterViewView"]
- )
- MasterViewView.defaults(config)
-
-
-def includeme(config): # pylint: disable=missing-function-docstring
- defaults(config)
diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py
index 818c38a..03799bd 100644
--- a/tests/forms/test_widgets.py
+++ b/tests/forms/test_widgets.py
@@ -207,8 +207,7 @@ class TestWuttaDateTimeWidget(WebTestCase):
# input data (from schema type) is always "local, zone-aware, isoformat"
dt = datetime.datetime(2024, 12, 12, 13, 49, tzinfo=tzlocal)
result = widget.serialize(field, dt.isoformat())
- self.assertTrue(result.startswith('")
- self.assertIn("New Master View", third)
- self.assertIn("http://example.com/views/master/new", third)
-
def test_get_row_grid_data(self):
model = self.app.model
view = self.make_view()
@@ -245,7 +203,6 @@ version_locations = wuttjamaican.db:alembic/versions
result = view.wizard_action()
self.assertIn("error", result)
self.assertEqual(result["error"], "File already exists")
- self.assertEqual(os.path.getsize(module_path), 0)
# but it can overwrite if requested
with patch.dict(sample, {"overwrite": True}):
diff --git a/tests/views/test_views.py b/tests/views/test_views.py
deleted file mode 100644
index 177a8b1..0000000
--- a/tests/views/test_views.py
+++ /dev/null
@@ -1,370 +0,0 @@
-# -*- coding: utf-8; -*-
-
-import os
-import sys
-from unittest.mock import patch
-
-from wuttaweb.testing import WebTestCase
-from wuttaweb.views import views as mod
-from wuttaweb.views.users import UserView
-
-
-class TestMasterViewView(WebTestCase):
-
- def make_view(self):
- return mod.MasterViewView(self.request)
-
- def test_includeme(self):
- self.pyramid_config.include("wuttaweb.views.views")
-
- def test_get_grid_data(self):
- view = self.make_view()
-
- # empty by default, since nothing registered in test setup
- data = view.get_grid_data()
- self.assertIsInstance(data, list)
- self.assertEqual(len(data), 0)
-
- # so let's register one and try again
- self.pyramid_config.add_wutta_master_view(UserView)
- data = view.get_grid_data()
- self.assertGreater(len(data), 0)
- master = data[0]
- self.assertIsInstance(master, dict)
- self.assertEqual(master["model_title_plural"], "Users")
- self.assertEqual(master["model_name"], "User")
- self.assertEqual(master["url_prefix"], "/users")
-
- def test_configure_grid(self):
- self.pyramid_config.add_route("users", "/users/")
- self.pyramid_config.add_wutta_master_view(UserView)
- view = self.make_view()
-
- # sanity / coverage check
- grid = view.make_grid(
- columns=["model_title_plural", "url_prefix"], data=view.get_grid_data()
- )
- view.configure_grid(grid)
-
- # nb. must invoke this to exercise the url logic
- grid.get_vue_context()
-
- def test_get_template_context(self):
- view = self.make_view()
- with patch.object(view, "Session", return_value=self.session):
-
- # normal view gets no extra context
- context = view.get_template_context({})
- self.assertIsInstance(context, dict)
- self.assertNotIn("app_models", context)
- self.assertNotIn("view_module_dirs", context)
- self.assertNotIn("view_module_dir", context)
- self.assertNotIn("menu_path", context)
- self.assertNotIn("roles", context)
- self.assertNotIn("listing_roles", context)
- self.assertNotIn("creating_roles", context)
- self.assertNotIn("viewing_roles", context)
- self.assertNotIn("editing_roles", context)
- self.assertNotIn("deleting_roles", context)
-
- # but 'create' view gets extra context
- with patch.object(view, "creating", new=True):
- context = view.get_template_context({})
- self.assertIsInstance(context, dict)
- self.assertIn("app_models", context)
- self.assertIn("view_module_dirs", context)
- self.assertIn("view_module_dir", context)
- self.assertIn("menu_path", context)
- self.assertIn("roles", context)
- self.assertIn("listing_roles", context)
- self.assertIn("creating_roles", context)
- self.assertIn("viewing_roles", context)
- self.assertIn("editing_roles", context)
- self.assertIn("deleting_roles", context)
-
- # try that again but this time make sure there is only
- # one possibility for view module path, which is auto
- # selected by default
- with patch.object(
- view, "get_view_module_options", return_value=["wuttaweb.views"]
- ):
- context = view.get_template_context({})
- self.assertEqual(context["view_module_dir"], "wuttaweb.views")
-
- def test_get_view_module_options(self):
- view = self.make_view()
-
- # register one master view, which should be reflected in options
- self.pyramid_config.add_wutta_master_view(UserView)
- options = view.get_view_module_options()
- self.assertEqual(len(options), 1)
- self.assertEqual(options[0], "wuttaweb.views")
-
- def test_suggest_details(self):
- view = self.make_view()
-
- # first test uses model_class
- sample = {
- "action": "suggest_details",
- "model_option": "model_class",
- "model_name": "Person",
- }
- with patch.object(self.request, "json_body", new=sample, create=True):
- result = view.wizard_action()
- self.assertEqual(result["class_file_name"], "people.py")
- self.assertEqual(result["class_name"], "PersonView")
- self.assertEqual(result["model_name"], "Person")
- self.assertEqual(result["model_title"], "Person")
- self.assertEqual(result["model_title_plural"], "People")
- self.assertEqual(result["route_prefix"], "people")
- self.assertEqual(result["permission_prefix"], "people")
- self.assertEqual(result["url_prefix"], "/people")
- self.assertEqual(result["template_prefix"], "/people")
- self.assertIn("grid_columns", result)
- self.assertIsInstance(result["grid_columns"], str)
- self.assertIn("form_fields", result)
- self.assertIsInstance(result["form_fields"], str)
-
- # second test uses model_name
- sample = {
- "action": "suggest_details",
- "model_option": "model_name",
- "model_name": "acme_brick",
- }
- with patch.object(self.request, "json_body", new=sample, create=True):
- result = view.wizard_action()
- self.assertEqual(result["class_file_name"], "acme_bricks.py")
- self.assertEqual(result["class_name"], "AcmeBrickView")
- self.assertEqual(result["model_name"], "acme_brick")
- self.assertEqual(result["model_title"], "Acme Brick")
- self.assertEqual(result["model_title_plural"], "Acme Bricks")
- self.assertEqual(result["route_prefix"], "acme_bricks")
- self.assertEqual(result["permission_prefix"], "acme_bricks")
- self.assertEqual(result["url_prefix"], "/acme-bricks")
- self.assertEqual(result["template_prefix"], "/acme-bricks")
- self.assertEqual(result["grid_columns"], "")
- self.assertEqual(result["form_fields"], "")
-
- def test_write_view_file(self):
- view = self.make_view()
- view_file_path = self.write_file("silly_things.py", "")
- wutta_file_path = os.path.join(
- os.path.dirname(sys.modules["wuttaweb.views"].__file__),
- "silly_things.py",
- )
- self.assertEqual(os.path.getsize(view_file_path), 0)
-
- # first test w/ Upgrade model_class and target file path
- sample = {
- "action": "write_view_file",
- "view_location": None,
- "view_file_path": view_file_path,
- "overwrite": False,
- "class_name": "UpgradeView",
- "model_option": "model_class",
- "model_name": "Upgrade",
- "model_title": "Upgrade",
- "model_title_plural": "Upgrades",
- "route_prefix": "upgrades",
- "permission_prefix": "upgrades",
- "url_prefix": "/upgrades",
- "template_prefix": "/upgrades",
- "listable": True,
- "creatable": True,
- "viewable": True,
- "editable": True,
- "deletable": True,
- "grid_columns": ["description", "created_by"],
- "form_fields": ["description", "created_by"],
- }
- with patch.object(self.request, "json_body", new=sample, create=True):
-
- # does not overwrite by default
- result = view.wizard_action()
- self.assertIn("error", result)
- self.assertEqual(result["error"], "File already exists")
- self.assertEqual(os.path.getsize(view_file_path), 0)
-
- # but can overwrite if requested
- with patch.dict(sample, {"overwrite": True}):
- result = view.wizard_action()
- self.assertNotIn("error", result)
- self.assertGreater(os.path.getsize(view_file_path), 1000)
- self.assertEqual(result["view_file_path"], view_file_path)
- self.assertEqual(
- result["view_module_path"], "poser.web.views.silly_things"
- )
-
- # reset file
- with open(view_file_path, "wb") as f:
- pass
- self.assertEqual(os.path.getsize(view_file_path), 0)
-
- # second test w/ silly_thing model_name and target module path
- sample = {
- "action": "write_view_file",
- "view_location": "wuttaweb.views",
- "view_file_name": "silly_things.py",
- "overwrite": False,
- "class_name": "SillyThingView",
- "model_option": "model_name",
- "model_name": "silly_thing",
- "model_title": "Silly Thing",
- "model_title_plural": "Silly Things",
- "route_prefix": "silly_things",
- "permission_prefix": "silly_things",
- "url_prefix": "/silly-things",
- "template_prefix": "/silly-things",
- "listable": True,
- "creatable": True,
- "viewable": True,
- "editable": True,
- "deletable": True,
- "grid_columns": ["id", "name", "description"],
- "form_fields": ["id", "name", "description"],
- }
- with patch.object(self.request, "json_body", new=sample, create=True):
-
- # file does not yet exist, so will be written
- result = view.wizard_action()
- self.assertNotIn("error", result)
- self.assertEqual(result["view_file_path"], wutta_file_path)
- self.assertGreater(os.path.getsize(wutta_file_path), 1000)
- self.assertEqual(os.path.getsize(view_file_path), 0)
- self.assertEqual(result["view_module_path"], "wuttaweb.views.silly_things")
-
- # once file exists, will not overwrite by default
- result = view.wizard_action()
- self.assertIn("error", result)
- self.assertEqual(result["error"], "File already exists")
- self.assertEqual(os.path.getsize(view_file_path), 0)
-
- # reset file
- with open(wutta_file_path, "wb") as f:
- pass
- self.assertEqual(os.path.getsize(wutta_file_path), 0)
-
- # can still overrwite explicitly
- with patch.dict(sample, {"overwrite": True}):
- result = view.wizard_action()
- self.assertNotIn("error", result)
- self.assertEqual(result["view_file_path"], wutta_file_path)
- self.assertGreater(os.path.getsize(wutta_file_path), 1000)
- self.assertEqual(os.path.getsize(view_file_path), 0)
- self.assertEqual(
- result["view_module_path"], "wuttaweb.views.silly_things"
- )
-
- # nb. must be sure to deleta that file!
- os.remove(wutta_file_path)
-
- def test_check_route(self):
- self.pyramid_config.add_route("people", "/people/")
- view = self.make_view()
- sample = {
- "action": "check_route",
- "route": "people",
- }
-
- with patch.object(self.request, "json_body", new=sample, create=True):
-
- # should get url and path
- result = view.wizard_action()
- self.assertEqual(result["url"], "http://example.com/people/")
- self.assertEqual(result["path"], "/people/")
- self.assertNotIn("problem", result)
-
- # unless we check a bad route
- with patch.dict(sample, {"route": "invalid_nothing_burger"}):
- result = view.wizard_action()
- self.assertIn("problem", result)
- self.assertNotIn("url", result)
- self.assertNotIn("path", result)
-
- def test_apply_permissions(self):
- model = self.app.model
- auth = self.app.get_auth_handler()
- admin = auth.get_role_administrator(self.session)
- known = auth.get_role_authenticated(self.session)
-
- manager = model.Role(name="Manager")
- self.session.add(manager)
-
- worker = model.Role(name="worker")
- self.session.add(worker)
-
- fred = model.User(username="fred")
- fred.roles.append(manager)
- fred.roles.append(worker)
- self.session.add(fred)
-
- self.session.commit()
-
- self.assertFalse(auth.has_permission(self.session, fred, "people.list"))
- self.assertFalse(auth.has_permission(self.session, fred, "people.create"))
- self.assertFalse(auth.has_permission(self.session, fred, "people.view"))
- self.assertFalse(auth.has_permission(self.session, fred, "people.edit"))
- self.assertFalse(auth.has_permission(self.session, fred, "people.delete"))
-
- view = self.make_view()
- with patch.object(view, "Session", return_value=self.session):
-
- sample = {
- "action": "apply_permissions",
- "permission_prefix": "people",
- "listing_roles": {known.uuid.hex: True},
- "creating_roles": {worker.uuid.hex: True},
- "viewing_roles": {known.uuid.hex: True},
- "editing_roles": {manager.uuid.hex: True},
- "deleting_roles": {manager.uuid.hex: True},
- }
- with patch.object(self.request, "json_body", new=sample, create=True):
-
- # nb. empty result is normal
- result = view.wizard_action()
- self.assertEqual(result, {})
-
- self.assertTrue(auth.has_permission(self.session, fred, "people.list"))
- self.assertTrue(
- auth.has_permission(self.session, fred, "people.create")
- )
- self.assertTrue(auth.has_permission(self.session, fred, "people.view"))
- self.assertTrue(auth.has_permission(self.session, fred, "people.edit"))
- self.assertTrue(
- auth.has_permission(self.session, fred, "people.delete")
- )
-
- def test_wizard_action(self):
- view = self.make_view()
-
- # missing action
- with patch.object(self.request, "json_body", create=True, new={}):
- result = view.wizard_action()
- self.assertIn("error", result)
- self.assertEqual(result["error"], "Must specify the action to perform.")
-
- # unknown action
- with patch.object(
- self.request, "json_body", create=True, new={"action": "nothing"}
- ):
- result = view.wizard_action()
- self.assertIn("error", result)
- self.assertEqual(result["error"], "Unknown action requested: nothing")
-
- # error invoking action
- with patch.object(
- self.request, "json_body", create=True, new={"action": "check_route"}
- ):
- with patch.object(view, "check_route", side_effect=RuntimeError("whoa")):
- result = view.wizard_action()
- self.assertIn("error", result)
- self.assertEqual(result["error"], "Unexpected error occurred: whoa")
-
- def test_configure(self):
- self.pyramid_config.add_route("home", "/")
- self.pyramid_config.add_route("login", "/auth/login")
- self.pyramid_config.add_route("master_views", "/views/master")
- view = self.make_view()
-
- # sanity/coverage
- view.configure()