Coverage for .tox/coverage/lib/python3.11/site-packages/wuttaweb/views/common.py: 100%
109 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-26 14:40 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-26 14:40 -0500
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"""
24Common Views
25"""
27import logging
29import colander
30from pyramid.renderers import render
32from wuttaweb.views import View
33from wuttaweb.forms import widgets
34from wuttaweb.db import Session
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 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'))
62 # maybe auto-redirect anons to login
63 if not self.request.user:
64 if self.config.get_bool('wuttaweb.home_redirect_to_login'):
65 return self.redirect(self.request.route_url('login'))
67 return {
68 'index_title': self.app.get_title(),
69 }
71 def forbidden_view(self):
72 """
73 This view is shown when a request triggers a 403 Forbidden error.
75 Template: ``/forbidden.mako``
76 """
77 return {'index_title': self.app.get_title()}
79 def notfound_view(self):
80 """
81 This view is shown when a request triggers a 404 Not Found error.
83 Template: ``/notfound.mako``
84 """
85 return {'index_title': self.app.get_title()}
87 def feedback(self):
88 """ """
89 model = self.app.model
90 session = Session()
92 # validate form
93 schema = self.feedback_make_schema()
94 form = self.make_form(schema=schema)
95 if not form.validate():
96 # TODO: native Form class should better expose error(s)
97 dform = form.get_deform()
98 return {'error': str(dform.error)}
100 # build email template context
101 context = dict(form.validated)
102 if context['user_uuid']:
103 context['user'] = session.get(model.User, context['user_uuid'])
104 context['user_url'] = self.request.route_url('users.view', uuid=context['user_uuid'])
105 context['client_ip'] = self.request.client_addr
107 # send email
108 try:
109 self.feedback_send(context)
110 except Exception as error:
111 log.warning("failed to send feedback email", exc_info=True)
112 return {'error': str(error) or error.__class__.__name__}
114 return {'ok': True}
116 def feedback_make_schema(self):
117 """ """
118 schema = colander.Schema()
120 schema.add(colander.SchemaNode(colander.String(),
121 name='referrer'))
123 schema.add(colander.SchemaNode(colander.String(),
124 name='user_uuid',
125 missing=None))
127 schema.add(colander.SchemaNode(colander.String(),
128 name='user_name'))
130 schema.add(colander.SchemaNode(colander.String(),
131 name='message'))
133 return schema
135 def feedback_send(self, context):
136 """ """
137 self.app.send_email('feedback', context)
139 def setup(self, session=None):
140 """
141 View for first-time app setup, to create admin user.
143 Template: ``/setup.mako``
145 This page is only meant for one-time use. As such, if the app
146 DB contains any users, this page will always redirect to the
147 home page.
149 However if no users exist yet, this will show a form which may
150 be used to create the first admin user. When finished, user
151 will be redirected to the login page.
153 .. note::
155 As long as there are no users in the DB, both the home and
156 login pages will automatically redirect to this one.
157 """
158 model = self.app.model
159 session = session or Session()
161 # nb. this view only available until first user is created
162 user = session.query(model.User).first()
163 if user:
164 return self.redirect(self.request.route_url('home'))
166 form = self.make_form(fields=['username', 'password', 'first_name', 'last_name'],
167 show_button_cancel=False,
168 show_button_reset=True)
169 form.set_widget('password', widgets.CheckedPasswordWidget())
170 form.set_required('first_name', False)
171 form.set_required('last_name', False)
173 if form.validate():
174 auth = self.app.get_auth_handler()
175 data = form.validated
177 # make user
178 user = auth.make_user(session=session, username=data['username'])
179 auth.set_user_password(user, data['password'])
181 # assign admin role
182 admin = auth.get_role_administrator(session)
183 user.roles.append(admin)
184 admin.notes = ("users in this role may \"become root\".\n\n"
185 "it's recommended not to grant other perms to this role.")
187 # initialize built-in roles
188 authed = auth.get_role_authenticated(session)
189 authed.notes = ("this role represents any user who *is* logged in.\n\n"
190 "you may grant any perms you like to it.")
191 anon = auth.get_role_anonymous(session)
192 anon.notes = ("this role represents any user who is *not* logged in.\n\n"
193 "you may grant any perms you like to it.")
195 # also make "Site Admin" role
196 site_admin_perms = [
197 'appinfo.list',
198 'appinfo.configure',
199 'people.list',
200 'people.create',
201 'people.view',
202 'people.edit',
203 'people.delete',
204 'roles.list',
205 'roles.create',
206 'roles.view',
207 'roles.edit',
208 'roles.edit_builtin',
209 'roles.delete',
210 'settings.list',
211 'settings.create',
212 'settings.view',
213 'settings.edit',
214 'settings.delete',
215 'settings.delete_bulk',
216 'upgrades.list',
217 'upgrades.create',
218 'upgrades.view',
219 'upgrades.edit',
220 'upgrades.delete',
221 'upgrades.execute',
222 'upgrades.download',
223 'upgrades.configure',
224 'users.list',
225 'users.create',
226 'users.view',
227 'users.edit',
228 'users.delete',
229 ]
230 admin2 = model.Role(name="Site Admin")
231 admin2.notes = ("this is the \"daily driver\" admin role.\n\n"
232 "you may grant any perms you like to it.")
233 session.add(admin2)
234 user.roles.append(admin2)
235 for perm in site_admin_perms:
236 auth.grant_permission(admin2, perm)
238 # maybe make person
239 if data['first_name'] or data['last_name']:
240 first = data['first_name']
241 last = data['last_name']
242 person = model.Person(first_name=first,
243 last_name=last,
244 full_name=(f"{first} {last}").strip())
245 session.add(person)
246 user.person = person
248 # send user to /login
249 self.request.session.flash("Account created! Please login below.")
250 return self.redirect(self.request.route_url('login'))
252 return {
253 'index_title': self.app.get_title(),
254 'form': form,
255 }
257 @classmethod
258 def defaults(cls, config):
259 cls._defaults(config)
261 @classmethod
262 def _defaults(cls, config):
264 config.add_wutta_permission_group('common', "(common)", overwrite=False)
266 # home page
267 config.add_route('home', '/')
268 config.add_view(cls, attr='home',
269 route_name='home',
270 renderer='/home.mako')
272 # forbidden
273 config.add_forbidden_view(cls, attr='forbidden_view',
274 renderer='/forbidden.mako')
276 # notfound
277 # nb. also, auto-correct URLs which require trailing slash
278 config.add_notfound_view(cls, attr='notfound_view',
279 append_slash=True,
280 renderer='/notfound.mako')
282 # feedback
283 config.add_route('feedback', '/feedback',
284 request_method='POST')
285 config.add_view(cls, attr='feedback',
286 route_name='feedback',
287 permission='common.feedback',
288 renderer='json')
289 config.add_wutta_permission('common', 'common.feedback',
290 "Send user feedback about the app")
292 # setup
293 config.add_route('setup', '/setup')
294 config.add_view(cls, attr='setup',
295 route_name='setup',
296 renderer='/setup.mako')
299def defaults(config, **kwargs):
300 base = globals()
302 CommonView = kwargs.get('CommonView', base['CommonView'])
303 CommonView.defaults(config)
306def includeme(config):
307 defaults(config)