Coverage for .tox/coverage/lib/python3.13/site-packages/wuttaweb/views/common.py: 100%
107 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-29 20:33 -0500
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-29 20:33 -0500
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
36log = logging.getLogger(__name__)
39class CommonView(View):
40 """
41 Common views shared by all apps.
42 """
44 def home(self, session=None):
45 """
46 Home page view.
48 Template: ``/home.mako``
50 This is normally the view shown when a user navigates to the
51 root URL for the web app.
52 """
53 # pylint: disable=duplicate-code
54 model = self.app.model
55 session = session or Session()
57 # nb. redirect to /setup if no users exist
58 user = session.query(model.User).first()
59 if not user:
60 return self.redirect(self.request.route_url("setup"))
61 # pylint: enable=duplicate-code
63 # maybe auto-redirect anons to login
64 if not self.request.user:
65 if self.config.get_bool("wuttaweb.home_redirect_to_login"):
66 return self.redirect(self.request.route_url("login"))
68 return {
69 "index_title": self.app.get_title(),
70 }
72 def forbidden_view(self):
73 """
74 This view is shown when a request triggers a 403 Forbidden error.
76 Note that this page template is rendered only if the user is
77 already logged in. If they are not yet, they're instead
78 redirected to the login page.
80 Template: ``/forbidden.mako``
81 """
82 if not self.request.user:
83 self.request.session["url_after_login"] = self.request.url
84 self.request.session.flash("You must login to see that page.", "error")
85 return self.redirect(self.request.route_url("login"))
87 return {"index_title": self.app.get_title()}
89 def notfound_view(self):
90 """
91 This view is shown when a request triggers a 404 Not Found error.
93 Template: ``/notfound.mako``
94 """
95 return {"index_title": self.app.get_title()}
97 def feedback(self): # pylint: disable=empty-docstring
98 """ """
99 model = self.app.model
100 session = Session()
102 # validate form
103 schema = self.feedback_make_schema()
104 form = self.make_form(schema=schema)
105 if not form.validate():
106 # TODO: native Form class should better expose error(s)
107 dform = form.get_deform()
108 return {"error": str(dform.error)}
110 # build email template context
111 context = dict(form.validated)
112 if context["user_uuid"]:
113 context["user"] = session.get(model.User, context["user_uuid"])
114 context["user_url"] = self.request.route_url(
115 "users.view", uuid=context["user_uuid"]
116 )
117 context["client_ip"] = self.request.client_addr
119 # send email
120 try:
121 self.feedback_send(context)
122 except Exception as error: # pylint: disable=broad-exception-caught
123 log.warning("failed to send feedback email", exc_info=True)
124 return {"error": str(error) or error.__class__.__name__}
126 return {"ok": True}
128 def feedback_make_schema(self): # pylint: disable=empty-docstring
129 """ """
130 schema = colander.Schema()
132 schema.add(colander.SchemaNode(colander.String(), name="referrer"))
134 schema.add(
135 colander.SchemaNode(colander.String(), name="user_uuid", missing=None)
136 )
138 schema.add(colander.SchemaNode(colander.String(), name="user_name"))
140 schema.add(colander.SchemaNode(colander.String(), name="message"))
142 return schema
144 def feedback_send(self, context): # pylint: disable=empty-docstring
145 """ """
146 self.app.send_email("feedback", context)
148 def setup(self):
149 """
150 View for first-time app setup, to create admin user etc.
152 Template: ``/setup.mako``
154 This page is only meant for one-time use. As such, if the app
155 DB contains any users, this page will always redirect to the
156 home page.
158 However if no users exist yet, this will show a form which may
159 be used to create the first admin user.
161 When form is submitted, this calls :meth:`do_setup()` to
162 handle the actual setup. When finished, user will be
163 redirected to the login page.
165 .. note::
167 As long as there are no users in the DB, both the home and
168 login pages will automatically redirect to this one.
169 """
170 model = self.app.model
171 session = Session()
173 # nb. this view only available until first user is created
174 user = session.query(model.User).first()
175 if user:
176 return self.redirect(self.request.route_url("home"))
178 form = self.make_form(
179 fields=["username", "password", "first_name", "last_name"],
180 show_button_cancel=False,
181 show_button_reset=True,
182 )
183 form.set_widget("password", widgets.CheckedPasswordWidget())
184 form.set_required("first_name", False)
185 form.set_required("last_name", False)
187 if form.validate():
189 # do the setup
190 self.do_setup(session, form.validated)
192 # send user to /login
193 self.request.session.flash("Account created! Please login below.")
194 return self.redirect(self.request.route_url("login"))
196 return {
197 "index_title": self.app.get_title(),
198 "form": form,
199 }
201 def do_setup(self, session, data):
202 """
203 Perform the actual setup operations. This is called from
204 :meth:`setup()`.
206 Default logic will invoke two methods on the :term:`install
207 handler`:
209 * :meth:`~wuttjamaican:wuttjamaican.install.InstallHandler.establish_builtin_roles()`
210 * :meth:`~wuttjamaican:wuttjamaican.install.InstallHandler.establish_site_admin_user()`
212 :param session: Current :term:`db session`.
214 :param data: Form data submitted from the /setup page.
215 """
216 install = self.app.get_install_handler()
218 install.establish_builtin_roles(session)
220 install.establish_site_admin_user(
221 session,
222 data["username"],
223 data["password"],
224 first_name=data["first_name"],
225 last_name=data["last_name"],
226 )
228 def change_theme(self):
229 """
230 This view will set the global app theme, then redirect back to
231 the referring page.
232 """
233 theme = self.request.params.get("theme")
234 if theme:
235 try:
236 set_app_theme(self.request, theme, session=Session())
237 except Exception as error: # pylint: disable=broad-exception-caught
238 error = self.app.render_error(error)
239 self.request.session.flash(f"Failed to set theme: {error}", "error")
240 referrer = self.request.params.get("referrer") or self.request.get_referrer()
241 return self.redirect(referrer)
243 @classmethod
244 def defaults(cls, config): # pylint: disable=empty-docstring
245 """ """
246 cls._defaults(config)
248 @classmethod
249 def _defaults(cls, config):
251 config.add_wutta_permission_group("common", "(General)", overwrite=False)
253 # home page
254 config.add_route("home", "/")
255 config.add_view(cls, attr="home", route_name="home", renderer="/home.mako")
257 # forbidden
258 config.add_forbidden_view(
259 cls, attr="forbidden_view", renderer="/forbidden.mako"
260 )
262 # notfound
263 # nb. also, auto-correct URLs which require trailing slash
264 config.add_notfound_view(
265 cls, attr="notfound_view", append_slash=True, renderer="/notfound.mako"
266 )
268 # feedback
269 config.add_route("feedback", "/feedback", request_method="POST")
270 config.add_view(
271 cls,
272 attr="feedback",
273 route_name="feedback",
274 permission="common.feedback",
275 renderer="json",
276 )
277 config.add_wutta_permission(
278 "common", "common.feedback", "Send a feedback message"
279 )
281 # setup
282 config.add_route("setup", "/setup")
283 config.add_view(cls, attr="setup", route_name="setup", renderer="/setup.mako")
285 # change theme
286 config.add_route("change_theme", "/change-theme", request_method="POST")
287 config.add_view(cls, attr="change_theme", route_name="change_theme")
288 config.add_wutta_permission(
289 "common", "common.change_theme", "Change the global app theme"
290 )
293def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
294 base = globals()
296 CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
297 "CommonView", base["CommonView"]
298 )
299 CommonView.defaults(config)
302def includeme(config): # pylint: disable=missing-function-docstring
303 defaults(config)