Commit 35e20aae9a759026652373e935c24f56c9bf4467

Authored by Miguel Barão
1 parent 461f8e1e
Exists in master and in 1 other branch dev

adds button to /admin page to download CSV file with grades.

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