Commit 2780d0843fcfbcce121dddca36df7ab9079ef949
1 parent
1699423a
Exists in
master
and in
1 other branch
- 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.
Showing
5 changed files
with
79 additions
and
69 deletions
Show diff stats
BUGS.md
1 | 1 | ||
2 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | -- implementar practice mode. | ||
5 | - usar thread.Lock para aceder a variaveis de estado? | 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 | # TODO | 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 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova. | 12 | - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova. |
10 | - 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) | 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 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | 14 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). |
13 | - aviso na pagina principal para quem usa browser da treta | 15 | - aviso na pagina principal para quem usa browser da treta |
14 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. | 16 | - permitir varios testes, aluno escolhe qual o teste que quer fazer. |
15 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput | 17 | - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput |
16 | - perguntas para professor corrigir mais tarde. | 18 | - perguntas para professor corrigir mais tarde. |
17 | -- visualizar um teste ja realizado na página de administração | ||
18 | - fazer uma calculadora javascript e por no menu. surge como modal | 19 | - fazer uma calculadora javascript e por no menu. surge como modal |
19 | - GeoIP? | 20 | - GeoIP? |
20 | - alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) | 21 | - alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) |
@@ -22,6 +23,8 @@ | @@ -22,6 +23,8 @@ | ||
22 | 23 | ||
23 | # FIXED | 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 | - server nao esta a receber eventos focus/blur dos utilizadores diferentes de '0', estranho... | 28 | - server nao esta a receber eventos focus/blur dos utilizadores diferentes de '0', estranho... |
26 | - permitir adicionar imagens nas perguntas. | 29 | - permitir adicionar imagens nas perguntas. |
27 | - detect_unfocus.js so funciona se estiver inline no html. porquê??? | 30 | - detect_unfocus.js so funciona se estiver inline no html. porquê??? |
app.py
@@ -131,22 +131,25 @@ class App(object): | @@ -131,22 +131,25 @@ class App(object): | ||
131 | grade = t.correct() | 131 | grade = t.correct() |
132 | logger.info('Student {0}: finished with {1} points.'.format(uid, grade)) | 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 | with self.db_session() as s: | 140 | with self.db_session() as s: |
140 | s.add(Test( | 141 | s.add(Test( |
141 | ref=t['ref'], | 142 | ref=t['ref'], |
143 | + title=t['title'], | ||
142 | grade=t['grade'], | 144 | grade=t['grade'], |
143 | starttime=str(t['start_time']), | 145 | starttime=str(t['start_time']), |
144 | finishtime=str(t['finish_time']), | 146 | finishtime=str(t['finish_time']), |
147 | + filename=fpath, | ||
145 | student_id=t['student']['number'])) | 148 | student_id=t['student']['number'])) |
146 | s.add_all([Question( | 149 | s.add_all([Question( |
147 | ref=q['ref'], | 150 | ref=q['ref'], |
148 | grade=q['grade'], | 151 | grade=q['grade'], |
149 | - starttime='', | 152 | + starttime=str(t['start_time']), |
150 | finishtime=str(t['finish_time']), | 153 | finishtime=str(t['finish_time']), |
151 | student_id=t['student']['number'], | 154 | student_id=t['student']['number'], |
152 | test_id=t['ref']) for q in t['questions'] if 'grade' in q]) | 155 | test_id=t['ref']) for q in t['questions'] if 'grade' in q]) |
@@ -176,9 +179,12 @@ class App(object): | @@ -176,9 +179,12 @@ class App(object): | ||
176 | return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} | 179 | return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} |
177 | def get_student_grades_from_all_tests(self, uid): | 180 | def get_student_grades_from_all_tests(self, uid): |
178 | with self.db_session() as s: | 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 | def get_online_students(self): | 188 | def get_online_students(self): |
183 | # [('uid', 'name', 'starttime')] | 189 | # [('uid', 'name', 'starttime')] |
184 | return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'] | 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,9 +29,11 @@ class Test(Base): | ||
29 | __tablename__ = 'tests' | 29 | __tablename__ = 'tests' |
30 | id = Column(Integer, primary_key=True) # auto_increment | 30 | id = Column(Integer, primary_key=True) # auto_increment |
31 | ref = Column(String) | 31 | ref = Column(String) |
32 | + title = Column(String) | ||
32 | grade = Column(Float) | 33 | grade = Column(Float) |
33 | starttime = Column(String) | 34 | starttime = Column(String) |
34 | finishtime = Column(String) | 35 | finishtime = Column(String) |
36 | + filename = Column(String) | ||
35 | student_id = Column(String, ForeignKey('students.id')) | 37 | student_id = Column(String, ForeignKey('students.id')) |
36 | 38 | ||
37 | # --- | 39 | # --- |
serve.py
@@ -198,7 +198,6 @@ class Root(object): | @@ -198,7 +198,6 @@ class Root(object): | ||
198 | # text - always returns string. no answer '', otherwise 'dskdjs' | 198 | # text - always returns string. no answer '', otherwise 'dskdjs' |
199 | uid = cherrypy.session.get(SESSION_KEY) | 199 | uid = cherrypy.session.get(SESSION_KEY) |
200 | name = self.app.get_student_name(uid) | 200 | name = self.app.get_student_name(uid) |
201 | - title = self.app.get_test(uid)['title'] | ||
202 | qq = self.app.get_test_qtypes(uid) # {'q1_ref': 'checkbox', ...} | 201 | qq = self.app.get_test_qtypes(uid) # {'q1_ref': 'checkbox', ...} |
203 | 202 | ||
204 | # each question that is marked to be classified must have an answer. | 203 | # each question that is marked to be classified must have an answer. |
@@ -215,6 +214,7 @@ class Root(object): | @@ -215,6 +214,7 @@ class Root(object): | ||
215 | ans[qref] = a | 214 | ans[qref] = a |
216 | 215 | ||
217 | grade = self.app.correct_test(uid, ans) | 216 | grade = self.app.correct_test(uid, ans) |
217 | + t = self.app.get_test(uid) | ||
218 | self.app.logout(uid) | 218 | self.app.logout(uid) |
219 | 219 | ||
220 | # --- Expire session | 220 | # --- Expire session |
@@ -223,9 +223,7 @@ class Root(object): | @@ -223,9 +223,7 @@ class Root(object): | ||
223 | 223 | ||
224 | # --- Show result to student | 224 | # --- Show result to student |
225 | return self.template['grade'].render( | 225 | return self.template['grade'].render( |
226 | - title=title, | ||
227 | - student_id=uid + ' - ' + name, | ||
228 | - grade=grade, | 226 | + t=t, |
229 | allgrades=self.app.get_student_grades_from_all_tests(uid) | 227 | allgrades=self.app.get_student_grades_from_all_tests(uid) |
230 | ) | 228 | ) |
231 | 229 | ||
@@ -267,6 +265,14 @@ class Root(object): | @@ -267,6 +265,14 @@ class Root(object): | ||
267 | def admin(self, **reset_pw): | 265 | def admin(self, **reset_pw): |
268 | return self.template['admin'].render() | 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 | def parse_arguments(): | 277 | def parse_arguments(): |
272 | 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.') | 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,34 +4,17 @@ | ||
4 | <meta charset="UTF-8"> | 4 | <meta charset="UTF-8"> |
5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
7 | - <title> ${title} </title> | 7 | + <title> Teste </title> |
8 | <link rel="icon" href="/static/favicon.ico"> | 8 | <link rel="icon" href="/static/favicon.ico"> |
9 | 9 | ||
10 | <!-- Bootstrap --> | 10 | <!-- Bootstrap --> |
11 | <link rel="stylesheet" href="/static/css/bootstrap.min.css"> | 11 | <link rel="stylesheet" href="/static/css/bootstrap.min.css"> |
12 | <link rel="stylesheet" href="/static/css/bootstrap-theme.min.css"> <!-- optional --> | 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 | <script src="/static/js/jquery.min.js"></script> | 15 | <script src="/static/js/jquery.min.js"></script> |
20 | <script src="/static/js/bootstrap.min.js"></script> | 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 | <body> | 19 | <body> |
37 | 20 | ||
@@ -43,13 +26,20 @@ | @@ -43,13 +26,20 @@ | ||
43 | <span class="icon-bar"></span> | 26 | <span class="icon-bar"></span> |
44 | <span class="icon-bar"></span> | 27 | <span class="icon-bar"></span> |
45 | </button> | 28 | </button> |
46 | - <a class="navbar-brand" href="#">UÉvora</a> | 29 | + <a class="navbar-brand" href="#"> |
30 | + ${t['title']} | ||
31 | + </a> | ||
47 | </div> | 32 | </div> |
33 | + | ||
48 | <div class="collapse navbar-collapse" id="myNavbar"> | 34 | <div class="collapse navbar-collapse" id="myNavbar"> |
35 | + | ||
49 | <ul class="nav navbar-nav navbar-right"> | 36 | <ul class="nav navbar-nav navbar-right"> |
50 | <li class="dropdown"> | 37 | <li class="dropdown"> |
51 | <a class="dropdown-toggle" data-toggle="dropdown" href="#"> | 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 | </a> | 43 | </a> |
54 | </li> | 44 | </li> |
55 | </ul> | 45 | </ul> |
@@ -60,46 +50,49 @@ | @@ -60,46 +50,49 @@ | ||
60 | <div class="container"> | 50 | <div class="container"> |
61 | <div class="jumbotron drop-shadow"> | 51 | <div class="jumbotron drop-shadow"> |
62 | <h1>Resultado</h1> | 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 | <p>A prova está terminada, pode fechar o browser e desligar o computador.</p> | 54 | <p>A prova está terminada, pode fechar o browser e desligar o computador.</p> |
65 | </div> | 55 | </div> |
66 | 56 | ||
67 | <div class="panel panel-default drop-shadow"> | 57 | <div class="panel panel-default drop-shadow"> |
68 | <div class="panel-heading"> | 58 | <div class="panel-heading"> |
69 | - Testes realizados | 59 | + Provas realizadas até ao momento |
70 | </div> | 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 | <tr> | 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 | </tr> | 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 | </div> <!-- panel --> | 96 | </div> <!-- panel --> |
104 | </div> <!-- container --> | 97 | </div> <!-- container --> |
105 | </body> | 98 | </body> |