WuttaWeb 0.31.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. wuttaweb/__init__.py +27 -0
  2. wuttaweb/_version.py +9 -0
  3. wuttaweb/app.py +295 -0
  4. wuttaweb/auth.py +252 -0
  5. wuttaweb/cleanup.py +102 -0
  6. wuttaweb/cli/__init__.py +28 -0
  7. wuttaweb/cli/webapp.py +97 -0
  8. wuttaweb/code-templates/new-master-view.mako +116 -0
  9. wuttaweb/code-templates/new-table.mako +64 -0
  10. wuttaweb/conf.py +89 -0
  11. wuttaweb/db/__init__.py +31 -0
  12. wuttaweb/db/continuum.py +62 -0
  13. wuttaweb/db/sess.py +66 -0
  14. wuttaweb/diffs.py +290 -0
  15. wuttaweb/email-templates/feedback.html.mako +40 -0
  16. wuttaweb/email-templates/feedback.txt.mako +23 -0
  17. wuttaweb/emails.py +49 -0
  18. wuttaweb/forms/__init__.py +32 -0
  19. wuttaweb/forms/base.py +1577 -0
  20. wuttaweb/forms/schema.py +771 -0
  21. wuttaweb/forms/widgets.py +631 -0
  22. wuttaweb/grids/__init__.py +33 -0
  23. wuttaweb/grids/base.py +2757 -0
  24. wuttaweb/grids/filters.py +891 -0
  25. wuttaweb/handler.py +218 -0
  26. wuttaweb/helpers.py +61 -0
  27. wuttaweb/menus.py +395 -0
  28. wuttaweb/progress.py +170 -0
  29. wuttaweb/static/__init__.py +74 -0
  30. wuttaweb/static/img/favicon.ico +0 -0
  31. wuttaweb/static/img/logo.png +0 -0
  32. wuttaweb/static/img/testing.png +0 -0
  33. wuttaweb/subscribers.py +431 -0
  34. wuttaweb/templates/alembic/dashboard.mako +238 -0
  35. wuttaweb/templates/alembic/migrations/configure.mako +31 -0
  36. wuttaweb/templates/alembic/migrations/create.mako +70 -0
  37. wuttaweb/templates/alembic/migrations/index.mako +25 -0
  38. wuttaweb/templates/alembic/migrations/view.mako +17 -0
  39. wuttaweb/templates/appinfo/configure.mako +412 -0
  40. wuttaweb/templates/appinfo/index.mako +120 -0
  41. wuttaweb/templates/auth/change_password.mako +7 -0
  42. wuttaweb/templates/auth/login.mako +65 -0
  43. wuttaweb/templates/base.mako +861 -0
  44. wuttaweb/templates/base_meta.mako +23 -0
  45. wuttaweb/templates/batch/view.mako +113 -0
  46. wuttaweb/templates/configure.mako +207 -0
  47. wuttaweb/templates/deform/checkbox.pt +13 -0
  48. wuttaweb/templates/deform/checkbox_choice.pt +18 -0
  49. wuttaweb/templates/deform/checked_password.pt +16 -0
  50. wuttaweb/templates/deform/dateinput.pt +7 -0
  51. wuttaweb/templates/deform/datetimeinput.pt +12 -0
  52. wuttaweb/templates/deform/moneyinput.pt +8 -0
  53. wuttaweb/templates/deform/password.pt +10 -0
  54. wuttaweb/templates/deform/permissions.pt +23 -0
  55. wuttaweb/templates/deform/readonly/checkbox.pt +4 -0
  56. wuttaweb/templates/deform/readonly/email_recips.pt +5 -0
  57. wuttaweb/templates/deform/readonly/external_link.pt +10 -0
  58. wuttaweb/templates/deform/readonly/filedownload.pt +14 -0
  59. wuttaweb/templates/deform/readonly/notes.pt +7 -0
  60. wuttaweb/templates/deform/readonly/objectref.pt +9 -0
  61. wuttaweb/templates/deform/readonly/permissions.pt +18 -0
  62. wuttaweb/templates/deform/readonly/rolerefs.pt +7 -0
  63. wuttaweb/templates/deform/select.pt +50 -0
  64. wuttaweb/templates/deform/textarea.pt +12 -0
  65. wuttaweb/templates/deform/textinput.pt +9 -0
  66. wuttaweb/templates/deform/wutta_checked_password.pt +8 -0
  67. wuttaweb/templates/diff.mako +15 -0
  68. wuttaweb/templates/email/settings/view.mako +39 -0
  69. wuttaweb/templates/forbidden.mako +26 -0
  70. wuttaweb/templates/form.mako +59 -0
  71. wuttaweb/templates/forms/vue_buttons.mako +43 -0
  72. wuttaweb/templates/forms/vue_fields.mako +4 -0
  73. wuttaweb/templates/forms/vue_template.mako +54 -0
  74. wuttaweb/templates/grids/table_element.mako +75 -0
  75. wuttaweb/templates/grids/vue_template.mako +779 -0
  76. wuttaweb/templates/home.mako +35 -0
  77. wuttaweb/templates/master/configure.mako +9 -0
  78. wuttaweb/templates/master/create.mako +7 -0
  79. wuttaweb/templates/master/create_row.mako +4 -0
  80. wuttaweb/templates/master/delete.mako +18 -0
  81. wuttaweb/templates/master/edit.mako +9 -0
  82. wuttaweb/templates/master/form.mako +5 -0
  83. wuttaweb/templates/master/index.mako +102 -0
  84. wuttaweb/templates/master/merge.mako +88 -0
  85. wuttaweb/templates/master/view.mako +90 -0
  86. wuttaweb/templates/master/view_version.mako +50 -0
  87. wuttaweb/templates/master/view_versions.mako +20 -0
  88. wuttaweb/templates/notfound.mako +23 -0
  89. wuttaweb/templates/page.mako +55 -0
  90. wuttaweb/templates/people/view_profile.mako +12 -0
  91. wuttaweb/templates/progress.mako +127 -0
  92. wuttaweb/templates/reports/view.mako +61 -0
  93. wuttaweb/templates/setup.mako +20 -0
  94. wuttaweb/templates/tables/app/create.mako +1035 -0
  95. wuttaweb/templates/tables/app/index.mako +33 -0
  96. wuttaweb/templates/themes/butterfly/base.mako +78 -0
  97. wuttaweb/templates/themes/butterfly/buefy-components.mako +802 -0
  98. wuttaweb/templates/themes/butterfly/buefy-plugin.mako +32 -0
  99. wuttaweb/templates/themes/butterfly/http-plugin.mako +100 -0
  100. wuttaweb/templates/upgrade.mako +64 -0
  101. wuttaweb/templates/upgrades/configure.mako +20 -0
  102. wuttaweb/templates/upgrades/index.mako +14 -0
  103. wuttaweb/templates/upgrades/view.mako +37 -0
  104. wuttaweb/templates/users/view.mako +126 -0
  105. wuttaweb/templates/views/master/configure.mako +31 -0
  106. wuttaweb/templates/views/master/create.mako +846 -0
  107. wuttaweb/templates/views/master/index.mako +17 -0
  108. wuttaweb/templates/wutta-components.mako +850 -0
  109. wuttaweb/testing.py +226 -0
  110. wuttaweb/util.py +906 -0
  111. wuttaweb/views/__init__.py +38 -0
  112. wuttaweb/views/alembic.py +576 -0
  113. wuttaweb/views/auth.py +340 -0
  114. wuttaweb/views/base.py +172 -0
  115. wuttaweb/views/batch.py +501 -0
  116. wuttaweb/views/common.py +378 -0
  117. wuttaweb/views/email.py +335 -0
  118. wuttaweb/views/essential.py +85 -0
  119. wuttaweb/views/master.py +4602 -0
  120. wuttaweb/views/people.py +240 -0
  121. wuttaweb/views/progress.py +77 -0
  122. wuttaweb/views/reports.py +280 -0
  123. wuttaweb/views/roles.py +463 -0
  124. wuttaweb/views/settings.py +441 -0
  125. wuttaweb/views/tables.py +486 -0
  126. wuttaweb/views/upgrades.py +388 -0
  127. wuttaweb/views/users.py +539 -0
  128. wuttaweb/views/views.py +469 -0
  129. wuttaweb-0.31.1.dist-info/METADATA +66 -0
  130. wuttaweb-0.31.1.dist-info/RECORD +133 -0
  131. wuttaweb-0.31.1.dist-info/WHEEL +4 -0
  132. wuttaweb-0.31.1.dist-info/entry_points.txt +20 -0
  133. wuttaweb-0.31.1.dist-info/licenses/COPYING.txt +674 -0
