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

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 

25 

26It is assumed that most apps will include this module somewhere during 

27startup. For instance this happens within 

28:func:`~wuttaweb.app.main()`:: 

29 

30 pyramid_config.include('wuttaweb.subscribers') 

31 

32This allows for certain common logic to be available for all apps. 

33 

34However some custom apps may need to supplement or replace the event 

35hooks contained here, depending on the circumstance. 

36""" 

37 

38import functools 

39import json 

40import logging 

41from collections import OrderedDict 

42 

43from pyramid import threadlocal 

44from pyramid.httpexceptions import HTTPFound 

45 

46from wuttaweb import helpers 

47from wuttaweb.db import Session 

48from wuttaweb.util import get_available_themes 

49 

50log = logging.getLogger(__name__) 

51 

52 

53def new_request(event): 

54 """ 

55 Event hook called when processing a new :term:`request`. 

56 

57 The hook is auto-registered if this module is "included" by 

58 Pyramid config object. Or you can explicitly register it:: 

59 

60 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request', 

61 'pyramid.events.NewRequest') 

62 

63 This will add to the request object: 

64 

65 .. attribute:: request.wutta_config 

66 

67 Reference to the app :term:`config object`. 

68 

69 .. function:: request.get_referrer(default=None) 

70 

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. 

74 

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. 

78 

79 .. attribute:: request.use_oruga 

80 

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. 

84 

85 .. function:: request.register_component(tagname, classname) 

86 

87 Request method which registers a Vue component for use within 

88 the app templates. 

89 

90 :param tagname: Component tag name as string. 

91 

92 :param classname: Component class name as string. 

93 

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: 

97 

98 .. code-block:: mako 

99 

100 ## define component template 

101 <script type="text/x-template" id="my-example-template"> 

102 <div>my example</div> 

103 </script> 

104 

105 <script> 

106 

107 ## define component logic 

108 const MyExample = { 

109 template: 'my-example-template' 

110 } 

111 

112 ## register the component both ways here.. 

113 

114 ## this is for Vue 2 - note the lack of quotes for classname 

115 Vue.component('my-example', MyExample) 

116 

117 ## this is for Vue 3 - note the classname must be quoted 

118 <% request.register_component('my-example', 'MyExample') %> 

119 

120 </script> 

121 """ 

122 request = event.request 

123 config = request.registry.settings["wutta_config"] 

124 app = config.get_app() 

125 

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")) 

136 

137 request.wutta_config = config 

138 

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 

152 

153 request.get_referrer = get_referrer 

154 

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) 

160 

161 theme = request.registry.settings.get("wuttaweb.theme") 

162 if theme == "butterfly": 

163 return True 

164 return False 

165 

166 request.set_property(use_oruga, reify=True) 

167 

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() 

175 

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 ) 

185 

186 request.wuttaweb_registered_components[tagname] = classname 

187 

188 request.register_component = register_component 

189 

190 

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 

205 

206 

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. 

215 

216 The hook is auto-registered if this module is "included" by 

217 Pyramid config object. Or you can explicitly register it:: 

218 

219 pyramid_config.add_subscriber('wuttaweb.subscribers.new_request_set_user', 

220 'pyramid.events.NewRequest') 

221 

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: 

225 

226 :param user_getter: Optional getter function to retrieve the user 

227 from database, instead of :func:`default_user_getter()`. 

228 

229 :param db_session: Optional :term:`db session` to use, 

230 instead of :class:`wuttaweb.db.sess.Session`. 

231 

232 This will add to the request object: 

233 

234 .. attribute:: request.user 

235 

236 Reference to the authenticated 

237 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` instance 

238 (if logged in), or ``None``. 

239 

240 .. attribute:: request.is_admin 

241 

242 Flag indicating whether current user is a member of the 

243 Administrator role. 

244 

245 .. attribute:: request.is_root 

246 

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. 

250 

251 .. attribute:: request.user_permissions 

252 

253 The ``set`` of permission names which are granted to the 

254 current user. 

255 

256 This set is obtained by calling 

257 :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.get_permissions()`. 

258 

259 .. function:: request.has_perm(name) 

260 

261 Shortcut to check if current user has the given permission:: 

262 

263 if not request.has_perm('users.edit'): 

264 raise self.forbidden() 

265 

266 .. function:: request.has_any_perm(*names) 

267 

268 Shortcut to check if current user has any of the given 

269 permissions:: 

270 

271 if request.has_any_perm('users.list', 'users.view'): 

272 return "can either list or view" 

273 else: 

274 raise self.forbidden() 

275 

276 """ 

277 request = event.request 

278 config = request.registry.settings["wutta_config"] 

279 app = config.get_app() 

280 auth = app.get_auth_handler() 

281 

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) 

286 

287 # request.is_admin 

288 def is_admin(request): 

289 return auth.user_is_admin(request.user) 

290 

291 request.set_property(is_admin, reify=True) 

292 

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 

299 

300 request.set_property(is_root, reify=True) 

301 

302 # request.user_permissions 

303 def user_permissions(request): 

304 session = db_session or Session() 

305 return auth.get_permissions(session, request.user) 

306 

307 request.set_property(user_permissions, reify=True) 

308 

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 

316 

317 request.has_perm = has_perm 

318 

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 

325 

326 request.has_any_perm = has_any_perm 

327 

328 

329def before_render(event): 

330 """ 

331 Event hook called just before rendering a template. 

332 

333 The hook is auto-registered if this module is "included" by 

334 Pyramid config object. Or you can explicitly register it:: 

335 

336 pyramid_config.add_subscriber('wuttaweb.subscribers.before_render', 

337 'pyramid.events.BeforeRender') 

338 

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.: 

341 

342 .. code-block:: mako 

343 

344 ${app.get_title()} 

345 

346 Here are the keys added to context dict by this hook: 

347 

348 .. data:: 'config' 

349 

350 Reference to the app :term:`config object`. 

351 

352 .. data:: 'app' 

353 

354 Reference to the :term:`app handler`. 

355 

356 .. data:: 'web' 

357 

358 Reference to the :term:`web handler`. 

359 

360 .. data:: 'h' 

361 

362 Reference to the helper module, :mod:`wuttaweb.helpers`. 

363 

364 .. data:: 'json' 

365 

366 Reference to the built-in module, :mod:`python:json`. 

367 

368 .. data:: 'menus' 

369 

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`. 

373 

374 .. data:: 'url' 

375 

376 Reference to the request method, 

377 :meth:`~pyramid:pyramid.request.Request.route_url()`. 

378 

379 .. data:: 'theme' 

380 

381 String name of the current theme. This will be ``'default'`` 

382 unless a custom theme is in effect. 

383 

384 .. data:: 'expose_theme_picker' 

385 

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. 

389 

390 .. data:: 'available_themes' 

391 

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() 

400 

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 

411 

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) 

417 

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) 

425 

426 

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")