Commit 2780d0843fcfbcce121dddca36df7ab9079ef949

Authored by Miguel Barão
1 parent 1699423a
Exists in master and in 1 other branch dev

- changed database table "test" to include title and JSON filename where the test was saved.

- fixed bug where incorrect test results were shown to student after /correct.
BUGS.md
1 1  
2 2 # BUGS
3 3  
4   -- implementar practice mode.
5 4 - usar thread.Lock para aceder a variaveis de estado?
  5 +- visualizar um teste ja realizado na página de administração
6 6  
7 7 # TODO
8 8  
  9 +- usar http://wtfforms.com para radio e checkboxes
  10 +- usar http://fontawesome.io/examples/ em vez dos do bootstrap3
  11 +- implementar practice mode.
9 12 - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova.
10 13 - detectar scroll e enviar posição para servidor (analise de scroll para detectar copianço? ou simplesmente para analisar como os alunos percorrem o teste)
11   -- detectar se janela perde focus e alertar o prof (http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active)
12 14 - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?).
13 15 - aviso na pagina principal para quem usa browser da treta
14 16 - permitir varios testes, aluno escolhe qual o teste que quer fazer.
15 17 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput
16 18 - perguntas para professor corrigir mais tarde.
17   -- visualizar um teste ja realizado na página de administração
18 19 - fazer uma calculadora javascript e por no menu. surge como modal
19 20 - GeoIP?
20 21 - alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...)
... ... @@ -22,6 +23,8 @@
22 23  
23 24 # FIXED
24 25  
  26 +- Depois da correcção, mostra testes realizados que não foram realizados pelo próprio
  27 +- detectar se janela perde focus e alertar o prof (http://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active)
25 28 - server nao esta a receber eventos focus/blur dos utilizadores diferentes de '0', estranho...
26 29 - permitir adicionar imagens nas perguntas.
27 30 - detect_unfocus.js so funciona se estiver inline no html. porquê???
... ...
app.py
... ... @@ -131,22 +131,25 @@ class App(object):
131 131 grade = t.correct()
132 132 logger.info('Student {0}: finished with {1} points.'.format(uid, grade))
133 133  
134   - if t['save_answers']:
135   - fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json'
136   - fpath = path.abspath(path.join(t['answers_dir'], fname))
137   - t.save_json(fpath)
  134 + # save JSON with the test
  135 + fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json'
  136 + fpath = path.abspath(path.join(t['answers_dir'], fname))
  137 + t.save_json(fpath)
138 138  
  139 + # insert test and questions into database
139 140 with self.db_session() as s:
140 141 s.add(Test(
141 142 ref=t['ref'],
  143 + title=t['title'],
142 144 grade=t['grade'],
143 145 starttime=str(t['start_time']),
144 146 finishtime=str(t['finish_time']),
  147 + filename=fpath,
145 148 student_id=t['student']['number']))
146 149 s.add_all([Question(
147 150 ref=q['ref'],
148 151 grade=q['grade'],
149   - starttime='',
  152 + starttime=str(t['start_time']),
150 153 finishtime=str(t['finish_time']),
151 154 student_id=t['student']['number'],
152 155 test_id=t['ref']) for q in t['questions'] if 'grade' in q])
... ... @@ -176,9 +179,12 @@ class App(object):
176 179 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']}
177 180 def get_student_grades_from_all_tests(self, uid):
178 181 with self.db_session() as s:
179   - r = s.query(Test).filter(Student.id == uid).all()
180   - return [(t.id, t.grade, t.finishtime) for t in r]
181   -
  182 + r = s.query(Test).filter(Test.student_id == uid).all()
  183 + return [(t.title, t.grade, t.finishtime) for t in r]
  184 + # def get_saved_tests_filenames(self):
  185 + # with self.db_session() as s:
  186 + # rr = s.query(Test).filter(Test.ref == self.testfactory['ref']).all()
  187 + # return [(r.student_id, r.finishtime, r.grade) for r in rr]
182 188 def get_online_students(self):
183 189 # [('uid', 'name', 'starttime')]
184 190 return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0']
... ...
models.py
... ... @@ -29,9 +29,11 @@ class Test(Base):
29 29 __tablename__ = 'tests'
30 30 id = Column(Integer, primary_key=True) # auto_increment
31 31 ref = Column(String)
  32 + title = Column(String)
32 33 grade = Column(Float)
33 34 starttime = Column(String)
34 35 finishtime = Column(String)
  36 + filename = Column(String)
35 37 student_id = Column(String, ForeignKey('students.id'))
36 38  
37 39 # ---
... ...
serve.py
... ... @@ -198,7 +198,6 @@ class Root(object):
198 198 # text - always returns string. no answer '', otherwise 'dskdjs'
199 199 uid = cherrypy.session.get(SESSION_KEY)
200 200 name = self.app.get_student_name(uid)
201   - title = self.app.get_test(uid)['title']
202 201 qq = self.app.get_test_qtypes(uid) # {'q1_ref': 'checkbox', ...}
203 202  
204 203 # each question that is marked to be classified must have an answer.
... ... @@ -215,6 +214,7 @@ class Root(object):
215 214 ans[qref] = a
216 215  
217 216 grade = self.app.correct_test(uid, ans)
  217 + t = self.app.get_test(uid)
218 218 self.app.logout(uid)
219 219  
220 220 # --- Expire session
... ... @@ -223,9 +223,7 @@ class Root(object):
223 223  
224 224 # --- Show result to student
225 225 return self.template['grade'].render(
226   - title=title,
227   - student_id=uid + ' - ' + name,
228   - grade=grade,
  226 + t=t,
229 227 allgrades=self.app.get_student_grades_from_all_tests(uid)
230 228 )
231 229  
... ... @@ -267,6 +265,14 @@ class Root(object):
267 265 def admin(self, **reset_pw):
268 266 return self.template['admin'].render()
269 267  
  268 + # --- REVIEW -------------------------------------------------------------
  269 + @cherrypy.expose
  270 + @require(name_is('0'))
  271 + def review(self, uid=None):
  272 + if uid is None:
  273 + # get show list of files
  274 + pass
  275 +
270 276 # ============================================================================
271 277 def parse_arguments():
272 278 argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.')
... ...
templates/grade.html
... ... @@ -4,34 +4,17 @@
4 4 <meta charset="UTF-8">
5 5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 6 <meta name="viewport" content="width=device-width, initial-scale=1">
7   - <title> ${title} </title>
  7 + <title> Teste </title>
8 8 <link rel="icon" href="/static/favicon.ico">
9 9  
10 10 <!-- Bootstrap -->
11 11 <link rel="stylesheet" href="/static/css/bootstrap.min.css">
12 12 <link rel="stylesheet" href="/static/css/bootstrap-theme.min.css"> <!-- optional -->
13   -
14   -<!-- <link rel="stylesheet" href="/js/jquery/jquery.mobile-1.4.5.min.css" />
15   - <script src="/js/jquery/jquery-2.1.1.min.js"></script>
16   - <script src="/js/jquery/jquery.mobile-1.4.5.min.js"></script>
17   - -->
  13 + <link rel="stylesheet" href="/static/css/test.css">
18 14  
19 15 <script src="/static/js/jquery.min.js"></script>
20 16 <script src="/static/js/bootstrap.min.js"></script>
21   -
22   - <style>
23   - /* Fixes navigation panel overlaying content */
24   - body {
25   - padding-top: 80px;
26   - background: #aaa;
27   - }
28   - .drop-shadow {
29   - -webkit-box-shadow: 0 0 5px 2px rgba(0, 0, 0, .5);
30   - box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2);
31   - border-radius:5px;
32   - }
33   - </style>
34   - </head>
  17 +</head>
