Commit 35e20aae9a759026652373e935c24f56c9bf4467
1 parent
461f8e1e
Exists in
master
and in
1 other branch
adds button to /admin page to download CSV file with grades.
Showing
4 changed files
with
87 additions
and
52 deletions
Show diff stats
perguntations/app.py
... | ... | @@ -6,6 +6,8 @@ Main application module |
6 | 6 | # python standard libraries |
7 | 7 | import asyncio |
8 | 8 | from contextlib import contextmanager # `with` statement in db sessions |
9 | +import csv | |
10 | +import io | |
9 | 11 | import json |
10 | 12 | import logging |
11 | 13 | from os import path |
... | ... | @@ -264,7 +266,7 @@ class App(): |
264 | 266 | |
265 | 267 | # ------------------------------------------------------------------------ |
266 | 268 | def event_test(self, uid, cmd, value): |
267 | - '''handle browser events the occur during the test''' | |
269 | + '''handles browser events the occur during the test''' | |
268 | 270 | if cmd == 'focus': |
269 | 271 | logger.info('Student %s: focus %s', uid, value) |
270 | 272 | elif cmd == 'size': |
... | ... | @@ -279,6 +281,21 @@ class App(): |
279 | 281 | # def get_student_name(self, uid): |
280 | 282 | # return self.online[uid]['student']['name'] |
281 | 283 | |
284 | + def get_test_csv(self): | |
285 | + '''generates a CSV with the grades of the test''' | |
286 | + with self.db_session() as sess: | |
287 | + grades = sess.query(Test.student_id, Test.grade, | |
288 | + Test.starttime, Test.finishtime)\ | |
289 | + .filter(Test.ref == self.testfactory['ref'])\ | |
290 | + .order_by(Test.student_id)\ | |
291 | + .all() | |
292 | + | |
293 | + csvstr = io.StringIO() | |
294 | + writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL) | |
295 | + writer.writerow(('Número', 'Nota', 'Início', 'Fim')) | |
296 | + writer.writerows(grades) | |
297 | + return csvstr.getvalue() | |
298 | + | |
282 | 299 | def get_student_test(self, uid, default=None): |
283 | 300 | '''get test from online student''' |
284 | 301 | return self.online[uid].get('test', default) | ... | ... |
perguntations/serve.py
... | ... | @@ -42,6 +42,7 @@ class WebApplication(tornado.web.Application): |
42 | 42 | (r'/file', FileHandler), |
43 | 43 | # (r'/root', MainHandler), # FIXME |
44 | 44 | # (r'/ws', AdminSocketHandler), |
45 | + (r'/adminwebservice', AdminWebservice), | |
45 | 46 | (r'/studentwebservice', StudentWebservice), |
46 | 47 | (r'/', RootHandler), |
47 | 48 | ] |
... | ... | @@ -62,7 +63,10 @@ class WebApplication(tornado.web.Application): |
62 | 63 | # ---------------------------------------------------------------------------- |
63 | 64 | def admin_only(func): |
64 | 65 | ''' |
65 | - Decorator used to restrict access to the administrator | |
66 | + Decorator used to restrict access to the administrator. For example: | |
67 | + | |
68 | + @admin_only() | |
69 | + def get(self): ... | |
66 | 70 | ''' |
67 | 71 | @functools.wraps(func) |
68 | 72 | async def wrapper(self, *args, **kwargs): |
... | ... | @@ -75,14 +79,14 @@ def admin_only(func): |
75 | 79 | # ---------------------------------------------------------------------------- |
76 | 80 | class BaseHandler(tornado.web.RequestHandler): |
77 | 81 | ''' |
78 | - Base handler. Other handlers will inherit this one. | |
82 | + Handlers should inherit this one instead of tornado.web.RequestHandler. | |
83 | + It automatically gets the user cookie, which is required to identify the | |
84 | + user in most handlers. | |
79 | 85 | ''' |
80 | 86 | |
81 | 87 | @property |
82 | 88 | def testapp(self): |
83 | - ''' | |
84 | - simplifies access to the application | |
85 | - ''' | |
89 | + '''simplifies access to the application''' | |
86 | 90 | return self.application.testapp |
87 | 91 | |
88 | 92 | def get_current_user(self): |
... | ... | @@ -155,8 +159,8 @@ class BaseHandler(tornado.web.RequestHandler): |
155 | 159 | |
156 | 160 | class StudentWebservice(BaseHandler): |
157 | 161 | ''' |
158 | - Receive ajax from students in the test: | |
159 | - focus, unfocus | |
162 | + Receive ajax from students in the test in response from focus, unfocus and | |
163 | + resize events. | |
160 | 164 | ''' |
161 | 165 | |
162 | 166 | @tornado.web.authenticated |
... | ... | @@ -167,6 +171,25 @@ class StudentWebservice(BaseHandler): |
167 | 171 | value = json.loads(self.get_body_argument('value', None)) |
168 | 172 | self.testapp.event_test(uid, cmd, value) |
169 | 173 | |
174 | + | |
175 | +# ---------------------------------------------------------------------------- | |
176 | +class AdminWebservice(BaseHandler): | |
177 | + ''' | |
178 | + Receive ajax requests from admin | |
179 | + ''' | |
180 | + | |
181 | + @tornado.web.authenticated | |
182 | + @admin_only | |
183 | + async def get(self): | |
184 | + '''admin webservices that do not change state''' | |
185 | + cmd = self.get_query_argument('cmd') | |
186 | + if cmd == 'testcsv': | |
187 | + self.set_header('Content-Type', 'text/csv') | |
188 | + self.set_header('content-Disposition', | |
189 | + 'attachment; filename=notas.csv') | |
190 | + self.write(self.testapp.get_test_csv()) | |
191 | + await self.flush() | |
192 | + | |
170 | 193 | # ---------------------------------------------------------------------------- |
171 | 194 | class AdminHandler(BaseHandler): |
172 | 195 | '''Handle /admin''' | ... | ... |
perguntations/static/js/admin.js
... | ... | @@ -14,35 +14,27 @@ jQuery.postJSON = function(url, args) { |
14 | 14 | $(document).ready(function() { |
15 | 15 | function button_handlers() { |
16 | 16 | // button handlers (runs once) |
17 | - $("#allow_all").click( | |
18 | - function() { | |
19 | - $(":checkbox").prop("checked", true).trigger('change'); | |
20 | - } | |
21 | - ); | |
22 | - $("#deny_all").click( | |
23 | - function() { | |
24 | - $(":checkbox").prop("checked", false).trigger('change'); | |
25 | - } | |
26 | - ); | |
27 | - $("#reset_password").click( | |
28 | - function () { | |
29 | - $.postJSON("/admin", { | |
30 | - "cmd": "reset_password", | |
31 | - "value": $("#reset_number").val() | |
32 | - }); | |
33 | - } | |
34 | - ); | |
35 | - $("#inserir_novo_aluno").click( | |
36 | - function () { | |
37 | - $.postJSON("/admin", { | |
38 | - "cmd": "insert_student", | |
39 | - "value": JSON.stringify({ | |
40 | - "number": $("#novo_numero").val(), | |
41 | - "name": $("#novo_nome").val() | |
42 | - }) | |
43 | - }); | |
44 | - } | |
45 | - ); | |
17 | + $("#allow_all").click(function() { | |
18 | + $(":checkbox").prop("checked", true).trigger('change'); | |
19 | + }); | |
20 | + $("#deny_all").click(function() { | |
21 | + $(":checkbox").prop("checked", false).trigger('change'); | |
22 | + }); | |
23 | + $("#reset_password").click(function () { | |
24 | + $.postJSON("/admin", { | |
25 | + "cmd": "reset_password", | |
26 | + "value": $("#reset_number").val() | |
27 | + }); | |
28 | + }); | |
29 | + $("#inserir_novo_aluno").click(function () { | |
30 | + $.postJSON("/admin", { | |
31 | + "cmd": "insert_student", | |
32 | + "value": JSON.stringify({ | |
33 | + "number": $("#novo_numero").val(), | |
34 | + "name": $("#novo_nome").val() | |
35 | + }) | |
36 | + }); | |
37 | + }); | |
46 | 38 | // authorization checkboxes in the students_table: |
47 | 39 | $("tbody", "#students_table").on("change", "input", autorizeStudent); |
48 | 40 | } | ... | ... |
perguntations/templates/admin.html
... | ... | @@ -68,24 +68,27 @@ |
68 | 68 | <div class="container-fluid"> |
69 | 69 | |
70 | 70 | <div class="jumbotron"> |
71 | - <h3 id="title"></h3> | |
72 | - Ref: <span id="ref"></span><br> | |
73 | - Enunciado: <span id="filename"></span><br> | |
74 | - Base de dados: <span id="database"></span><br> | |
75 | - Testes submetidos: <span id="answers_dir"></span> | |
71 | + <h3 id="title"></h3> | |
72 | + Ref: <span id="ref"></span><br> | |
73 | + Enunciado: <span id="filename"></span><br> | |
74 | + Base de dados: <span id="database"></span><br> | |
75 | + Testes submetidos: <span id="answers_dir"></span> | |
76 | + <p> | |
77 | + <a href="/adminwebservice?cmd=testcsv" class="btn btn-primary">Obter CSV com as notas</a> | |
78 | + </p> | |
76 | 79 | </div> <!-- jumbotron --> |
77 | 80 | |
78 | 81 | <table class="table table-sm table-striped" style="width:100%" id="students_table"> |
79 | - <thead class="thead thead-light"> | |
80 | - <tr> | |
81 | - <th>#</th> | |
82 | - <th>Ok</th> | |
83 | - <th>Número</th> | |
84 | - <th>Nome</th> | |
85 | - <th>Estado</th> | |
86 | - <th>Nota</th> | |
87 | - </tr> | |
88 | - </thead> | |
82 | + <thead class="thead thead-light"> | |
83 | + <tr> | |
84 | + <th>#</th> | |
85 | + <th>Ok</th> | |
86 | + <th>Número</th> | |
87 | + <th>Nome</th> | |
88 | + <th>Estado</th> | |
89 | + <th>Nota</th> | |
90 | + </tr> | |
91 | + </thead> | |
89 | 92 | </table> |
90 | 93 | |
91 | 94 | </div> <!-- container --> | ... | ... |