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