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.
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ê???
@@ -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']
@@ -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 # ---
@@ -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>