Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / subscribers.py: 100%
112 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 19:57 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 19:57 -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"""
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
51log = logging.getLogger(__name__)
54def new_request(event):
55 """
56 Event hook called when processing a new :term:`request`.
58 The hook is auto-registered if this module is "included" by
59 Pyramid config object. Or you can explicitly register it::
61 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request',
62 'pyramid.events.NewRequest')
64 This will add to the request object:
66 .. attribute:: request.wutta_config
68 Reference to the app :term:`config object`.
70 .. function:: request.get_referrer(default=None)
72 Request method to get the "canonical" HTTP referrer value.
73 This has logic to check for referrer in the request params,
74 user session etc.
76 :param default: Optional default URL if none is found in
77 request params/session. If no default is specified,
78 the ``'home'`` route is used.
80 .. attribute:: request.use_oruga
82 Flag indicating whether the frontend should be displayed using
83 Vue 3 + Oruga (if ``True``), or else Vue 2 + Buefy (if
84 ``False``). This flag is ``False`` by default.
86 .. function:: request.register_component(tagname, classname)
88 Request method which registers a Vue component for use within
89 the app templates.
91 :param tagname: Component tag name as string.
93 :param classname: Component class name as string.
95 This is meant to be analogous to the ``Vue.component()`` call
96 which is part of Vue 2. It is good practice to always call
97 both at the same time/place:
99 .. code-block:: mako
101 ## define component template
102 <script type="text/x-template" id="my-example-template">
103 <div>my example</div>
104 </script>
106 <script>
108 ## define component logic
109 const MyExample = {
110 template: 'my-example-template'
111 }
113 ## register the component both ways here..
115 ## this is for Vue 2 - note the lack of quotes for classname
116 Vue.component('my-example', MyExample)
118 ## this is for Vue 3 - note the classname must be quoted
119 <% request.register_component('my-example', 'MyExample') %>
121 </script>
122 """
123 request = event.request
124 config = request.registry.settings["wutta_config"]
125 app = config.get_app()
127 # nb. in rare circumstances i have received unhandled error email,
128 # which somehow was triggered by 'fanstatic.needed' being missing
129 # from the environ. not sure why that would happen, but it seems
130 # to when crawlers request a non-existent filename under the
131 # fanstatic path. there isn't a great way to handle it since
132 # e.g. can't show the normal error page if JS resources won't
133 # load, so we try a hail-mary redirect..
134 # (nb. also we skip this if environ is empty, i.e. for tests)
135 if request.environ and "fanstatic.needed" not in request.environ:
136 raise HTTPFound(location=request.route_url("home"))
138 request.wutta_config = config
140 def get_referrer(default=None):
141 if request.params.get("referrer"):
142 return request.params["referrer"]
143 if request.session.get("referrer"):
144 return request.session.pop("referrer")
145 referrer = getattr(request, "referrer", None)
146 if (
147 not referrer
148 or referrer == request.current_route_url()
149 or not referrer.startswith(request.host_url)
150 ):
151 referrer = default or request.route_url("home")
152 return referrer
154 request.get_referrer = get_referrer
156 def use_oruga(request):
157 spec = config.get("wuttaweb.oruga_detector.spec")
158 if spec:
159 func = app.load_object(spec)
160 return func(request)
162 theme = request.registry.settings.get("wuttaweb.theme")
163 if theme == "butterfly":
164 return True
165 return False
167 request.set_property(use_oruga, reify=True)
169 def register_component(tagname, classname):
170 """
171 Register a Vue 3 component, so the base template knows to
172 declare it for use within the app (page).
173 """
174 if not hasattr(request, "wuttaweb_registered_components"):
175 request.wuttaweb_registered_components = OrderedDict()
177 if tagname in request.wuttaweb_registered_components:
178 log.warning(
179 "component with tagname '%s' already registered "
180 "with class '%s' but we are replacing that "
181 "with class '%s'",
182 tagname,
183 request.wuttaweb_registered_components[tagname],
184 classname,
185 )
187 request.wuttaweb_registered_components[tagname] = classname
189 request.register_component = register_component
192def default_user_getter(request, db_session=None):
193 """
194 This is the default function used to retrieve user object from
195 database. Result of this is then assigned to :attr:`request.user`
196 as part of the :func:`new_request_set_user()` hook.
197 """
198 uuid = request.authenticated_userid
199 if uuid:
200 config = request.wutta_config
201 app = config.get_app()
202 model = app.model
203 session = db_session or Session()
204 return session.get(model.User, uuid)
205 return None
208def new_request_set_user(
209 event,
210 user_getter=default_user_getter,
211 db_session=None,
212):
213 """
214 Event hook called when processing a new :term:`request`, for sake
215 of setting the :attr:`request.user` and similar properties.
217 The hook is auto-registered if this module is "included" by
218 Pyramid config object. Or you can explicitly register it::
220 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request_set_user',
221 'pyramid.events.NewRequest')
223 You may wish to "supplement" this hook by registering your own
224 custom hook and then invoking this one as needed. You can then
225 pass certain params to override only parts of the logic:
227 :param user_getter: Optional getter function to retrieve the user
228 from database, instead of :func:`default_user_getter()`.
230 :param db_session: Optional :term:`db session` to use,
231 instead of :class:`wuttaweb.db.sess.Session`.
233 This will add to the request object:
235 .. attribute:: request.user
237 Reference to the authenticated
238 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` instance
239 (if logged in), or ``None``.
241 .. attribute:: request.is_admin
243 Flag indicating whether current user is a member of the
244 Administrator role.
246 .. attribute:: request.is_root
248 Flag indicating whether user is currently elevated to root
249 privileges. This is only possible if :attr:`request.is_admin`
250 is also true.
252 .. attribute:: request.user_permissions
254 The ``set`` of permission names which are granted to the
255 current user.
257 This set is obtained by calling
258 :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.get_permissions()`.
260 .. function:: request.has_perm(name)
262 Shortcut to check if current user has the given permission::
264 if not request.has_perm('users.edit'):
265 raise self.forbidden()
267 .. function:: request.has_any_perm(*names)
269 Shortcut to check if current user has any of the given
270 permissions::
272 if request.has_any_perm('users.list', 'users.view'):
273 return "can either list or view"
274 else:
275 raise self.forbidden()
277 """
278 request = event.request
279 config = request.registry.settings["wutta_config"]
280 app = config.get_app()
281 auth = app.get_auth_handler()
283 # request.user
284 if db_session:
285 user_getter = functools.partial(user_getter, db_session=db_session)
286 request.set_property(user_getter, name="user", reify=True)
288 # request.is_admin
289 def is_admin(request):
290 return auth.user_is_admin(request.user)
292 request.set_property(is_admin, reify=True)
294 # request.is_root
295 def is_root(request):
296 if request.is_admin:
297 if request.session.get("is_root", False):
298 return True
299 return False
301 request.set_property(is_root, reify=True)
303 # request.user_permissions
304 def user_permissions(request):
305 session = db_session or Session()
306 return auth.get_permissions(session, request.user)
308 request.set_property(user_permissions, reify=True)
310 # request.has_perm()
311 def has_perm(name):
312 if request.is_root:
313 return True
314 if name in request.user_permissions:
315 return True
316 return False
318 request.has_perm = has_perm
320 # request.has_any_perm()
321 def has_any_perm(*names):
322 for name in names:
323 if request.has_perm(name):
324 return True
325 return False
327 request.has_any_perm = has_any_perm
330def before_render(event):
331 """
332 Event hook called just before rendering a template.
334 The hook is auto-registered if this module is "included" by
335 Pyramid config object. Or you can explicitly register it::
337 pyramid_config.add_subscriber('wuttaweb.subscribers.before_render',
338 'pyramid.events.BeforeRender')
340 This will add some things to the template context dict. Each of
341 these may be used "directly" in a template then, e.g.:
343 .. code-block:: mako
345 ${app.get_title()}
347 Here are the keys added to context dict by this hook:
349 .. data:: 'config'
351 Reference to the app :term:`config object`.
353 .. data:: 'app'
355 Reference to the :term:`app handler`.
357 .. data:: 'web'
359 Reference to the :term:`web handler`.
361 .. data:: 'h'
363 Reference to the helper module, :mod:`wuttaweb.helpers`.
365 .. data:: 'json'
367 Reference to the built-in module, :mod:`python:json`.
369 .. data:: 'menus'
371 Set of entries to be shown in the main menu. This is obtained
372 by calling :meth:`~wuttaweb.menus.MenuHandler.do_make_menus()`
373 on the configured :class:`~wuttaweb.menus.MenuHandler`.
375 .. data:: 'url'
377 Reference to the request method,
378 :meth:`~pyramid:pyramid.request.Request.route_url()`.
380 .. data:: 'theme'
382 String name of the current theme. This will be ``'default'``
383 unless a custom theme is in effect.
385 .. data:: 'expose_theme_picker'
387 Boolean indicating whether the theme picker should *ever* be
388 exposed. For a user to see it, this flag must be true *and*
389 the user must have permission to change theme.
391 .. data:: 'available_themes'
393 List of theme names from which user may choose, if they are
394 allowed to change theme. Only set/relevant if
395 ``expose_theme_picker`` is true (see above).
396 """
397 request = event.get("request") or threadlocal.get_current_request()
398 config = request.wutta_config
399 app = config.get_app()
400 web = app.get_web_handler()
402 context = event
403 context["config"] = config
404 context["app"] = app
405 context["model"] = app.model
406 context["enum"] = app.enum
407 context["web"] = web
408 context["h"] = helpers
409 context["url"] = request.route_url
410 context["json"] = json
411 context["b"] = "o" if request.use_oruga else "b" # for buefy
413 # TODO: this should be avoided somehow, for non-traditional web
414 # apps, esp. "API" web apps. (in the meantime can configure the
415 # app to use NullMenuHandler which avoids most of the overhead.)
416 menus = web.get_menu_handler()
417 context["menus"] = menus.do_make_menus(request)
419 # theme
420 context["theme"] = request.registry.settings.get("wuttaweb.theme", "default")
421 context["expose_theme_picker"] = config.get_bool(
422 "wuttaweb.themes.expose_picker", default=False
423 )
424 if context["expose_theme_picker"]:
425 context["available_themes"] = get_available_themes(config)
428def includeme(config): # pylint: disable=missing-function-docstring
429 config.add_subscriber(new_request, "pyramid.events.NewRequest")
430 config.add_subscriber(new_request_set_user, "pyramid.events.NewRequest")
431 config.add_subscriber(before_render, "pyramid.events.BeforeRender")