Source code for wuttaweb.views.settings

# -*- coding: utf-8; -*-
################################################################################
#
#  wuttaweb -- Web App for Wutta Framework
#  Copyright © 2024-2026 Lance Edgar
#
#  This file is part of Wutta Framework.
#
#  Wutta Framework is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  Wutta Framework is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for app settings
"""

import datetime
import json
import os
import sys
import subprocess
from collections import OrderedDict

from wuttjamaican.db.model import Setting
from wuttjamaican.util import get_timezone_by_name
from wuttaweb.views import MasterView
from wuttaweb.util import get_libver, get_liburl


[docs] class AppInfoView(MasterView): # pylint: disable=abstract-method """ Master view for the core app info, to show/edit config etc. Default route prefix is ``appinfo``. Notable URLs provided by this class: * ``/appinfo/`` * ``/appinfo/configure`` See also :class:`SettingView`. """ model_name = "AppInfo" model_title_plural = "App Info" route_prefix = "appinfo" filterable = False sort_on_backend = False sort_defaults = "name" paginated = False creatable = False viewable = False editable = False deletable = False configurable = True grid_columns = [ "name", "version", "editable_project_location", ] # TODO: for tailbone backward compat with get_liburl() etc. weblib_config_prefix = None def get_template_context(self, context): # pylint: disable=empty-docstring """ """ if self.listing: context["appinfo"] = self.get_appinfo_dict() return context def get_appinfo_dict(self): # pylint: disable=missing-function-docstring appinfo = OrderedDict( [ ( "distribution", { "label": "Distribution", "value": self.app.get_distribution() or f"?? - set config for `{self.app.appname}.app_dist`", }, ), ( "version", { "label": "Version", "value": self.app.get_version() or f"?? - set config for `{self.app.appname}.app_dist`", }, ), ( "app_title", { "label": "App Title", "value": self.app.get_title(), }, ), ( "node_type", { "label": "Node Type", "value": self.app.get_node_type(), }, ), ( "node_title", { "label": "Node Title", "value": self.app.get_node_title(), }, ), ( "db_backend", { "label": "DB Backend", "value": self.config.appdb_engine.dialect.name, }, ), ( "timezone", { "label": "Timezone", "value": self.app.get_timezone_name(), }, ), ( "production", { "label": "Production Mode", "value": "Yes" if self.config.production() else "No", }, ), ( "email_enabled", { "label": "Email Enabled", "value": ( "Yes" if self.app.get_email_handler().sending_is_enabled() else "No" ), }, ), ] ) if not appinfo["node_type"]["value"]: del appinfo["node_type"] if appinfo["app_title"]["value"] == appinfo["node_title"]["value"]: del appinfo["node_title"] return appinfo def get_grid_data( # pylint: disable=empty-docstring self, columns=None, session=None ): """ """ # nb. init with empty data, only load it upon user request if not self.request.GET.get("partial"): return [] # TODO: pretty sure this is not cross-platform. probably some # sort of pip methods belong on the app handler? or it should # have a pip handler for all that? pip = os.path.join(sys.prefix, "bin", "pip") output = subprocess.check_output([pip, "list", "--format=json"], text=True) data = json.loads(output.strip()) # must avoid null values for sort to work right for pkg in data: pkg.setdefault("editable_project_location", "") return data def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ g = grid super().configure_grid(g) g.sort_multiple = False # name g.set_searchable("name") # editable_project_location g.set_searchable("editable_project_location") def get_weblibs(self): # pylint: disable=empty-docstring """ """ return OrderedDict( [ ("vue", "(Vue2) Vue"), ("vue_resource", "(Vue2) vue-resource"), ("buefy", "(Vue2) Buefy"), ("buefy_css", "(Vue2) Buefy CSS"), ("fontawesome", "(Vue2) FontAwesome"), ("bb_vue", "(Vue3) vue"), ("bb_oruga", "(Vue3) @oruga-ui/oruga-next"), ("bb_oruga_bulma", "(Vue3) @oruga-ui/theme-bulma (JS)"), ("bb_oruga_bulma_css", "(Vue3) @oruga-ui/theme-bulma (CSS)"), ("bb_fontawesome_svg_core", "(Vue3) @fortawesome/fontawesome-svg-core"), ("bb_free_solid_svg_icons", "(Vue3) @fortawesome/free-solid-svg-icons"), ("bb_vue_fontawesome", "(Vue3) @fortawesome/vue-fontawesome"), ] ) def configure_get_simple_settings(self): # pylint: disable=empty-docstring """ """ simple_settings = [ # basics {"name": f"{self.config.appname}.app_title"}, {"name": f"{self.config.appname}.node_title"}, {"name": f"{self.config.appname}.production", "type": bool}, {"name": "wuttaweb.themes.expose_picker", "type": bool}, {"name": f"{self.config.appname}.timezone.default"}, {"name": f"{self.config.appname}.web.menus.handler.spec"}, # nb. this is deprecated; we define so it is auto-deleted # when we replace with newer setting {"name": f"{self.config.appname}.web.menus.handler_spec"}, # user/auth {"name": "wuttaweb.home_redirect_to_login", "type": bool, "default": False}, # email { "name": f"{self.config.appname}.mail.send_emails", "type": bool, "default": False, }, {"name": f"{self.config.appname}.email.default.sender"}, {"name": f"{self.config.appname}.email.default.subject"}, {"name": f"{self.config.appname}.email.default.to"}, {"name": f"{self.config.appname}.email.feedback.subject"}, {"name": f"{self.config.appname}.email.feedback.to"}, # grids {"name": "wuttaweb.grids.default_pagesize", "type": int}, ] def getval(key): return self.config.get(f"wuttaweb.{key}") weblibs = self.get_weblibs() for key in weblibs: simple_settings.append( { "name": f"wuttaweb.libver.{key}", "default": getval(f"libver.{key}"), } ) simple_settings.append( { "name": f"wuttaweb.liburl.{key}", "default": getval(f"liburl.{key}"), } ) return simple_settings
[docs] def configure_check_timezone(self): """ AJAX view to validate a user-specified timezone name. Route name for this is: ``appinfo.check_timezone`` """ tzname = self.request.GET.get("tzname") if not tzname: return {"invalid": "Must provide 'tzname' parameter."} try: get_timezone_by_name(tzname) return {"invalid": False} except Exception as err: # pylint: disable=broad-exception-caught return {"invalid": str(err)}
def configure_get_context( # pylint: disable=empty-docstring,arguments-differ self, **kwargs ): """ """ context = super().configure_get_context(**kwargs) # default system timezone dt = datetime.datetime.now().astimezone() context["default_timezone"] = dt.tzname() # add registered menu handlers web = self.app.get_web_handler() handlers = web.get_menu_handler_specs() handlers = [{"spec": spec} for spec in handlers] context["menu_handlers"] = handlers # add pagesize options g = self.make_grid() context["grid_pagesize_options"] = g.get_pagesize_options() context["grid_pagesize_default"] = g.get_pagesize() # add `weblibs` to context, based on config values weblibs = self.get_weblibs() for key in weblibs: title = weblibs[key] weblibs[key] = { "key": key, "title": title, # nb. these values are exactly as configured, and are # used for editing the settings "configured_version": get_libver( self.request, key, prefix=self.weblib_config_prefix, configured_only=True, ), "configured_url": get_liburl( self.request, key, prefix=self.weblib_config_prefix, configured_only=True, ), # nb. these are for display only "default_version": get_libver( self.request, key, prefix=self.weblib_config_prefix, default_only=True, ), "live_url": get_liburl( self.request, key, prefix=self.weblib_config_prefix ), } context["weblibs"] = list(weblibs.values()) return context @classmethod def defaults(cls, config): # pylint: disable=empty-docstring """ """ cls._defaults(config) cls._appinfo_defaults(config) @classmethod def _appinfo_defaults(cls, config): route_prefix = cls.get_route_prefix() permission_prefix = cls.get_permission_prefix() url_prefix = cls.get_url_prefix() # check timezone config.add_route( f"{route_prefix}.check_timezone", f"{url_prefix}/check-timezone", request_method="GET", ) config.add_view( cls, attr="configure_check_timezone", route_name=f"{route_prefix}.check_timezone", permission=f"{permission_prefix}.configure", renderer="json", )
[docs] class SettingView(MasterView): # pylint: disable=abstract-method """ Master view for the "raw" settings table. Default route prefix is ``settings``. Notable URLs provided by this class: * ``/settings/`` See also :class:`AppInfoView`. """ model_class = Setting model_title = "Raw Setting" deletable_bulk = True filter_defaults = { "name": {"active": True}, } sort_defaults = "name" # TODO: master should handle this (per model key) def configure_grid(self, grid): # pylint: disable=empty-docstring """ """ g = grid super().configure_grid(g) # name g.set_link("name") def configure_form(self, form): # pylint: disable=empty-docstring """ """ f = form super().configure_form(f) # name f.set_validator("name", self.unique_name) # value # TODO: master should handle this (per column nullable) f.set_required("value", False) def unique_name(self, node, value): # pylint: disable=empty-docstring """ """ model = self.app.model session = self.Session() query = session.query(model.Setting).filter(model.Setting.name == value) if self.editing: name = self.request.matchdict["name"] query = query.filter(model.Setting.name != name) if query.count(): node.raise_invalid("Setting name must be unique")
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring base = globals() AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name "AppInfoView", base["AppInfoView"] ) AppInfoView.defaults(config) SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name "SettingView", base["SettingView"] ) SettingView.defaults(config) def includeme(config): # pylint: disable=missing-function-docstring defaults(config)