Coverage for .tox/coverage/lib/python3.13/site-packages/wuttaweb/subscribers.py: 100%
112 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 16:36 -0500
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-24 16:36 -0500
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"""
24Event Subscribers
26It is assumed that most apps will include this module somewhere during
27startup. For instance this happens within
28:func:`~wuttaweb.app.main()`::
30 pyramid_config.include('wuttaweb.subscribers')
32This allows for certain common logic to be available for all apps.
34However some custom apps may need to supplement or replace the event
35hooks contained here, depending on the circumstance.
36"""
38import functools
39import json
40import logging
41from collections import OrderedDict
43from pyramid import threadlocal
44from pyramid.httpexceptions import HTTPFound
46from wuttaweb import helpers
47from wuttaweb.db import Session
48from wuttaweb.util import get_available_themes
50log = logging.getLogger(__name__)
53def new_request(event):
54 """
55 Event hook called when processing a new :term:`request`.
57 The hook is auto-registered if this module is "included" by
58 Pyramid config object. Or you can explicitly register it::
60 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request',
61 'pyramid.events.NewRequest')
63 This will add to the request object:
65 .. attribute:: request.wutta_config
67 Reference to the app :term:`config object`.
69 .. function:: request.get_referrer(default=None)
71 Request method to get the "canonical" HTTP referrer value.
72 This has logic to check for referrer in the request params,
73 user session etc.
75 :param default: Optional default URL if none is found in
76 request params/session. If no default is specified,
77 the ``'home'`` route is used.
79 .. attribute:: request.use_oruga
81 Flag indicating whether the frontend should be displayed using
82 Vue 3 + Oruga (if ``True``), or else Vue 2 + Buefy (if
83 ``False``). This flag is ``False`` by default.
85 .. function:: request.register_component(tagname, classname)
87 Request method which registers a Vue component for use within
88 the app templates.
90 :param tagname: Component tag name as string.
92 :param classname: Component class name as string.
94 This is meant to be analogous to the ``Vue.component()`` call
95 which is part of Vue 2. It is good practice to always call
96 both at the same time/place:
98 .. code-block:: mako
100 ## define component template
101 <script type="text/x-template" id="my-example-template">
102 <div>my example</div>
103 </script>
105 <script>
107 ## define component logic
108 const MyExample = {
109 template: 'my-example-template'
110 }
112 ## register the component both ways here..
114 ## this is for Vue 2 - note the lack of quotes for classname
115 Vue.component('my-example', MyExample)
117 ## this is for Vue 3 - note the classname must be quoted
118 <% request.register_component('my-example', 'MyExample') %>
120 </script>
121 """
122 request = event.request
123 config = request.registry.settings["wutta_config"]
124 app = config.get_app()
126 # nb. in rare circumstances i have received unhandled error email,
127 # which somehow was triggered by 'fanstatic.needed' being missing
128 # from the environ. not sure why that would happen, but it seems
129 # to when crawlers request a non-existent filename under the
130 # fanstatic path. there isn't a great way to handle it since
131 # e.g. can't show the normal error page if JS resources won't
132 # load, so we try a hail-mary redirect..
133 # (nb. also we skip this if environ is empty, i.e. for tests)
134 if request.environ and "fanstatic.needed" not in request.environ:
135 raise HTTPFound(location=request.route_url("home"))
137 request.wutta_config = config
139 def get_referrer(default=None):
140 if request.params.get("referrer"):
141 return request.params["referrer"]
142 if request.session.get("referrer"):
143 return request.session.pop("referrer")
144 referrer = getattr(request, "referrer", None)
145 if (
146 not referrer
147 or referrer == request.current_route_url()
148 or not referrer.startswith(request.host_url)
149 ):
150 referrer = default or request.route_url("home")
151 return referrer
153 request.get_referrer = get_referrer
155 def use_oruga(request):
156 spec = config.get("wuttaweb.oruga_detector.spec")
157 if spec:
158 func = app.load_object(spec)
159 return func(request)
161 theme = request.registry.settings.get("wuttaweb.theme")
162 if theme == "butterfly":
163 return True
164 return False
166 request.set_property(use_oruga, reify=True)
168 def register_component(tagname, classname):
169 """
170 Register a Vue 3 component, so the base template knows to
171 declare it for use within the app (page).
172 """
173 if not hasattr(request, "wuttaweb_registered_components"):
174 request.wuttaweb_registered_components = OrderedDict()
176 if tagname in request.wuttaweb_registered_components:
177 log.warning(
178 "component with tagname '%s' already registered "
179 "with class '%s' but we are replacing that "
180 "with class '%s'",
181 tagname,
182 request.wuttaweb_registered_components[tagname],
183 classname,
184 )
186 request.wuttaweb_registered_components[tagname] = classname
188 request.register_component = register_component
191def default_user_getter(request, db_session=None):
192 """
193 This is the default function used to retrieve user object from
194 database. Result of this is then assigned to :attr:`request.user`
195 as part of the :func:`new_request_set_user()` hook.
196 """
197 uuid = request.authenticated_userid
198 if uuid:
199 config = request.wutta_config
200 app = config.get_app()
201 model = app.model
202 session = db_session or Session()
203 return session.get(model.User, uuid)
204 return None
207def new_request_set_user(
208 event,
209 user_getter=default_user_getter,
210 db_session=None,
211):
212 """
213 Event hook called when processing a new :term:`request`, for sake
214 of setting the :attr:`request.user` and similar properties.
216 The hook is auto-registered if this module is "included" by
217 Pyramid config object. Or you can explicitly register it::
219 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request_set_user',
220 'pyramid.events.NewRequest')
222 You may wish to "supplement" this hook by registering your own
223 custom hook and then invoking this one as needed. You can then
224 pass certain params to override only parts of the logic:
226 :param user_getter: Optional getter function to retrieve the user
227 from database, instead of :func:`default_user_getter()`.
229 :param db_session: Optional :term:`db session` to use,
230 instead of :class:`wuttaweb.db.sess.Session`.
232 This will add to the request object:
234 .. attribute:: request.user
236 Reference to the authenticated
237 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` instance
238 (if logged in), or ``None``.
240 .. attribute:: request.is_admin
242 Flag indicating whether current user is a member of the
243 Administrator role.
245 .. attribute:: request.is_root
247 Flag indicating whether user is currently elevated to root
248 privileges. This is only possible if :attr:`request.is_admin`
249 is also true.
251 .. attribute:: request.user_permissions
253 The ``set`` of permission names which are granted to the
254 current user.
256 This set is obtained by calling
257 :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.get_permissions()`.
259 .. function:: request.has_perm(name)
261 Shortcut to check if current user has the given permission::
263 if not request.has_perm('users.edit'):
264 raise self.forbidden()
266 .. function:: request.has_any_perm(*names)
268 Shortcut to check if current user has any of the given
269 permissions::
271 if request.has_any_perm('users.list', 'users.view'):
272 return "can either list or view"
273 else:
274 raise self.forbidden()
276 """
277 request = event.request
278 config = request.registry.settings["wutta_config"]
279 app = config.get_app()
280 auth = app.get_auth_handler()
282 # request.user
283 if db_session:
284 user_getter = functools.partial(user_getter, db_session=db_session)
285 request.set_property(user_getter, name="user", reify=True)
287 # request.is_admin
288 def is_admin(request):
289 return auth.user_is_admin(request.user)
291 request.set_property(is_admin, reify=True)
293 # request.is_root
294 def is_root(request):
295 if request.is_admin:
296 if request.session.get("is_root", False):
297 return True
298 return False
300 request.set_property(is_root, reify=True)
302 # request.user_permissions
303 def user_permissions(request):
304 session = db_session or Session()
305 return auth.get_permissions(session, request.user)
307 request.set_property(user_permissions, reify=True)
309 # request.has_perm()
310 def has_perm(name):
311 if request.is_root:
312 return True
313 if name in request.user_permissions:
314 return True
315 return False
317 request.has_perm = has_perm
319 # request.has_any_perm()
320 def has_any_perm(*names):
321 for name in names:
322 if request.has_perm(name):
323 return True
324 return False
326 request.has_any_perm = has_any_perm
329def before_render(event):
330 """
331 Event hook called just before rendering a template.
333 The hook is auto-registered if this module is "included" by
334 Pyramid config object. Or you can explicitly register it::
336 pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
337 'pyramid.events.BeforeRender')
339 This will add some things to the template context dict. Each of
340 these may be used "directly" in a template then, e.g.:
342 .. code-block:: mako
344 ${app.get_title()}
346 Here are the keys added to context dict by this hook:
348 .. data:: 'config'
350 Reference to the app :term:`config object`.
352 .. data:: 'app'
354 Reference to the :term:`app handler`.
356 .. data:: 'web'
358 Reference to the :term:`web handler`.
360 .. data:: 'h'
362 Reference to the helper module, :mod:`wuttaweb.helpers`.
364 .. data:: 'json'
366 Reference to the built-in module, :mod:`python:json`.
368 .. data:: 'menus'
370 Set of entries to be shown in the main menu. This is obtained
371 by calling :meth:`~wuttaweb.menus.MenuHandler.do_make_menus()`
372 on the configured :class:`~wuttaweb.menus.MenuHandler`.
374 .. data:: 'url'
376 Reference to the request method,
377 :meth:`~pyramid:pyramid.request.Request.route_url()`.
379 .. data:: 'theme'
381 String name of the current theme. This will be ``'default'``
382 unless a custom theme is in effect.
384 .. data:: 'expose_theme_picker'
386 Boolean indicating whether the theme picker should *ever* be
387 exposed. For a user to see it, this flag must be true *and*
388 the user must have permission to change theme.
390 .. data:: 'available_themes'
392 List of theme names from which user may choose, if they are
393 allowed to change theme. Only set/relevant if
394 ``expose_theme_picker`` is true (see above).
395 """
396 request = event.get("request") or threadlocal.get_current_request()
397 config = request.wutta_config
398 app = config.get_app()
399 web = app.get_web_handler()
401 context = event
402 context["config"] = config
403 context["app"] = app
404 context["model"] = app.model
405 context["enum"] = app.enum
406 context["web"] = web
407 context["h"] = helpers
408 context["url"] = request.route_url
409 context["json"] = json
410 context["b"] = "o" if request.use_oruga else "b" # for buefy
412 # TODO: this should be avoided somehow, for non-traditional web
413 # apps, esp. "API" web apps. (in the meantime can configure the
414 # app to use NullMenuHandler which avoids most of the overhead.)
415 menus = web.get_menu_handler()
416 context["menus"] = menus.do_make_menus(request)
418 # theme
419 context["theme"] = request.registry.settings.get("wuttaweb.theme", "default")
420 context["expose_theme_picker"] = config.get_bool(
421 "wuttaweb.themes.expose_picker", default=False
422 )
423 if context["expose_theme_picker"]:
424 context["available_themes"] = get_available_themes(config)
427def includeme(config): # pylint: disable=missing-function-docstring
428 config.add_subscriber(new_request, "pyramid.events.NewRequest")
429 config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
430 config.add_subscriber(before_render, "pyramid.events.BeforeRender")