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 +6,8 @@ Main application module | ||
6 | # python standard libraries | 6 | # python standard libraries |
7 | import asyncio | 7 | import asyncio |
8 | from contextlib import contextmanager # `with` statement in db sessions | 8 | from contextlib import contextmanager # `with` statement in db sessions |
9 | +import csv | ||
10 | +import io | ||
9 | import json | 11 | import json |
10 | import logging | 12 | import logging |
11 | from os import path | 13 | from os import path |
@@ -264,7 +266,7 @@ class App(): | @@ -264,7 +266,7 @@ class App(): | ||
264 | 266 | ||
265 | # ------------------------------------------------------------------------ | 267 | # ------------------------------------------------------------------------ |
266 | def event_test(self, uid, cmd, value): | 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 | if cmd == 'focus': | 270 | if cmd == 'focus': |
269 | logger.info('Student %s: focus %s', uid, value) | 271 | logger.info('Student %s: focus %s', uid, value) |
270 | elif cmd == 'size': | 272 | elif cmd == 'size': |
@@ -279,6 +281,21 @@ class App(): | @@ -279,6 +281,21 @@ class App(): | ||
279 | # def get_student_name(self, uid): | 281 | # def get_student_name(self, uid): |
280 | # return self.online[uid]['student']['name'] | 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 | def get_student_test(self, uid, default=None): | 299 | def get_student_test(self, uid, default=None): |
283 | '''get test from online student''' | 300 | '''get test from online student''' |
284 | return self.online[uid].get('test', default) | 301 | return self.online[uid].get('test', default) |
perguntations/serve.py
@@ -42,6 +42,7 @@ class WebApplication(tornado.web.Application): | @@ -42,6 +42,7 @@ class WebApplication(tornado.web.Application): | ||
42 | (r'/file', FileHandler), | 42 | (r'/file', FileHandler), |
43 | # (r'/root', MainHandler), # FIXME | 43 | # (r'/root', MainHandler), # FIXME |
44 | # (r'/ws', AdminSocketHandler), | 44 | # (r'/ws', AdminSocketHandler), |
45 | + (r'/adminwebservice', AdminWebservice), | ||
45 | (r'/studentwebservice', StudentWebservice), | 46 | (r'/studentwebservice', StudentWebservice), |
46 | (r'/', RootHandler), | 47 | (r'/', RootHandler), |
47 | ] | 48 | ] |
@@ -62,7 +63,10 @@ class WebApplication(tornado.web.Application): | @@ -62,7 +63,10 @@ class WebApplication(tornado.web.Application): | ||
62 | # ---------------------------------------------------------------------------- | 63 | # ---------------------------------------------------------------------------- |
63 | def admin_only(func): | 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 | @functools.wraps(func) | 71 | @functools.wraps(func) |
68 | async def wrapper(self, *args, **kwargs): | 72 | async def wrapper(self, *args, **kwargs): |
@@ -75,14 +79,14 @@ def admin_only(func): | @@ -75,14 +79,14 @@ def admin_only(func): | ||
75 | # ---------------------------------------------------------------------------- | 79 | # ---------------------------------------------------------------------------- |
76 | class BaseHandler(tornado.web.RequestHandler): | 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 | @property | 87 | @property |
82 | def testapp(self): | 88 | def testapp(self): |
83 | - ''' | ||
84 | - simplifies access to the application | ||
85 | - ''' | 89 | + '''simplifies access to the application''' |
86 | return self.application.testapp | 90 | return self.application.testapp |
87 | 91 | ||
88 | def get_current_user(self): | 92 | def get_current_user(self): |
@@ -155,8 +159,8 @@ class BaseHandler(tornado.web.RequestHandler): | @@ -155,8 +159,8 @@ class BaseHandler(tornado.web.RequestHandler): | ||
155 | 159 | ||
156 | class StudentWebservice(BaseHandler): | 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 | @tornado.web.authenticated | 166 | @tornado.web.authenticated |
@@ -167,6 +171,25 @@ class StudentWebservice(BaseHandler): | @@ -167,6 +171,25 @@ class StudentWebservice(BaseHandler): | ||
167 | value = json.loads(self.get_body_argument('value', None)) | 171 | value = json.loads(self.get_body_argument('value', None)) |
168 | self.testapp.event_test(uid, cmd, value) | 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 | class AdminHandler(BaseHandler): | 194 | class AdminHandler(BaseHandler): |
172 | '''Handle /admin''' | 195 | '''Handle /admin''' |
perguntations/static/js/admin.js
@@ -14,35 +14,27 @@ jQuery.postJSON = function(url, args) { | @@ -14,35 +14,27 @@ jQuery.postJSON = function(url, args) { | ||
14 | $(document).ready(function() { | 14 | $(document).ready(function() { |
15 | function button_handlers() { | 15 | function button_handlers() { |
16 | // button handlers (runs once) | 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 | // authorization checkboxes in the students_table: | 38 | // authorization checkboxes in the students_table: |
47 | $("tbody", "#students_table").on("change", "input", autorizeStudent); | 39 | $("tbody", "#students_table").on("change", "input", autorizeStudent); |
48 | } | 40 | } |
perguntations/templates/admin.html
@@ -68,24 +68,27 @@ | @@ -68,24 +68,27 @@ | ||
68 | <div class="container-fluid"> | 68 | <div class="container-fluid"> |
69 | 69 | ||
70 | <div class="jumbotron"> | 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 | </div> <!-- jumbotron --> | 79 | </div> <!-- jumbotron --> |
77 | 80 | ||
78 | <table class="table table-sm table-striped" style="width:100%" id="students_table"> | 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 | </table> | 92 | </table> |
90 | 93 | ||
91 | </div> <!-- container --> | 94 | </div> <!-- container --> |