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