35 18 <!-- ===================================================================== -->
36 19 <body>
37 20  
... ... @@ -43,13 +26,20 @@
43 26 <span class="icon-bar"></span>
44 27 <span class="icon-bar"></span>
45 28 </button>
46   - <a class="navbar-brand" href="#">UÉvora</a>
  29 + <a class="navbar-brand" href="#">
  30 + ${t['title']}
  31 + </a>
47 32 </div>
  33 +
48 34 <div class="collapse navbar-collapse" id="myNavbar">
  35 +
49 36 <ul class="nav navbar-nav navbar-right">
50 37 <li class="dropdown">
51 38 <a class="dropdown-toggle" data-toggle="dropdown" href="#">
52   - ${student_id} <span class="caret"></span>
  39 + <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
  40 + <span id="name">${t['student']['name']}</span>
  41 + (<span id="number">${t['student']['number']}</span>)
  42 + <!-- <span class="caret"></span> -->
53 43 </a>
54 44 </li>
55 45 </ul>
... ... @@ -60,46 +50,49 @@
60 50 <div class="container">
61 51 <div class="jumbotron drop-shadow">
62 52 <h1>Resultado</h1>
63   - <p>Teve <strong>${'{}'.format(grade)}</strong> valores na escala de 0 a 20.</p>
  53 + <p>Teve <strong>${t['grade']}</strong> valores na escala de 0 a 20.</p>
