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

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 

50 

51log = logging.getLogger(__name__) 

52 

53 

54def new_request(event): 

55 """ 

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

57 

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

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

60 

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

62 'pyramid.events.NewRequest') 

63 

64 This will add to the request object: 

65 

66 .. attribute:: request.wutta_config 

67 

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

69 

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

71 

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. 

75 

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. 

79 

80 .. attribute:: request.use_oruga 

81 

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. 

85 

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

87 

88 Request method which registers a Vue component for use within 

89 the app templates. 

90 

91 :param tagname: Component tag name as string. 

92 

93 :param classname: Component class name as string. 

94 

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: 

98 

99 .. code-block:: mako 

100 

101 ## define component template 

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

103 <div>my example</div> 

104 </script> 

105 

106 <script> 

107 

108 ## define component logic 

109 const MyExample = { 

110 template: 'my-example-template' 

111 } 

112 

113 ## register the component both ways here.. 

114 

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

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

117 

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

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

120 

121 </script> 

122 """ 

123 request = event.request 

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

125 app = config.get_app() 

126 

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

137 

138 request.wutta_config = config 

139 

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 

153 

154 request.get_referrer = get_referrer 

155 

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) 

161 

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

163 if theme == "butterfly": 

164 return True 

165 return False 

166 

167 request.set_property(use_oruga, reify=True) 

168 

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

176 

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 ) 

186 

187 request.wuttaweb_registered_components[tagname] = classname 

188 

189 request.register_component = register_component 

190 

191 

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 

206 

207 

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. 

216 

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

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

219 

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

221 'pyramid.events.NewRequest') 

222 

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: 

226 

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

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

229 

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

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

232 

233 This will add to the request object: 

234 

235 .. attribute:: request.user 

236 

237 Reference to the authenticated 

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

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

240 

241 .. attribute:: request.is_admin 

242 

243 Flag indicating whether current user is a member of the 

244 Administrator role. 

245 

246 .. attribute:: request.is_root 

247 

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. 

251 

252 .. attribute:: request.user_permissions 

253 

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

255 current user. 

256 

257 This set is obtained by calling 

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

259 

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

261 

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

263 

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

265 raise self.forbidden() 

266 

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

268 

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

270 permissions:: 

271 

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

273 return "can either list or view" 

274 else: 

275 raise self.forbidden() 

276 

277 """ 

278 request = event.request 

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

280 app = config.get_app() 

281 auth = app.get_auth_handler() 

282 

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) 

287 

288 # request.is_admin 

289 def is_admin(request): 

290 return auth.user_is_admin(request.user) 

291 

292 request.set_property(is_admin, reify=True) 

293 

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 

300 

301 request.set_property(is_root, reify=True) 

302 

303 # request.user_permissions 

304 def user_permissions(request): 

305 session = db_session or Session() 

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

307 

308 request.set_property(user_permissions, reify=True) 

309 

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 

317 

318 request.has_perm = has_perm 

319 

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 

326 

327 request.has_any_perm = has_any_perm 

328 

329 

330def before_render(event): 

331 """ 

332 Event hook called just before rendering a template. 

333 

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

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

336 

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

338 'pyramid.events.BeforeRender') 

339 

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

342 

343 .. code-block:: mako 

344 

345 ${app.get_title()} 

346 

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

348 

349 .. data:: 'config' 

350 

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

352 

353 .. data:: 'app' 

354 

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

356 

357 .. data:: 'web' 

358 

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

360 

361 .. data:: 'h' 

362 

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

364 

365 .. data:: 'json' 

366 

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

368 

369 .. data:: 'menus' 

370 

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

374 

375 .. data:: 'url' 

376 

377 Reference to the request method, 

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

379 

380 .. data:: 'theme' 

381 

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

383 unless a custom theme is in effect. 

384 

385 .. data:: 'expose_theme_picker' 

386 

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. 

390 

391 .. data:: 'available_themes' 

392 

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

401 

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 

412 

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) 

418 

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) 

426 

427 

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