Source code for wuttaweb.views.common

# -*- coding: utf-8; -*-
################################################################################
#
#  wuttaweb -- Web App for Wutta Framework
#  Copyright © 2024-2026 Lance Edgar
#
#  This file is part of Wutta Framework.
#
#  Wutta Framework is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  Wutta Framework is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Common Views
"""

import logging

import colander

from wuttaweb.views import View
from wuttaweb.forms import widgets
from wuttaweb.db import Session
from wuttaweb.util import set_app_theme


log = logging.getLogger(__name__)


[docs] class CommonView(View): """ Common views shared by all apps. """
[docs] def home(self, session=None): """ Home page view. Template: ``/home.mako`` This is normally the view shown when a user navigates to the root URL for the web app. """ # pylint: disable=duplicate-code model = self.app.model session = session or Session() # nb. redirect to /setup if no users exist user = session.query(model.User).first() if not user: return self.redirect(self.request.route_url("setup")) # pylint: enable=duplicate-code # maybe auto-redirect anons to login if not self.request.user: if self.config.get_bool("wuttaweb.home_redirect_to_login"): return self.redirect(self.request.route_url("login")) return { "index_title": self.app.get_title(), }
[docs] def forbidden_view(self): """ This view is shown when a request triggers a 403 Forbidden error. Template: ``/forbidden.mako`` """ return {"index_title": self.app.get_title()}
[docs] def notfound_view(self): """ This view is shown when a request triggers a 404 Not Found error. Template: ``/notfound.mako`` """ return {"index_title": self.app.get_title()}
def feedback(self): # pylint: disable=empty-docstring """ """ model = self.app.model session = Session() # validate form schema = self.feedback_make_schema() form = self.make_form(schema=schema) if not form.validate(): # TODO: native Form class should better expose error(s) dform = form.get_deform() return {"error": str(dform.error)} # build email template context context = dict(form.validated) if context["user_uuid"]: context["user"] = session.get(model.User, context["user_uuid"]) context["user_url"] = self.request.route_url( "users.view", uuid=context["user_uuid"] ) context["client_ip"] = self.request.client_addr # send email try: self.feedback_send(context) except Exception as error: # pylint: disable=broad-exception-caught log.warning("failed to send feedback email", exc_info=True) return {"error": str(error) or error.__class__.__name__} return {"ok": True} def feedback_make_schema(self): # pylint: disable=empty-docstring """ """ schema = colander.Schema() schema.add(colander.SchemaNode(colander.String(), name="referrer")) schema.add( colander.SchemaNode(colander.String(), name="user_uuid", missing=None) ) schema.add(colander.SchemaNode(colander.String(), name="user_name")) schema.add(colander.SchemaNode(colander.String(), name="message")) return schema def feedback_send(self, context): # pylint: disable=empty-docstring """ """ self.app.send_email("feedback", context)
[docs] def setup(self, session=None): # pylint: disable=too-many-locals """ View for first-time app setup, to create admin user. Template: ``/setup.mako`` This page is only meant for one-time use. As such, if the app DB contains any users, this page will always redirect to the home page. However if no users exist yet, this will show a form which may be used to create the first admin user. When finished, user will be redirected to the login page. .. note:: As long as there are no users in the DB, both the home and login pages will automatically redirect to this one. """ model = self.app.model session = session or Session() # nb. this view only available until first user is created user = session.query(model.User).first() if user: return self.redirect(self.request.route_url("home")) form = self.make_form( fields=["username", "password", "first_name", "last_name"], show_button_cancel=False, show_button_reset=True, ) form.set_widget("password", widgets.CheckedPasswordWidget()) form.set_required("first_name", False) form.set_required("last_name", False) if form.validate(): auth = self.app.get_auth_handler() data = form.validated # make user user = auth.make_user(session=session, username=data["username"]) auth.set_user_password(user, data["password"]) # assign admin role admin = auth.get_role_administrator(session) user.roles.append(admin) admin.notes = ( 'users in this role may "become root".\n\n' "it's recommended not to grant other perms to this role." ) # initialize built-in roles authed = auth.get_role_authenticated(session) authed.notes = ( "this role represents any user who *is* logged in.\n\n" "you may grant any perms you like to it." ) anon = auth.get_role_anonymous(session) anon.notes = ( "this role represents any user who is *not* logged in.\n\n" "you may grant any perms you like to it." ) # also make "Site Admin" role site_admin_perms = [ "alembic.migrations.list", "alembic.migrations.create", "alembic.migrations.view", "alembic.migrations.delete", "alembic.migrations.configure", "alembic.dashboard", "alembic.migrate", "app_tables.list", "app_tables.create", "app_tables.view", "appinfo.list", "appinfo.configure", "email_settings.list", "email_settings.edit", "email_settings.view", "master_views.list", "master_views.create", "master_views.configure", "app_tables.view", "people.list", "people.create", "people.view", "people.edit", "people.delete", "people.versions", "roles.list", "roles.create", "roles.view", "roles.edit", "roles.edit_builtin", "roles.delete", "roles.versions", "settings.list", "settings.create", "settings.view", "settings.edit", "settings.delete", "settings.delete_bulk", "upgrades.list", "upgrades.create", "upgrades.view", "upgrades.edit", "upgrades.delete", "upgrades.execute", "upgrades.download", "upgrades.configure", "users.list", "users.create", "users.view", "users.edit", "users.delete", "users.versions", ] admin2 = model.Role(name="Site Admin") admin2.notes = ( 'this is the "daily driver" admin role.\n\n' "you may grant any perms you like to it." ) session.add(admin2) user.roles.append(admin2) for perm in site_admin_perms: auth.grant_permission(admin2, perm) # maybe make person if data["first_name"] or data["last_name"]: first = data["first_name"] last = data["last_name"] person = model.Person( first_name=first, last_name=last, full_name=(f"{first} {last}").strip(), ) session.add(person) user.person = person self.setup_enhance_admin_user(user) # send user to /login self.request.session.flash("Account created! Please login below.") return self.redirect(self.request.route_url("login")) return { "index_title": self.app.get_title(), "form": form, }
[docs] def setup_enhance_admin_user(self, user): """ Further "enhance" the initial admin user when it is first created. This does nothing by default; subclass can override if needed. :param user: New admin :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` which was just created as part of initial setup. """
[docs] def change_theme(self): """ This view will set the global app theme, then redirect back to the referring page. """ theme = self.request.params.get("theme") if theme: try: set_app_theme(self.request, theme, session=Session()) except Exception as error: # pylint: disable=broad-exception-caught error = self.app.render_error(error) self.request.session.flash(f"Failed to set theme: {error}", "error") referrer = self.request.params.get("referrer") or self.request.get_referrer() return self.redirect(referrer)
@classmethod def defaults(cls, config): # pylint: disable=empty-docstring """ """ cls._defaults(config) @classmethod def _defaults(cls, config): config.add_wutta_permission_group("common", "(General)", overwrite=False) # home page config.add_route("home", "/") config.add_view(cls, attr="home", route_name="home", renderer="/home.mako") # forbidden config.add_forbidden_view( cls, attr="forbidden_view", renderer="/forbidden.mako" ) # notfound # nb. also, auto-correct URLs which require trailing slash config.add_notfound_view( cls, attr="notfound_view", append_slash=True, renderer="/notfound.mako" ) # feedback config.add_route("feedback", "/feedback", request_method="POST") config.add_view( cls, attr="feedback", route_name="feedback", permission="common.feedback", renderer="json", ) config.add_wutta_permission( "common", "common.feedback", "Send a feedback message" ) # setup config.add_route("setup", "/setup") config.add_view(cls, attr="setup", route_name="setup", renderer="/setup.mako") # change theme config.add_route("change_theme", "/change-theme", request_method="POST") config.add_view(cls, attr="change_theme", route_name="change_theme") config.add_wutta_permission( "common", "common.change_theme", "Change the global app theme" )
def defaults(config, **kwargs): # pylint: disable=missing-function-docstring base = globals() CommonView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name "CommonView", base["CommonView"] ) CommonView.defaults(config) def includeme(config): # pylint: disable=missing-function-docstring defaults(config)