Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / views / common.py: 100%
124 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-17 14:42 -0600
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-17 14:42 -0600
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"""
24Common Views
25"""
27import logging
29import colander
31from wuttaweb.views import View
32from wuttaweb.forms import widgets
33from wuttaweb.db import Session
34from wuttaweb.util import set_app_theme
37log = logging.getLogger(__name__)
40class CommonView(View):
41 """
42 Common views shared by all apps.
43 """
45 def home(self, session=None):
46 """
47 Home page view.
49 Template: ``/home.mako``
51 This is normally the view shown when a user navigates to the
52 root URL for the web app.
53 """
54 # pylint: disable=duplicate-code
55 model = self.app.model
56 session = session or Session()
58 # nb. redirect to /setup if no users exist
59 user = session.query(model.User).first()
60 if not user:
61 return self.redirect(self.request.route_url("setup"))
62 # pylint: enable=duplicate-code
64 # maybe auto-redirect anons to login
65 if not self.request.user:
66 if self.config.get_bool("wuttaweb.home_redirect_to_login"):
67 return self.redirect(self.request.route_url("login"))
69 return {
70 "index_title": self.app.get_title(),
71 }
73 def forbidden_view(self):
74 """
75 This view is shown when a request triggers a 403 Forbidden error.
77 Template: ``/forbidden.mako``
78 """
79 return {"index_title": self.app.get_title()}
81 def notfound_view(self):
82 """
83 This view is shown when a request triggers a 404 Not Found error.
85 Template: ``/notfound.mako``
86 """
87 return {"index_title": self.app.get_title()}
89 def feedback(self): # pylint: disable=empty-docstring
90 """ """
91 model = self.app.model
92 session = Session()
94 # validate form
95 schema = self.feedback_make_schema()
96 form = self.make_form(schema=schema)
97 if not form.validate():
98 # TODO: native Form class should better expose error(s)
99 dform = form.get_deform()
100 return {"error": str(dform.error)}
102 # build email template context
103 context = dict(form.validated)
104 if context["user_uuid"]:
105 context["user"] = session.get(model.User, context["user_uuid"])
106 context["user_url"] = self.request.route_url(
107 "users.view", uuid=context["user_uuid"]
108 )
109 context["client_ip"] = self.request.client_addr
111 # send email
112 try:
113 self.feedback_send(context)
114 except Exception as error: # pylint: disable=broad-exception-caught
115 log.warning("failed to send feedback email", exc_info=True)
116 return {"error": str(error) or error.__class__.__name__}
118 return {"ok": True}
120 def feedback_make_schema(self): # pylint: disable=empty-docstring
121 """ """
122 schema = colander.Schema()
124 schema.add(colander.SchemaNode(colander.String(), name="referrer"))
126 schema.add(
127 colander.SchemaNode(colander.String(), name="user_uuid", missing=None)
128 )
130 schema.add(colander.SchemaNode(colander.String(), name="user_name"))
132 schema.add(colander.SchemaNode(colander.String(), name="message"))
134 return schema
136 def feedback_send(self, context): # pylint: disable=empty-docstring
137 """ """
138 self.app.send_email("feedback", context)
140 def setup(self, session=None): # pylint: disable=too-many-locals
141 """
142 View for first-time app setup, to create admin user.
144 Template: ``/setup.mako``
146 This page is only meant for one-time use. As such, if the app
147 DB contains any users, this page will always redirect to the
148 home page.
150 However if no users exist yet, this will show a form which may
151 be used to create the first admin user. When finished, user
152 will be redirected to the login page.
154 .. note::
156 As long as there are no users in the DB, both the home and
157 login pages will automatically redirect to this one.
158 """
159 model = self.app.model
160 session = session or Session()
162 # nb. this view only available until first user is created
163 user = session.query(model.User).first()
164 if user:
165 return self.redirect(self.request.route_url("home"))
167 form = self.make_form(
168 fields=["username", "password", "first_name", "last_name"],
169 show_button_cancel=False,
170 show_button_reset=True,
171 )
172 form.set_widget("password", widgets.CheckedPasswordWidget())
173 form.set_required("first_name", False)
174 form.set_required("last_name", False)
176 if form.validate():
177 auth = self.app.get_auth_handler()
178 data = form.validated
180 # make user
181 user = auth.make_user(session=session, username=data["username"])
182 auth.set_user_password(user, data["password"])
184 # assign admin role
185 admin = auth.get_role_administrator(session)
186 user.roles.append(admin)
187 admin.notes = (
188 'users in this role may "become root".\n\n'
189 "it's recommended not to grant other perms to this role."
190 )
192 # initialize built-in roles
193 authed = auth.get_role_authenticated(session)
194 authed.notes = (
195 "this role represents any user who *is* logged in.\n\n"
196 "you may grant any perms you like to it."
197 )
198 anon = auth.get_role_anonymous(session)
199 anon.notes = (
200 "this role represents any user who is *not* logged in.\n\n"
201 "you may grant any perms you like to it."
202 )
204 # also make "Site Admin" role
205 site_admin_perms = [
206 "alembic.migrations.list",
207 "alembic.migrations.create",
208 "alembic.migrations.view",
209 "alembic.migrations.delete",
210 "alembic.migrations.configure",
211 "alembic.dashboard",
212 "alembic.migrate",
213 "app_tables.list",
214 "app_tables.create",
215 "app_tables.view",
216 "appinfo.list",
217 "appinfo.configure",
218 "email_settings.list",
219 "email_settings.edit",
220 "email_settings.view",
221 "master_views.list",
222 "master_views.create",
223 "master_views.configure",
224 "app_tables.view",
225 "people.list",
226 "people.create",
227 "people.view",
228 "people.edit",
229 "people.delete",
230 "people.versions",
231 "roles.list",
232 "roles.create",
233 "roles.view",
234 "roles.edit",
235 "roles.edit_builtin",
236 "roles.delete",
237 "roles.versions",
238 "settings.list",
239 "settings.create",
240 "settings.view",
241 "settings.edit",
242 "settings.delete",
243 "settings.delete_bulk",
244 "upgrades.list",
245 "upgrades.create",
246 "upgrades.view",
247 "upgrades.edit",
248 "upgrades.delete",
249 "upgrades.execute",
250 "upgrades.download",
251 "upgrades.configure",
252 "users.list",
253 "users.create",
254 "users.view",
255 "users.edit",
256 "users.delete",
257 "users.versions",
258 ]
259 admin2 = model.Role(name="Site Admin")
260 admin2.notes = (
261 'this is the "daily driver" admin role.\n\n'
262 "you may grant any perms you like to it."
263 )
264 session.add(admin2)
265 user.roles.append(admin2)
266 for perm in site_admin_perms:
267 auth.grant_permission(admin2, perm)
269 # maybe make person
270 if data["first_name"] or data["last_name"]:
271 first = data["first_name"]
272 last = data["last_name"]
273 person = model.Person(
274 first_name=first,
275 last_name=last,
276 full_name=(f"{first} {last}").strip(),
277 )
278 session.add(person)
279 user.person = person
281 self.setup_enhance_admin_user(user)
283 # send user to /login
284 self.request.session.flash("Account created! Please login below.")
285 return self.redirect(self.request.route_url("login"))
287 return {
288 "index_title": self.app.get_title(),
289 "form": form,
290 }
292 def setup_enhance_admin_user(self, user):
293 """
294 Further "enhance" the initial admin user when it is first created.
296 This does nothing by default; subclass can override if needed.
298 :param user: New admin
299 :class:`~wuttjamaican:wuttjamaican.db.model.auth.User`
300 which was just created as part of initial setup.
301 """
303 def change_theme(self):
304 """
305 This view will set the global app theme, then redirect back to
306 the referring page.
307 """
308 theme = self.request.params.get("theme")
309 if theme:
310 try:
311 set_app_theme(self.request, theme, session=Session())
312 except Exception as error: # pylint: disable=broad-exception-caught
313 error = self.app.render_error(error)
314 self.request.session.flash(f"Failed to set theme: {error}", "error")
315 referrer = self.request.params.get("referrer") or self.request.get_referrer()
316 return self.redirect(referrer)
318 @classmethod
319 def defaults(cls, config): # pylint: disable=empty-docstring
320 """ """
321 cls._defaults(config)
323 @classmethod
324 def _defaults(cls, config):
326 config.add_wutta_permission_group("common", "(General)", overwrite=False)
328 # home page
329 config.add_route("home", "/")
330 config.add_view(cls, attr="home", route_name="home", renderer="/home.mako")
332 # forbidden
333 config.add_forbidden_view(
334 cls, attr="forbidden_view", renderer="/forbidden.mako"
335 )
337 # notfound
338 # nb. also, auto-correct URLs which require trailing slash
339 config.add_notfound_view(
340 cls, attr="notfound_view", append_slash=True, renderer="/notfound.mako"
341 )
343 # feedback
344 config.add_route("feedback", "/feedback", request_method="POST")
345 config.add_view(
346 cls,
347 attr="feedback",
348 route_name="feedback",
349 permission="common.feedback",
350 renderer="json",
351 )
352 config.add_wutta_permission(
353 "common", "common.feedback", "Send a feedback message"
354 )
356 # setup
357 config.add_route("setup", "/setup")
358 config.add_view(cls, attr="setup", route_name="setup", renderer="/setup.mako")
360 # change theme
361 config.add_route("change_theme", "/change-theme", request_method="POST")
362 config.add_view(cls, attr="change_theme", route_name="change_theme")
363 config.add_wutta_permission(
364 "common", "common.change_theme", "Change the global app theme"
365 )
368def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
369 base = globals()
371 CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
372 "CommonView", base["CommonView"]
373 )
374 CommonView.defaults(config)
377def includeme(config): # pylint: disable=missing-function-docstring
378 defaults(config)