Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Starts an application. 

5""" 

6import os 

7from starlette.applications import Starlette 

8from starlette.staticfiles import StaticFiles 

9from starlette.responses import RedirectResponse, PlainTextResponse 

10# from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware 

11from starlette.middleware.trustedhost import TrustedHostMiddleware 

12from starlette.templating import Jinja2Templates 

13from ..common import LogApp, AuthentificationAnswers 

14from ..display import DisplayQuestionChoiceHTML 

15from ...tests import get_game 

16 

17 

18class QCMApp(LogApp, AuthentificationAnswers): 

19 """ 

20 Implements routes for a web application. 

21 

22 .. faqref:: 

23 :title: Which server to server starlette application? 

24 :lid: faq-server-app-starlette 

25 

26 :epkg:`starlette` does not implement a webserver, it 

27 only provides a way to map urls to answers and to interect 

28 with the user. To launch a server serving :epkg:`starlette` 

29 applications, there is module :epkg:`uvicorn` but it does not 

30 implement a secured connection. There is :epkg:`hypercorn` 

31 which should support it. Other alternatives are described 

32 on `starlette/installation <https://www.starlette.io/#installation>`_. 

33 """ 

34 

35 def __init__(self, 

36 # log parameters 

37 secret_log=None, folder='.', 

38 # authentification parameters 

39 max_age=14 * 24 * 60 * 60, cookie_key=None, 

40 cookie_name="mathenjeu", cookie_domain="127.0.0.1", 

41 cookie_path="/", 

42 # application parameters 

43 title="Web Application MathEnJeu", short_title="MathEnJeu", 

44 page_doc="http://www.xavierdupre.fr/app/mathenjeu/helpsphinx/", 

45 secure=False, display=None, fct_game=None, games=None, 

46 middles=None, debug=False, userpwd=None): 

47 """ 

48 @param secret_log to encrypt log (None to ignore) 

49 @param folder folder where to write the logs (None to disable the logging) 

50 

51 @param max_age cookie's duration in seconds 

52 @param cookie_key to encrypt information in the cookie (cannot be None) 

53 @param cookie_name name of the session cookie 

54 @param cookie_domain cookie is valid for this path only, also defines the 

55 domain of the web app (its url) 

56 @param cookie_path path of the cookie once storeds 

57 @param secure use secured connection for cookies 

58 

59 @param title title 

60 @param short_title short application title 

61 @param page_doc documentation page 

62 @param display display such as @see cl DisplayQuestionChoiceHTML (default value) 

63 @param fct_game function *lambda name:* @see cl ActivityGroup 

64 @param games defines which games is available as a dictionary 

65 ``{ game_id: (game name, first page id) }`` 

66 @param middles middles ware, list of couple ``[(class, **kwargs)]`` 

67 where *kwargs* are the parameter constructor 

68 @param userpwd users are authentified with any alias but a common password 

69 @param debug display debug information (:epkg:`starlette` option) 