64 54 <p>A prova está terminada, pode fechar o browser e desligar o computador.</p>
65 55 </div>
66 56  
67 57 <div class="panel panel-default drop-shadow">
68 58 <div class="panel-heading">
69   - Testes realizados
  59 + Provas realizadas até ao momento
70 60 </div>
71   - <table class="table table-condensed">
72   - <thead>
73   - <tr>
74   - <th>Data</th>
75   - <th>Hora</th>
76   - <th>Teste</th>
77   - <th>Nota (0-20)</th>
78   - </tr>
79   - </thead>
80   - <tbody>
81   - % for g in allgrades:
  61 + <div class="panel-body">
  62 +
  63 + <table class="table table-condensed">
  64 + <thead>
82 65 <tr>
83   - <td>${g[2][:10]}</td> <!-- data -->
84   - <td>${g[2][11:19]}</td> <!-- hora -->
85   - <td>${g[0]}</td> <!-- teste -->
86   - <td>
87   - <div class="progress">
88   - % if g[1] < 10:
89   - <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
90   - % elif g[1] < 15:
91   - <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
92   - % else:
93   - <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
94   - % endif
95   - ${'{:.1f}'.format(g[1])}
96   - </div>
97   - </div>
98   - </td>
  66 + <th>Prova</th>
  67 + <th>Data</th>
  68 + <th>Hora</th>
  69 + <th>Nota (0-20)</th>
99 70 </tr>
100   - % endfor
101   - </tbody>
102   - </table>
  71 + </thead>
  72 + <tbody>
  73 + % for g in allgrades:
  74 + <tr>
  75 + <td>${g[0]}</td> <!-- teste -->
  76 + <td>${g[2][:10]}</td> <!-- data -->
  77 + <td>${g[2][11:19]}</td> <!-- hora -->
  78 + <td>
  79 + <div class="progress">
  80 + % if g[1] < 10:
  81 + <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
  82 + % elif g[1] < 15:
  83 + <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
  84 + % else:
  85 + <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="${'{0}'.format(round(g[1]))}" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ${'{0}'.format(round(5 * g[1]))}%;">
  86 + % endif
  87 + ${'{:.1f}'.format(g[1])}
  88 + </div>
  89 + </div>
  90 + </td>
  91 + </tr>
  92 + % endfor
  93 + </tbody>
  94 + </table>
  95 + </div>
103 96 </div> <!-- panel -->
104 97 </div> <!-- container -->
105 98 </body>
... ...