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

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""" 

26 

27import logging 

28 

29import colander 

30from pyramid.renderers import render 

31 

32from wuttaweb.views import View 

33from wuttaweb.forms import widgets 

34from wuttaweb.db import Session 

35 

36 

37log = logging.getLogger(__name__) 

38 

39 

40class CommonView(View): 

41 """ 

42 Common views shared by all apps. 

43 """ 

44 

45 def home(self, session=None): 

46 """ 

47 Home page view. 

48 

49 Template: ``/home.mako`` 

50 

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() 

56 

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 

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')) 

66 

67 return { 

68 'index_title': self.app.get_title(), 

69 } 

70 

71 def forbidden_view(self): 

72 """ 

73 This view is shown when a request triggers a 403 Forbidden error. 

74 

75 Template: ``/forbidden.mako`` 

76 """ 

77 return {'index_title': self.app.get_title()} 

78 

79 def notfound_view(self): 

80 """ 

81 This view is shown when a request triggers a 404 Not Found error. 

82 

83 Template: ``/notfound.mako`` 

84 """ 

85 return {'index_title': self.app.get_title()} 

86 

87 def feedback(self): 

88 """ """ 

89 model = self.app.model 

90 session = Session() 

91 

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)} 

99 

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 

106 

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__} 

113 

114 return {'ok': True} 

115 

116 def feedback_make_schema(self): 

117 """ """ 

118 schema = colander.Schema() 

119 

120 schema.add(colander.SchemaNode(colander.String(), 

121 name='referrer')) 

122 

123 schema.add(colander.SchemaNode(colander.String(), 

124 name='user_uuid', 

125 missing=None)) 

126 

127 schema.add(colander.SchemaNode(colander.String(), 

128 name='user_name')) 

129 

130 schema.add(colander.SchemaNode(colander.String(), 

131 name='message')) 

132 

133 return schema 

134 

135 def feedback_send(self, context): 

136 """ """ 

137 self.app.send_email('feedback', context) 

138 

139 def setup(self, session=None): 

140 """ 

141 View for first-time app setup, to create admin user. 

142 

143 Template: ``/setup.mako`` 

144 

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. 

148 

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. 

152 

153 .. note:: 

154 

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() 

160 

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')) 

165 

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) 

172 

173 if form.validate(): 

174 auth = self.app.get_auth_handler() 

175 data = form.validated 

176 

177 # make user 

178 user = auth.make_user(session=session, username=data['username']) 

179 auth.set_user_password(user, data['password']) 

180 

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.") 

186 

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.") 

194 

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) 

237 

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 

247 

248 # send user to /login 

249 self.request.session.flash("Account created! Please login below.") 

250 return self.redirect(self.request.route_url('login')) 

251 

252 return { 

253 'index_title': self.app.get_title(), 

254 'form': form, 

255 } 

256 

257 @classmethod 

258 def defaults(cls, config): 

259 cls._defaults(config) 

260 

261 @classmethod 

262 def _defaults(cls, config): 

263 

264 config.add_wutta_permission_group('common', "(common)", overwrite=False) 

265 

266 # home page 

267 config.add_route('home', '/') 

268 config.add_view(cls, attr='home', 

269 route_name='home', 

270 renderer='/home.mako') 

271 

272 # forbidden 

273 config.add_forbidden_view(cls, attr='forbidden_view', 

274 renderer='/forbidden.mako') 

275 

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') 

281 

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") 

291 

292 # setup 

293 config.add_route('setup', '/setup') 

294 config.add_view(cls, attr='setup', 

295 route_name='setup', 

296 renderer='/setup.mako') 

297 

298 

299def defaults(config, **kwargs): 

300 base = globals() 

301 

302 CommonView = kwargs.get('CommonView', base['CommonView']) 

303 CommonView.defaults(config) 

304 

305 

306def includeme(config): 

307 defaults(config)