70 """ 

71 if title is None: 

72 raise ValueError("title cannot be None.") 

73 if short_title is None: 

74 raise ValueError("short_title cannot be None.") 

75 if display is None: 

76 display = DisplayQuestionChoiceHTML() 

77 if fct_game is None: 

78 fct_game = get_game 

79 if games is None: 

80 games = dict(test_qcm1=('Maths', 0), 

81 test_ml1=('ML', 0)) 

82 

83 this = os.path.abspath(os.path.dirname(__file__)) 

84 templates = os.path.join(this, "templates") 

85 statics = os.path.join(this, "statics") 

86 if not os.path.exists(statics): 

87 raise FileNotFoundError("Unable to find '{0}'".format(statics)) 

88 if not os.path.exists(templates): 

89 raise FileNotFoundError("Unable to find '{0}'".format(templates)) 

90 

91 login_page = "login.html" 

92 notauth_page = "notauthorized.html" 

93 auth_page = "authorized.html" 

94 redirect_logout = "/" 

95 app = Starlette(debug=debug) 

96 

97 AuthentificationAnswers.__init__(self, app, login_page=login_page, auth_page=auth_page, 

98 notauth_page=notauth_page, redirect_logout=redirect_logout, 

99 max_age=max_age, cookie_name=cookie_name, cookie_key=cookie_key, 

100 cookie_domain=cookie_domain, cookie_path=cookie_path, 

101 page_context=self.page_context, userpwd=userpwd) 

102 LogApp.__init__(self, folder=folder, secret_log=secret_log, 

103 fct_session=self.get_session) 

104 

105 self.title = title 

106 self.short_title = short_title 

107 self.page_doc = page_doc 

108 self.display = display 

109 self.get_game = fct_game 

110 self.games = games 

111 self.templates = Jinja2Templates(directory=templates) 

112 

113 if middles is not None: 

114 for middle, kwargs in middles: 

115 app.add_middleware(middle, **kwargs) 

116 app.add_middleware(TrustedHostMiddleware, 

117 allowed_hosts=[cookie_domain]) 

118 # app.add_middleware(HTTPSRedirectMiddleware) 

119 

120 app.mount('/static', StaticFiles(directory=statics), name='static') 

121 app.add_route('/login', self.login) 

122 app.add_route('/logout', self.logout) 

123 app.add_route('/error', self.on_error) 

124 app.add_route('/authenticate', self.authenticate, methods=['POST']) 

125 app.add_route('/answer', self.answer, methods=['POST', 'GET']) 

126 app.add_exception_handler(404, self.not_found) 

127 app.add_exception_handler(500, self.server_error) 

128 app.add_event_handler("startup", self.startup) 

129 app.add_event_handler("shutdown", self.cleanup) 

130 app.add_route('/', self.main) 

131 app.add_route('/qcm', self.qcm, methods=['GET', 'POST']) 

132 app.add_route('/last', self.lastpage, methods=['GET', 'POST']) 

133 app.add_route('/event', self.event, methods=['GET', 'POST']) 

134 self.info("[QCMApp.create_app] create application", None) 

135 

136 ######### 

137 # common 

138 ######### 

139 

140 def page_context(self, **kwargs): 

141 """ 

142 Returns the page context before applying any template. 

143 

144 @param kwargs arguments 

145 @return parameters 

146 """ 

147 res = dict(title=self.title, short_title=self.short_title, 

148 page_doc=self.page_doc) 

149 res.update(kwargs) 

150 self.info('[QCMApp] page_context', str(res)) 

151 return res 

152 

153 def startup(self): 

154 """ 

155 Startups. 

156 """ 

157 self.info('[QCMApp] startup', None) 

158 

159 def cleanup(self): 

160 """ 

161 Cleans up. 

162 """ 

163 self.info('[QCMApp] cleanup', None) 

164 

165 def unlogged_response(self, request, session): 

166 """ 

167 Returns an answer for somebody looking to access 

168 the questions without being authentified. 

169 """ 

170 self.log_event("home-unlogged", request, session=session) 

171 context = {'request': request} 

172 context.update(self.page_context(**session)) 

173 self.info('[QCMApp] unlogged_response', str(context)) 

174 return self.templates.TemplateResponse('notlogged.html', context) 

175 

176 def unknown_game(self, request, session): 

177 """ 

178 Returns an answer for somebody looking to access 

179 the questions without being authentified. 

180 """ 

181 self.log_event("home-nogame", request, session=session) 

182 context = {'request': request} 

183 context.update(self.page_context(**session)) 

184 return self.templates.TemplateResponse('nogame.html', context) 

185 

186 ######## 

187 # route 

188 ######## 

189 

190 async def main(self, request): 

191 """ 

192 Defines the main page. 

