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
18class QCMApp(LogApp, AuthentificationAnswers):
19 """
20 Implements routes for a web application.
22 .. faqref::
23 :title: Which server to server starlette application?
24 :lid: faq-server-app-starlette
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 """
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)
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
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))
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))
91 login_page = "login.html"
92 notauth_page = "notauthorized.html"
93 auth_page = "authorized.html"
94 redirect_logout = "/"
95 app = Starlette(debug=debug)
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)
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)
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)
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)
136 #########
137 # common
138 #########
140 def page_context(self, **kwargs):
141 """
142 Returns the page context before applying any template.
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
153 def startup(self):
154 """
155 Startups.
156 """
157 self.info('[QCMApp] startup', None)
159 def cleanup(self):
160 """
161 Cleans up.
162 """
163 self.info('[QCMApp] cleanup', None)
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)
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)
186 ########
187 # route
188 ########
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)
204 async def on_error(self, request):
205 """
206 An example error.
207 """
208 self.log_any('[error]', "?", request)
209 raise RuntimeError("Oh no")
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)
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)
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)
259 async def answer(self, request):
260 """
261 Captures an answer.
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
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)
294 #########
295 # event route
296 #########
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("")