Source code for wuttaweb.views.views

# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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__)


[docs] 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)
[docs] 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)