193 """ 

194 session = self.get_session(request, notnone=True) 

195 if 'alias' in session: 

196 self.log_event("home-logged", request, session=session) 

197 context = {'request': request} 

198 context.update(self.page_context(games=self.games, **session)) 

199 page = self.templates.TemplateResponse('index.html', context) 

200 self.info('[QCMApp] main', str(page)) 

201 return page 

202 return self.unlogged_response(request, session) 

203 

204 async def on_error(self, request): 

205 """ 

206 An example error. 

207 """ 

208 self.log_any('[error]', "?", request) 

209 raise RuntimeError("Oh no") 

210 

211 async def not_found(self, request, exc): 

212 """ 

213 Returns an :epkg:`HTTP 404` page. 

214 """ 

215 context = {'request': request} 

216 context.update(self.page_context()) 

217 return self.templates.TemplateResponse('404.html', context, status_code=404) 

218 

219 async def server_error(self, request, exc): 

220 """ 

221 Returns an :epkg:`HTTP 500` page. 

222 """ 

223 context = {'request': request} 

224 context.update(self.page_context()) 

225 return self.templates.TemplateResponse('500.html', context, status_code=500) 

226 

227 async def qcm(self, request): 

228 """ 

229 Defines the main page. 

230 """ 

231 session = self.get_session(request, notnone=True) 

232 if 'alias' in session: 

233 game = request.query_params.get('game', None) 

234 if game is None: 

235 return self.unknown_game(request, session) 

236 obj_game = self.get_game(game) 

237 self.info('[QCMApp] qcm.1', str(obj_game)) 

238 if isinstance(obj_game, str): 

239 raise RuntimeError( 

240 "obj_game for '{0}' cannot be string".format(game)) 

241 qn = request.query_params.get('qn', 0) 

242 data = dict(game=game, qn=qn) 

243 events = request.query_params.get('events', None) 

244 if events: 

245 data['events'] = events 

246 self.log_event("qcm", request, session=session, **data) 

247 disp = self.display 

248 context = disp.get_context(obj_game, qn) 

249 context.update(session) 

250 context['game'] = game 

251 if events: 

252 context['events'] = events 

253 context_req = {'request': request} 

254 context_req.update(self.page_context(**context)) 

255 page = self.templates.TemplateResponse('qcm.html', context_req) 

256 return page 

257 return self.unlogged_response(request, session) 

258 

259 async def answer(self, request): 

260 """ 

261 Captures an answer. 

262 

263 @param request request 

264 @return response 

265 """ 

266 try: 

267 fo = await request.form() 

268 except Exception as e: 

269 raise RuntimeError( # pylint: disable=W0707 

270 "Unable to read answer due to '{0}'".format(e)) 

271 session = self.get_session(request, notnone=True) 

272 values = {k: v for k, v in fo.items()} # pylint: disable=R1721 

273 ps = request.query_params 

274 values.update(ps) 

275 self.log_event("answer", request, session=session, data=values) 

276 if 'next' in values and 'game' in values and values['next'] in (None, 'None'): 

277 response = RedirectResponse(url='/last?game=' + values['game']) 

278 else: 

279 response = RedirectResponse( 

280 url='/qcm?game={0}&qn={1}'.format(values.get('game', ''), values.get('next', ''))) 

281 return response 

282 

283 async def lastpage(self, request): 

284 """ 

285 Defines the last page. 

286 """ 

287 session = self.get_session(request, notnone=True) 

288 ps = request.query_params 

289 self.log_event("finish", request, session=session, data=ps) 

290 context = {'request': request, 'alias': session.get('alias')} 

291 context.update(self.page_context()) 

292 return self.templates.TemplateResponse('lastpage.html', context) 

293 

294 ######### 

295 # event route 

296 ######### 

297 

298 async def event(self, request): 

299 """ 

300 This route does not return anything interesting except 

301 a blank page, but it logs 

302 """ 

303 session = self.get_session(request, notnone=True) 

304 ps = request.query_params 

305 tostr = ','.join('{0}:{1}'.format(k, v) for k, v in sorted(ps.items())) 

306 self.log_event("event", request, session=session, events=[tostr]) 

307 return PlainTextResponse("")