Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / views / settings.py: 100%
117 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-28 15:23 -0600
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# wuttaweb -- Web App for Wutta Framework
5# Copyright © 2024-2025 Lance Edgar
6#
7# This file is part of Wutta Framework.
8#
9# Wutta Framework is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License as published by the Free
11# Software Foundation, either version 3 of the License, or (at your option) any
12# later version.
13#
14# Wutta Framework is distributed in the hope that it will be useful, but
15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
17# more details.
18#
19# You should have received a copy of the GNU General Public License along with
20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Views for app settings
25"""
27import datetime
28import json
29import os
30import sys
31import subprocess
32from collections import OrderedDict
34from wuttjamaican.db.model import Setting
35from wuttjamaican.util import get_timezone_by_name
36from wuttaweb.views import MasterView
37from wuttaweb.util import get_libver, get_liburl
40class AppInfoView(MasterView): # pylint: disable=abstract-method
41 """
42 Master view for the core app info, to show/edit config etc.
44 Default route prefix is ``appinfo``.
46 Notable URLs provided by this class:
48 * ``/appinfo/``
49 * ``/appinfo/configure``
51 See also :class:`SettingView`.
52 """
54 model_name = "AppInfo"
55 model_title_plural = "App Info"
56 route_prefix = "appinfo"
57 filterable = False
58 sort_on_backend = False
59 sort_defaults = "name"
60 paginated = False
61 creatable = False
62 viewable = False
63 editable = False
64 deletable = False
65 configurable = True
67 grid_columns = [
68 "name",
69 "version",
70 "editable_project_location",
71 ]
73 # TODO: for tailbone backward compat with get_liburl() etc.
74 weblib_config_prefix = None
76 def get_grid_data( # pylint: disable=empty-docstring
77 self, columns=None, session=None
78 ):
79 """ """
81 # nb. init with empty data, only load it upon user request
82 if not self.request.GET.get("partial"):
83 return []
85 # TODO: pretty sure this is not cross-platform. probably some
86 # sort of pip methods belong on the app handler? or it should
87 # have a pip handler for all that?
88 pip = os.path.join(sys.prefix, "bin", "pip")
89 output = subprocess.check_output([pip, "list", "--format=json"], text=True)
90 data = json.loads(output.strip())
92 # must avoid null values for sort to work right
93 for pkg in data:
94 pkg.setdefault("editable_project_location", "")
96 return data
98 def configure_grid(self, grid): # pylint: disable=empty-docstring
99 """ """
100 g = grid
101 super().configure_grid(g)
103 g.sort_multiple = False
105 # name
106 g.set_searchable("name")
108 # editable_project_location
109 g.set_searchable("editable_project_location")
111 def get_weblibs(self): # pylint: disable=empty-docstring
112 """ """
113 return OrderedDict(
114 [
115 ("vue", "(Vue2) Vue"),
116 ("vue_resource", "(Vue2) vue-resource"),
117 ("buefy", "(Vue2) Buefy"),
118 ("buefy.css", "(Vue2) Buefy CSS"),
119 ("fontawesome", "(Vue2) FontAwesome"),
120 ("bb_vue", "(Vue3) vue"),
121 ("bb_oruga", "(Vue3) @oruga-ui/oruga-next"),
122 ("bb_oruga_bulma", "(Vue3) @oruga-ui/theme-bulma (JS)"),
123 ("bb_oruga_bulma_css", "(Vue3) @oruga-ui/theme-bulma (CSS)"),
124 ("bb_fontawesome_svg_core", "(Vue3) @fortawesome/fontawesome-svg-core"),
125 ("bb_free_solid_svg_icons", "(Vue3) @fortawesome/free-solid-svg-icons"),
126 ("bb_vue_fontawesome", "(Vue3) @fortawesome/vue-fontawesome"),
127 ]
128 )
130 def configure_get_simple_settings(self): # pylint: disable=empty-docstring
131 """ """
132 simple_settings = [
133 # basics
134 {"name": f"{self.config.appname}.app_title"},
135 {"name": f"{self.config.appname}.node_type"},
136 {"name": f"{self.config.appname}.node_title"},
137 {"name": f"{self.config.appname}.production", "type": bool},
138 {"name": "wuttaweb.themes.expose_picker", "type": bool},
139 {"name": f"{self.config.appname}.timezone.default"},
140 {"name": f"{self.config.appname}.web.menus.handler.spec"},
141 # nb. this is deprecated; we define so it is auto-deleted
142 # when we replace with newer setting
143 {"name": f"{self.config.appname}.web.menus.handler_spec"},
144 # user/auth
145 {"name": "wuttaweb.home_redirect_to_login", "type": bool, "default": False},
146 # email
147 {
148 "name": f"{self.config.appname}.mail.send_emails",
149 "type": bool,
150 "default": False,
151 },
152 {"name": f"{self.config.appname}.email.default.sender"},
153 {"name": f"{self.config.appname}.email.default.subject"},
154 {"name": f"{self.config.appname}.email.default.to"},
155 {"name": f"{self.config.appname}.email.feedback.subject"},
156 {"name": f"{self.config.appname}.email.feedback.to"},
157 ]
159 def getval(key):
160 return self.config.get(f"wuttaweb.{key}")
162 weblibs = self.get_weblibs()
163 for key in weblibs:
165 simple_settings.append(
166 {
167 "name": f"wuttaweb.libver.{key}",
168 "default": getval(f"libver.{key}"),
169 }
170 )
171 simple_settings.append(
172 {
173 "name": f"wuttaweb.liburl.{key}",
174 "default": getval(f"liburl.{key}"),
175 }
176 )
178 return simple_settings
180 def configure_check_timezone(self):
181 """
182 AJAX view to validate a user-specified timezone name.
184 Route name for this is: ``appinfo.check_timezone``
185 """
186 tzname = self.request.GET.get("tzname")
187 if not tzname:
188 return {"invalid": "Must provide 'tzname' parameter."}
189 try:
190 get_timezone_by_name(tzname)
191 return {"invalid": False}
192 except Exception as err: # pylint: disable=broad-exception-caught
193 return {"invalid": str(err)}
195 def configure_get_context( # pylint: disable=empty-docstring,arguments-differ
196 self, **kwargs
197 ):
198 """ """
199 context = super().configure_get_context(**kwargs)
201 # default system timezone
202 dt = datetime.datetime.now().astimezone()
203 context["default_timezone"] = dt.tzname()
205 # add registered menu handlers
206 web = self.app.get_web_handler()
207 handlers = web.get_menu_handler_specs()
208 handlers = [{"spec": spec} for spec in handlers]
209 context["menu_handlers"] = handlers
211 # add `weblibs` to context, based on config values
212 weblibs = self.get_weblibs()
213 for key in weblibs:
214 title = weblibs[key]
215 weblibs[key] = {
216 "key": key,
217 "title": title,
218 # nb. these values are exactly as configured, and are
219 # used for editing the settings
220 "configured_version": get_libver(
221 self.request,
222 key,
223 prefix=self.weblib_config_prefix,
224 configured_only=True,
225 ),
226 "configured_url": get_liburl(
227 self.request,
228 key,
229 prefix=self.weblib_config_prefix,
230 configured_only=True,
231 ),
232 # nb. these are for display only
233 "default_version": get_libver(
234 self.request,
235 key,
236 prefix=self.weblib_config_prefix,
237 default_only=True,
238 ),
239 "live_url": get_liburl(
240 self.request, key, prefix=self.weblib_config_prefix
241 ),
242 }
243 context["weblibs"] = list(weblibs.values())
245 return context
247 @classmethod
248 def defaults(cls, config): # pylint: disable=empty-docstring
249 """ """
250 cls._defaults(config)
251 cls._appinfo_defaults(config)
253 @classmethod
254 def _appinfo_defaults(cls, config):
255 route_prefix = cls.get_route_prefix()
256 permission_prefix = cls.get_permission_prefix()
257 url_prefix = cls.get_url_prefix()
259 # check timezone
260 config.add_route(
261 f"{route_prefix}.check_timezone",
262 f"{url_prefix}/check-timezone",
263 request_method="GET",
264 )
265 config.add_view(
266 cls,
267 attr="configure_check_timezone",
268 route_name=f"{route_prefix}.check_timezone",
269 permission=f"{permission_prefix}.configure",
270 renderer="json",
271 )
274class SettingView(MasterView): # pylint: disable=abstract-method
275 """
276 Master view for the "raw" settings table.
278 Default route prefix is ``settings``.
280 Notable URLs provided by this class:
282 * ``/settings/``
284 See also :class:`AppInfoView`.
285 """
287 model_class = Setting
288 model_title = "Raw Setting"
289 deletable_bulk = True
290 filter_defaults = {
291 "name": {"active": True},
292 }
293 sort_defaults = "name"
295 # TODO: master should handle this (per model key)
296 def configure_grid(self, grid): # pylint: disable=empty-docstring
297 """ """
298 g = grid
299 super().configure_grid(g)
301 # name
302 g.set_link("name")
304 def configure_form(self, form): # pylint: disable=empty-docstring
305 """ """
306 f = form
307 super().configure_form(f)
309 # name
310 f.set_validator("name", self.unique_name)
312 # value
313 # TODO: master should handle this (per column nullable)
314 f.set_required("value", False)
316 def unique_name(self, node, value): # pylint: disable=empty-docstring
317 """ """
318 model = self.app.model
319 session = self.Session()
321 query = session.query(model.Setting).filter(model.Setting.name == value)
323 if self.editing:
324 name = self.request.matchdict["name"]
325 query = query.filter(model.Setting.name != name)
327 if query.count():
328 node.raise_invalid("Setting name must be unique")
331def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
332 base = globals()
334 AppInfoView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
335 "AppInfoView", base["AppInfoView"]
336 )
337 AppInfoView.defaults(config)
339 SettingView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
340 "SettingView", base["SettingView"]
341 )
342 SettingView.defaults(config)
345def includeme(config): # pylint: disable=missing-function-docstring
346 defaults(config)