wuttaweb/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024 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
+ """
24
+ wuttaweb - base package for web app
25
+ """
26
+
27
+ from ._version import __version__
wuttaweb/_version.py ADDED
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8; -*-
2
+ """
3
+ Package Version
4
+ """
5
+
6
+ from importlib.metadata import version
7
+
8
+
9
+ __version__ = version("wuttaweb")
wuttaweb/app.py ADDED
@@ -0,0 +1,295 @@
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
+ """
24
+ Application
25
+ """
26
+
27
+ import logging
28
+ import os
29
+
30
+ from wuttjamaican.app import AppProvider
31
+ from wuttjamaican.conf import make_config
32
+
33
+ from asgiref.wsgi import WsgiToAsgi
34
+ from pyramid.config import Configurator
35
+
36
+ import wuttaweb.db
37
+ from wuttaweb.auth import WuttaSecurityPolicy
38
+ from wuttaweb.util import get_effective_theme, get_theme_template_path
39
+
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+
44
+ class WebAppProvider(AppProvider):
45
+ """
46
+ The :term:`app provider` for WuttaWeb. This adds some methods to
47
+ the :term:`app handler`, which are specific to web apps. It also
48
+ registers some :term:`email templates <email template>` for the
49
+ app, etc.
50
+ """
51
+
52
+ email_modules = ["wuttaweb.emails"]
53
+ email_templates = ["wuttaweb:email-templates"]
54
+
55
+ def get_web_handler(self):
56
+ """
57
+ Get the configured "web" handler for the app.
58
+
59
+ Specify a custom handler in your config file like this:
60
+
61
+ .. code-block:: ini
62
+
63
+ [wutta]
64
+ web.handler_spec = poser.web.handler:PoserWebHandler
65
+
66
+ :returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
67
+ """
68
+ if "web" not in self.app.handlers:
69
+ spec = self.config.get(
70
+ f"{self.appname}.web.handler_spec",
71
+ default="wuttaweb.handler:WebHandler",
72
+ )
73
+ self.app.handlers["web"] = self.app.load_object(spec)(self.config)
74
+ return self.app.handlers["web"]
75
+
76
+
77
+ def make_wutta_config(settings, config_maker=None, **kwargs):
78
+ """
79
+ Make a WuttaConfig object from the given settings.
80
+
81
+ Note that ``settings`` dict will (typically) correspond to the
82
+ ``[app:main]`` section of your config file.
83
+
84
+ Regardless, the ``settings`` must contain a special key/value
85
+ which is needed to identify the location of the config file.
86
+ Assuming the typical scenario then, your config file should have
87
+ an entry like this:
88
+
89
+ .. code-block:: ini
90
+
91
+ [app:main]
92
+ wutta.config = %(__file__)s
93
+
94
+ The ``%(__file__)s`` is auto-replaced with the config file path,
95
+ so ultimately ``settings`` would contain something like (at
96
+ minimum)::
97
+
98
+ {'wutta.config': '/path/to/config/file'}
99
+
100
+ If this config file path cannot be discovered, an error is raised.
101
+ """
102
+ wutta_config = settings.get("wutta_config")
103
+ if not wutta_config:
104
+
105
+ # validate config file path
106
+ path = settings.get("wutta.config")
107
+ if not path or not os.path.exists(path):
108
+ raise ValueError(
109
+ "Please set 'wutta.config' in [app:main] "
110
+ "section of config to the path of your "
111
+ "config file. Lame, but necessary."
112
+ )
113
+
114
+ # make config, add to settings
115
+ config_maker = config_maker or make_config
116
+ wutta_config = config_maker(path, **kwargs)
117
+ settings["wutta_config"] = wutta_config
118
+
119
+ # configure database sessions
120
+ if hasattr(wutta_config, "appdb_engine"):
121
+ wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine)
122
+
123
+ return wutta_config
124
+
125
+
126
+ def make_pyramid_config(settings):
127
+ """
128
+ Make and return a Pyramid config object from the given settings.
129
+
130
+ The config is initialized with certain features deemed useful for
131
+ all apps.
132
+
133
+ :returns: Instance of
134
+ :class:`pyramid:pyramid.config.Configurator`.
135
+ """
136
+ settings.setdefault("fanstatic.versioning", "true")
137
+ settings.setdefault("mako.directories", ["wuttaweb:templates"])
138
+ settings.setdefault(
139
+ "pyramid_deform.template_search_path", "wuttaweb:templates/deform"
140
+ )
141
+
142
+ # update settings per current theme
143
+ establish_theme(settings)
144
+
145
+ pyramid_config = Configurator(settings=settings)
146
+
147
+ # configure user authorization / authentication
148
+ pyramid_config.set_security_policy(WuttaSecurityPolicy())
149
+
150
+ # require CSRF token for POST
151
+ pyramid_config.set_default_csrf_options(
152
+ require_csrf=True, token="_csrf", header="X-CSRF-TOKEN"
153
+ )
154
+
155
+ pyramid_config.include("pyramid_beaker")
156
+ pyramid_config.include("pyramid_deform")
157
+ pyramid_config.include("pyramid_fanstatic")
158
+ pyramid_config.include("pyramid_mako")
159
+ pyramid_config.include("pyramid_tm")
160
+
161
+ # add some permissions magic
162
+ pyramid_config.add_directive(
163
+ "add_wutta_permission_group", "wuttaweb.auth.add_permission_group"
164
+ )
165
+ pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
166
+
167
+ # add some more config magic
168
+ pyramid_config.add_directive(
169
+ "add_wutta_master_view", "wuttaweb.conf.add_master_view"
170
+ )
171
+
172
+ return pyramid_config
173
+
174
+
175
+ def main(global_config, **settings): # pylint: disable=unused-argument
176
+ """
177
+ Make and return the WSGI application, per given settings.
178
+
179
+ This function is designed to be called via Paste, hence it does
180
+ require params and therefore can't be used directly as app factory
181
+ for general WSGI servers. For the latter see
182
+ :func:`make_wsgi_app()` instead.
183
+
184
+ And this *particular* function is not even that useful, it only
185
+ constructs an app with minimal views built-in to WuttaWeb. Most
186
+ apps will define their own ``main()`` function (e.g. as
187
+ ``poser.web.app:main``), similar to this one but with additional
188
+ views and other config.
189
+ """
190
+ wutta_config = make_wutta_config(settings) # pylint: disable=unused-variable
191
+ pyramid_config = make_pyramid_config(settings)
192
+
193
+ pyramid_config.include("wuttaweb.static")
194
+ pyramid_config.include("wuttaweb.subscribers")
195
+ pyramid_config.include("wuttaweb.views")
196
+
197
+ return pyramid_config.make_wsgi_app()
198
+
199
+
200
+ def make_wsgi_app(main_app=None, config=None):
201
+ """
202
+ Make and return a WSGI app, using the given Paste app factory.
203
+
204
+ See also :func:`make_asgi_app()` for the ASGI equivalent.
205
+
206
+ This function could be used directly for general WSGI servers
207
+ (e.g. uvicorn), ***if*** you just want the built-in :func:`main()`
208
+ app factory.
209
+
210
+ But most likely you do not, in which case you must define your own
211
+ function and call this one with your preferred app factory::
212
+
213
+ from wuttaweb.app import make_wsgi_app
214
+
215
+ def my_main(global_config, **settings):
216
+ # TODO: build your app
217
+ pass
218
+
219
+ def make_my_wsgi_app():
220
+ return make_wsgi_app(my_main)
221
+
222
+ So ``make_my_wsgi_app()`` could then be used as-is for general
223
+ WSGI servers. However, note that this approach will require
224
+ setting the ``WUTTA_CONFIG_FILES`` environment variable, unless
225
+ running via :ref:`wutta-webapp`.
226
+
227
+ :param main_app: Either a Paste-compatible app factory, or
228
+ :term:`spec` for one. If not specified, the built-in
229
+ :func:`main()` is assumed.
230
+
231
+ :param config: Optional :term:`config object`. If not specified,
232
+ one is created based on ``WUTTA_CONFIG_FILES`` environment
233
+ variable.
234
+ """
235
+ if not config:
236
+ config = make_config()
237
+ app = config.get_app()
238
+
239
+ # extract pyramid settings
240
+ settings = config.get_dict("app:main")
241
+
242
+ # keep same config object
243
+ settings["wutta_config"] = config
244
+
245
+ # determine the app factory
246
+ if isinstance(main_app, str):
247
+ factory = app.load_object(main_app)
248
+ elif callable(main_app):
249
+ factory = main_app
250
+ else:
251
+ raise ValueError("main_app must be spec or callable")
252
+
253
+ # construct a pyramid app "per usual"
254
+ return factory({}, **settings)
255
+
256
+
257
+ def make_asgi_app(main_app=None, config=None):
258
+ """
259
+ Make and return a ASGI app, using the given Paste app factory.
260
+
261
+ This works the same as :func:`make_wsgi_app()` and should be
262
+ called in the same way etc.
263
+ """
264
+ wsgi_app = make_wsgi_app(main_app, config=config)
265
+ return WsgiToAsgi(wsgi_app)
266
+
267
+
268
+ def establish_theme(settings):
269
+ """
270
+ Establishes initial theme on app startup. This mostly involves
271
+ updating the given ``settings`` dict.
272
+
273
+ This function is called automatically from within
274
+ :func:`make_pyramid_config()`.
275
+
276
+ It will first call :func:`~wuttaweb.util.get_effective_theme()` to
277
+ read the current theme from the :term:`settings table`, and store
278
+ this within ``settings['wuttaweb.theme']``.
279
+
280
+ It then calls :func:`~wuttaweb.util.get_theme_template_path()` and
281
+ will update ``settings['mako.directories']`` such that the theme's
282
+ template path is listed first.
283
+ """
284
+ config = settings["wutta_config"]
285
+
286
+ theme = get_effective_theme(config)
287
+ settings["wuttaweb.theme"] = theme
288
+
289
+ directories = settings["mako.directories"]
290
+ if isinstance(directories, str):
291
+ directories = config.parse_list(directories)
292
+
293
+ path = get_theme_template_path(config)
294
+ directories.insert(0, path)
295
+ settings["mako.directories"] = directories
wuttaweb/auth.py ADDED
@@ -0,0 +1,252 @@
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
+ """
24
+ Auth Utility Logic
25
+ """
26
+
27
+ from pyramid.authentication import SessionAuthenticationHelper
28
+ from pyramid.request import RequestLocalCache
29
+ from pyramid.security import remember, forget
30
+
31
+ from wuttaweb.db import Session
32
+
33
+
34
+ def login_user(request, user):
35
+ """
36
+ Perform the steps necessary to "login" the given user. This
37
+ returns a ``headers`` dict which you should pass to the final
38
+ redirect, like so::
39
+
40
+ from pyramid.httpexceptions import HTTPFound
41
+
42
+ headers = login_user(request, user)
43
+ return HTTPFound(location='/', headers=headers)
44
+
45
+ .. warning::
46
+
47
+ This logic does not "authenticate" the user! It assumes caller
48
+ has already authenticated the user and they are safe to login.
49
+
50
+ See also :func:`logout_user()`.
51
+ """
52
+ headers = remember(request, user.uuid)
53
+ return headers
54
+
55
+
56
+ def logout_user(request):
57
+ """
58
+ Perform the logout action for the given request. This returns a
59
+ ``headers`` dict which you should pass to the final redirect, like
60
+ so::
61
+
62
+ from pyramid.httpexceptions import HTTPFound
63
+
64
+ headers = logout_user(request)
65
+ return HTTPFound(location='/', headers=headers)
66
+
67
+ See also :func:`login_user()`.
68
+ """
69
+ request.session.delete()
70
+ request.session.invalidate()
71
+ headers = forget(request)
72
+ return headers
73
+
74
+
75
+ class WuttaSecurityPolicy:
76
+ """
77
+ Pyramid :term:`security policy` for WuttaWeb.
78
+
79
+ For more on the Pyramid details, see :doc:`pyramid:narr/security`.
80
+
81
+ But the idea here is that you should be able to just use this,
82
+ without thinking too hard::
83
+
84
+ from pyramid.config import Configurator
85
+ from wuttaweb.auth import WuttaSecurityPolicy
86
+
87
+ pyramid_config = Configurator()
88
+ pyramid_config.set_security_policy(WuttaSecurityPolicy())
89
+
90
+ This security policy will then do the following:
91
+
92
+ * use the request "web session" for auth storage (e.g. current
93
+ ``user.uuid``)
94
+ * check permissions as needed, by calling
95
+ :meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()`
96
+ for current user
97
+
98
+ :param db_session: Optional :term:`db session` to use, instead of
99
+ :class:`wuttaweb.db.sess.Session`. Probably only useful for
100
+ tests.
101
+ """
102
+
103
+ def __init__(self, db_session=None):
104
+ self.session_helper = SessionAuthenticationHelper()
105
+ self.identity_cache = RequestLocalCache(self.load_identity)
106
+ self.db_session = db_session or Session()
107
+
108
+ def load_identity(self, request): # pylint: disable=empty-docstring
109
+ """ """
110
+ config = request.registry.settings["wutta_config"]
111
+ app = config.get_app()
112
+ model = app.model
113
+
114
+ # fetch user uuid from current session
115
+ uuid = self.session_helper.authenticated_userid(request)
116
+ if not uuid:
117
+ return None
118
+
119
+ # fetch user object from db
120
+ user = self.db_session.get(model.User, uuid)
121
+ if not user:
122
+ return None
123
+
124
+ return user
125
+
126
+ def identity(self, request): # pylint: disable=empty-docstring
127
+ """ """
128
+ return self.identity_cache.get_or_create(request)
129
+
130
+ def authenticated_userid(self, request): # pylint: disable=empty-docstring
131
+ """ """
132
+ user = self.identity(request)
133
+ if user is not None:
134
+ return user.uuid
135
+ return None
136
+
137
+ def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
138
+ """ """
139
+ return self.session_helper.remember(request, userid, **kw)
140
+
141
+ def forget(self, request, **kw): # pylint: disable=empty-docstring
142
+ """ """
143
+ return self.session_helper.forget(request, **kw)
144
+
145
+ def permits( # pylint: disable=unused-argument,empty-docstring
146
+ self, request, context, permission
147
+ ):
148
+ """ """
149
+
150
+ # nb. root user can do anything
151
+ if getattr(request, "is_root", False):
152
+ return True
153
+
154
+ config = request.registry.settings["wutta_config"]
155
+ app = config.get_app()
156
+ auth = app.get_auth_handler()
157
+ user = self.identity(request)
158
+ return auth.has_permission(self.db_session, user, permission)
159
+
160
+
161
+ def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True):
162
+ """
163
+ Pyramid directive to add a "permission group" to the app's
164
+ awareness.
165
+
166
+ The app must be made aware of all permissions, so they are exposed
167
+ when editing a
168
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
169
+ for discovering permissions is in
170
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
171
+
172
+ This is usually called from within a master view's
173
+ :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
174
+ the permission group which applies to the view model.
175
+
176
+ A simple example of usage::
177
+
178
+ pyramid_config.add_permission_group('widgets', label="Widgets")
179
+
180
+ :param groupkey: Unique key for the permission group. In the
181
+ context of a master view, this will be the same as
182
+ :attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
183
+
184
+ :param label: Optional label for the permission group. If not
185
+ specified, it is derived from ``groupkey``.
186
+
187
+ :param overwrite: If the permission group was already established,
188
+ this flag controls whether the group's label should be
189
+ overwritten (with ``label``).
190
+
191
+ See also :func:`add_permission()`.
192
+ """
193
+ config = pyramid_config.get_settings()["wutta_config"]
194
+ app = config.get_app()
195
+
196
+ def action():
197
+ perms = pyramid_config.get_settings().get("wutta_permissions", {})
198
+ if overwrite or groupkey not in perms:
199
+ group = perms.setdefault(groupkey, {"key": groupkey})
200
+ group["label"] = label or app.make_title(groupkey)
201
+ pyramid_config.add_settings({"wutta_permissions": perms})
202
+
203
+ pyramid_config.action(None, action)
204
+
205
+
206
+ def add_permission(pyramid_config, groupkey, key, label=None):
207
+ """
208
+ Pyramid directive to add a single "permission" to the app's
209
+ awareness.
210
+
211
+ The app must be made aware of all permissions, so they are exposed
212
+ when editing a
213
+ :class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
214
+ for discovering permissions is in
215
+ :meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
216
+
217
+ This is usually called from within a master view's
218
+ :meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
219
+ "known" permissions based on master view feature flags
220
+ (:attr:`~wuttaweb.views.master.MasterView.viewable`,
221
+ :attr:`~wuttaweb.views.master.MasterView.editable`, etc.).
222
+
223
+ A simple example of usage::
224
+
225
+ pyramid_config.add_permission('widgets', 'widgets.polish',
226
+ label="Polish all the widgets")
227
+
228
+ :param groupkey: Unique key for the permission group. In the
229
+ context of a master view, this will be the same as
230
+ :attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
231
+
232
+ :param key: Unique key for the permission. This should be the
233
+ "complete" permission name which includes the permission
234
+ prefix.
235
+
236
+ :param label: Optional label for the permission. If not
237
+ specified, it is derived from ``key``.
238
+
239
+ See also :func:`add_permission_group()`.
240
+ """
241
+
242
+ def action():
243
+ config = pyramid_config.get_settings()["wutta_config"]
244
+ app = config.get_app()
245
+ perms = pyramid_config.get_settings().get("wutta_permissions", {})
246
+ group = perms.setdefault(groupkey, {"key": groupkey})
247
+ group.setdefault("label", app.make_title(groupkey))
248
+ perm = group.setdefault("perms", {}).setdefault(key, {"key": key})
249
+ perm["label"] = label or app.make_title(key)
250
+ pyramid_config.add_settings({"wutta_permissions": perms})
251
+
252
+ pyramid_config.action(None, action)
wuttaweb/cleanup.py ADDED
@@ -0,0 +1,102 @@
1
+ # -*- coding: utf-8; -*-
2
+ ################################################################################
3
+ #
4
+ # wuttaweb -- Web App for Wutta Framework
5
+ # Copyright © 2024-2026 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
+ """
24
+ Cleanup for WuttaWeb
25
+ """
26
+
27
+ import os
28
+ import logging
29
+ import time
30
+
31
+ from wuttjamaican.cleanup import Cleaner
32
+
33
+
34
+ log = logging.getLogger(__name__)
35
+
36
+
37
+ class WuttaWebCleaner(Cleaner):
38
+ """
39
+ This :term:`cleaner` removes old Beaker session files.
40
+ """
41
+
42
+ def cleanup(self, dry_run=False):
43
+ """
44
+ This first calls :meth:`get_beaker_session_dir()` and then
45
+ iterates over the contents, looking for files older than the
46
+ cutoff (default is 30 days). Such files will be deleted
47
+ unless ``dry_mode`` is specified.
48
+ """
49
+ session_dir = self.get_beaker_session_dir()
50
+ if not session_dir:
51
+ log.debug("Beaker session folder could not be located")
52
+ return
53
+
54
+ data_dir = os.path.join(session_dir, "data")
55
+ lock_dir = os.path.join(session_dir, "lock")
56
+
57
+ # looking for files older than X days
58
+ days = self.config.get_int(
59
+ f"{self.config.appname}.cleanup.wuttaweb.beaker_session_cutoff_days",
60
+ default=30,
61
+ )
62
+ cutoff = time.time() - 3600 * 24 * days
63
+
64
+ for topdir in (data_dir, lock_dir):
65
+ if not os.path.isdir(topdir):
66
+ continue
67
+
68
+ for ( # pylint: disable=unused-variable
69
+ dirpath,
70
+ dirnames,
71
+ filenames,
72
+ ) in os.walk(topdir):
73
+ for fname in filenames:
74
+ path = os.path.join(dirpath, fname)
75
+ ts = os.path.getmtime(path)
76
+ if ts <= cutoff:
77
+ if dry_run:
78
+ log.debug("would delete file: %s", path)
79
+ else:
80
+ os.remove(path)
81
+ log.debug("deleted file: %s", path)
82
+
83
+ def get_beaker_session_dir(self):
84
+ """
85
+ Retrieve the path to Beaker session storage folder.
86
+
87
+ By default this will be at ``venv/app/cache/sessions`` but it
88
+ can be overridden via config.
89
+
90
+ :returns: Path as string, or ``None`` if it could not be
91
+ determined.
92
+ """
93
+ if path := self.config.get(
94
+ f"{self.config.appname}.cleanup.wuttaweb.beaker_session_dir"
95
+ ):
96
+ return path
97
+
98
+ path = self.app.get_appdir("cache", "sessions")
99
+ if os.path.isdir(path):
100
+ return path
101
+
102
+ return None