Commit 8584ceb0feb3a50227068967d9955635b2616568

Authored by Miguel Barao
1 parent a1c9a10e
Exists in master and in 1 other branch dev

- /admin has been completly rebuilt (still missing: insert new student)

1 1
2 # BUGS 2 # BUGS
3 3
4 -- pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static...  
5 -- usar thread.Lock para aceder a variaveis de estado.  
6 -- permitir adicionar imagens nas perguntas  
7 -- browser e ip usados gravado no test. 4 +- alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...)
8 - configuracao dos logs cherrypy para se darem bem com os outros 5 - configuracao dos logs cherrypy para se darem bem com os outros
  6 +- browser e ip usados gravado no test.
  7 +- usar thread.Lock para aceder a variaveis de estado.
  8 +- permitir adicionar imagens nas perguntas.
  9 +- argumentos da linha de comando a funcionar.
9 10
10 # TODO 11 # TODO
11 12
12 - implementar practice mode. 13 - implementar practice mode.
  14 +- botões allow all/deny all.
  15 +- enviar logs para web?
13 - SQLAlchemy em vez da classe database. 16 - SQLAlchemy em vez da classe database.
14 -- argumentos da linha de comando a funcionar.  
15 - aviso na pagina principal para quem usa browser da treta 17 - aviso na pagina principal para quem usa browser da treta
16 - permitir varios testes, aluno escolhe qual o teste que quer fazer. 18 - permitir varios testes, aluno escolhe qual o teste que quer fazer.
17 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput 19 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput
18 - perguntas para professor corrigir mais tarde. 20 - perguntas para professor corrigir mais tarde.
19 -- single page web no frontend  
20 -- criar script json2md.py ou outra forma de visualizar um teste ja realizado 21 +- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?).
  22 +- visualizar um teste ja realizado na página de administração
21 - Menu para professor com link para /results e /students 23 - Menu para professor com link para /results e /students
22 - fazer uma calculadora javascript e por no menu. surge como modal 24 - fazer uma calculadora javascript e por no menu. surge como modal
23 - GeoIP? 25 - GeoIP?
24 -- mostrar botão de reset apenas para alunos com password definida? 26 +- mostrar botão de reset apenas no final da pagina, com edit para escrever o número.
25 27
26 # FIXED 28 # FIXED
27 29
  30 +- aluno faz login, mas fecha browser, ficando no estado (online,deny). Ao tentar login com outro browser está deny e o prof não consegue pô-lo em allow pois já não está na lista. => solucao é manter todos os alunos numa tabela.
  31 +- pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static...
28 - Não mostrar Professor nos activos em /admin 32 - Não mostrar Professor nos activos em /admin
29 - /admin mostrar actualizações automaticamente? 33 - /admin mostrar actualizações automaticamente?
30 - se no teste uma das "ref" nao existir nos ficheiros de perguntas, rebenta. 34 - se no teste uma das "ref" nao existir nos ficheiros de perguntas, rebenta.
@@ -23,7 +23,7 @@ class App(object): @@ -23,7 +23,7 @@ class App(object):
23 # } 23 # }
24 logger.info('============= Running perguntations =============') 24 logger.info('============= Running perguntations =============')
25 self.online = dict() # {uid: {'student':{}}} 25 self.online = dict() # {uid: {'student':{}}}
26 - self.allowed = set([13361,13682]) # '0' is hardcoded to allowed elsewhere 26 + self.allowed = set([]) # '0' is hardcoded to allowed elsewhere FIXME
27 self.testfactory = test.TestFactory(filename, conf=conf) 27 self.testfactory = test.TestFactory(filename, conf=conf)
28 self.db = database.Database(self.testfactory['database']) # FIXME 28 self.db = database.Database(self.testfactory['database']) # FIXME
29 try: 29 try:
@@ -117,20 +117,47 @@ class App(object): @@ -117,20 +117,47 @@ class App(object):
117 return self.online[uid].get('test', default) 117 return self.online[uid].get('test', default)
118 def get_test_qtypes(self, uid): 118 def get_test_qtypes(self, uid):
119 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} 119 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']}
120 - def get_student_grades(self, uid):  
121 - return self.db.get_student_grades(uid) 120 + def get_student_grades_from_all_tests(self, uid):
  121 + return self.db.get_student_grades_from_all_tests(uid)
  122 +
  123 + # def get_student_grades_from_test(self, uid, testid):
  124 + # return self.db.get_student_grades_from_test(uid, testid)
  125 +
  126 +
  127 + # def get_online_students(self):
  128 + # # list of ('uid', 'name', 'start_time') sorted by start time
  129 + # return sorted(
  130 + # ((k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'),
  131 + # key=lambda k: k[2] # sort key
  132 + # )
122 133
123 def get_online_students(self): 134 def get_online_students(self):
124 - # list of ('uid', 'name', 'start_time') sorted by start time  
125 - return sorted(  
126 - ((k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'),  
127 - key=lambda k: k[2] # sort key  
128 - ) 135 + # {'123': '2016-12-02 12:04:12.344243', ...}
  136 + return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0']
129 137
130 def get_offline_students(self): 138 def get_offline_students(self):
131 # list of ('uid', 'name') sorted by number 139 # list of ('uid', 'name') sorted by number
132 return sorted((s[:2] for s in self.db.get_all_students() if s[0] not in self.online), key=lambda k: k[0]) 140 return sorted((s[:2] for s in self.db.get_all_students() if s[0] not in self.online), key=lambda k: k[0])
133 141
  142 + def get_all_students(self):
  143 + # list of ('uid', 'name') sorted by number
  144 + return sorted((s[:2] for s in self.db.get_all_students() if s[0] != '0'), key=lambda k: k[0])
  145 +
  146 + def get_students_state(self):
  147 + # {'123': {'name': 'John', 'start_time':'', 'grades':[10.2, 13.1], ...}}
  148 + d = {}
  149 + for s in self.db.get_all_students():
  150 + uid, name, pw = s
  151 + if uid == '0':
  152 + continue
  153 + d[uid] = {'name': name}
  154 + d[uid]['allowed'] = uid in self.allowed
  155 + d[uid]['online'] = uid in self.online
  156 + d[uid]['start_time'] = self.online.get(uid, {}).get('test', {}).get('start_time','')
  157 + d[uid]['password_defined'] = pw != ''
  158 + d[uid]['grades'] = self.db.get_student_grades_from_test(uid, self.testfactory['ref'])
  159 + return d
  160 +
134 # def get_this_students_grades(self): 161 # def get_this_students_grades(self):
135 # # list of ('uid', 'name') sorted by number 162 # # list of ('uid', 'name') sorted by number
136 # return self.db.get_students_grades(self.testfactory['ref']) 163 # return self.db.get_students_grades(self.testfactory['ref'])
@@ -45,11 +45,16 @@ class Database(object): @@ -45,11 +45,16 @@ class Database(object):
45 # return grades.fetchall() 45 # return grades.fetchall()
46 46
47 # get results from previous tests of a student 47 # get results from previous tests of a student
48 - def get_student_grades(self, uid): 48 + def get_student_grades_from_all_tests(self, uid):
49 with sqlite3.connect(self.db) as c: 49 with sqlite3.connect(self.db) as c:
50 grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid]) 50 grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid])
51 return grades.fetchall() 51 return grades.fetchall()
52 52
  53 + def get_student_grades_from_test(self, uid, testid):
  54 + with sqlite3.connect(self.db) as c:
  55 + grades = c.execute('SELECT grade,finish_time FROM tests WHERE student_id==? and test_id==?', [uid, testid])
  56 + return grades.fetchall()
  57 +
53 def save_test(self, t): 58 def save_test(self, t):
54 with sqlite3.connect(self.db) as c: 59 with sqlite3.connect(self.db) as c:
55 # save final grade of the test 60 # save final grade of the test
@@ -75,23 +75,21 @@ def secureheaders(): @@ -75,23 +75,21 @@ def secureheaders():
75 # ============================================================================ 75 # ============================================================================
76 class AdminWebService(object): 76 class AdminWebService(object):
77 exposed = True 77 exposed = True
  78 + _cp_config = {
  79 + 'auth.require': [name_is('0')]
  80 + }
78 81
79 def __init__(self, app): 82 def __init__(self, app):
80 self.app = app 83 self.app = app
81 84
82 @cherrypy.tools.accept(media='application/json') # FIXME 85 @cherrypy.tools.accept(media='application/json') # FIXME
83 - @require(name_is('0'))  
84 def GET(self): 86 def GET(self):
85 data = { 87 data = {
86 - 'online': self.app.get_online_students(),  
87 - 'offline': self.app.get_offline_students(),  
88 - 'allowed': list(self.app.get_allowed_students()),  
89 - # 'finished': self.app.get_this_students_grades() 88 + 'students': list(self.app.get_students_state().items()),
  89 + 'test': self.app.testfactory
90 } 90 }
91 - # print(dict(data['finished']))  
92 return json.dumps(data, default=str) 91 return json.dumps(data, default=str)
93 92
94 - @require(name_is('0'))  
95 def POST(self, **args): 93 def POST(self, **args):
96 # print('POST', args) # FIXME 94 # print('POST', args) # FIXME
97 if args['cmd'] == 'allow': 95 if args['cmd'] == 'allow':
@@ -117,20 +115,17 @@ class Root(object): @@ -117,20 +115,17 @@ class Root(object):
117 'admin': t.get_template('/admin.html'), 115 'admin': t.get_template('/admin.html'),
118 } 116 }
119 117
  118 + # --- DEFAULT ------------------------------------------------------------
120 @cherrypy.expose 119 @cherrypy.expose
121 @require() 120 @require()
122 - def default(self, **args): 121 + def default(self, *args, **kwargs):
123 uid = cherrypy.session.get(SESSION_KEY) 122 uid = cherrypy.session.get(SESSION_KEY)
124 if uid == '0': 123 if uid == '0':
125 raise cherrypy.HTTPRedirect('/admin') 124 raise cherrypy.HTTPRedirect('/admin')
126 else: 125 else:
127 raise cherrypy.HTTPRedirect('/test') 126 raise cherrypy.HTTPRedirect('/test')
128 - # # FIXME  
129 - # title = self.app.testfactory['title']  
130 - # return '''Start test here: <a href="/test">{}</a>'''.format(title)  
131 - # # raise cherrypy.HTTPRedirect('/test')  
132 -  
133 127
  128 + # --- LOGIN --------------------------------------------------------------
134 @cherrypy.expose 129 @cherrypy.expose
135 def login(self, uid=None, pw=None): 130 def login(self, uid=None, pw=None):
136 if uid is None or pw is None: # first try 131 if uid is None or pw is None: # first try
@@ -138,11 +133,11 @@ class Root(object): @@ -138,11 +133,11 @@ class Root(object):
138 133
139 if self.app.login(uid, pw): # ok 134 if self.app.login(uid, pw): # ok
140 cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid 135 cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid
141 - raise cherrypy.HTTPRedirect('/admin') # FIXME 136 + raise cherrypy.HTTPRedirect('/') # FIXME
142 else: # denied 137 else: # denied
143 return self.template['login'].render() 138 return self.template['login'].render()
144 139
145 - 140 + # --- LOGOUT -------------------------------------------------------------
146 @cherrypy.expose 141 @cherrypy.expose
147 @require() 142 @require()
148 def logout(self): 143 def logout(self):
@@ -154,8 +149,7 @@ class Root(object): @@ -154,8 +149,7 @@ class Root(object):
154 self.app.logout(uid) 149 self.app.logout(uid)
155 raise cherrypy.HTTPRedirect('/') 150 raise cherrypy.HTTPRedirect('/')
156 151
157 -  
158 - # --- TEST --------------------------------------------------------------- 152 + # --- TEST ---------------------------------------------------------------
159 # Get student number and assigned questions from current session. 153 # Get student number and assigned questions from current session.
160 # If it's the first time, create instance of the test and register the 154 # If it's the first time, create instance of the test and register the
161 # time. 155 # time.
@@ -200,71 +194,29 @@ class Root(object): @@ -200,71 +194,29 @@ class Root(object):
200 grade = self.app.correct_test(uid, ans) 194 grade = self.app.correct_test(uid, ans)
201 self.app.logout(uid) 195 self.app.logout(uid)
202 196
203 - # ---- Expire session ---- 197 + # --- Expire session
204 cherrypy.lib.sessions.expire() # session coockie expires client side 198 cherrypy.lib.sessions.expire() # session coockie expires client side
205 cherrypy.session[SESSION_KEY] = cherrypy.request.login = None 199 cherrypy.session[SESSION_KEY] = cherrypy.request.login = None
206 200
207 - # ---- Show result to student ---- 201 + # --- Show result to student
208 return self.template['grade'].render( 202 return self.template['grade'].render(
209 title=title, 203 title=title,
210 student_id=uid + ' - ' + student_name, 204 student_id=uid + ' - ' + student_name,
211 grade=grade, 205 grade=grade,
212 - allgrades=self.app.get_student_grades(uid) 206 + allgrades=self.app.get_student_grades_from_all_tests(uid)
213 ) 207 )
214 208
215 -  
216 -  
217 - # --- STUDENTS ----------------------------------------------------------- 209 + # --- ADMIN --------------------------------------------------------------
218 @cherrypy.expose 210 @cherrypy.expose
219 @require(name_is('0')) 211 @require(name_is('0'))
220 def admin(self, **reset_pw): 212 def admin(self, **reset_pw):
221 - return self.template['admin'].render(  
222 - online_students=self.app.get_online_students(),  
223 - offline_students=self.app.get_offline_students(),  
224 - allowed_students=self.app.get_allowed_students()  
225 - )  
226 - # t = TemplateLookup(directories=[TEMPLATES_DIR], input_encoding='utf-8').get_template('admin.html')  
227 - # return t.render(online_students=online_students,  
228 - # offline_students=offline_students,  
229 - # allowed_students=allowed_students)  
230 -  
231 -  
232 -  
233 -  
234 -  
235 - # def students(self, **reset_pw):  
236 - # if reset_pw:  
237 - # self.database.student_reset_pw(reset_pw)  
238 - # for num in reset_pw:  
239 - # cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION')  
240 -  
241 - # grades = self.database.test_grades2(self.testconf['ref'])  
242 - # students = self.database.get_students()  
243 -  
244 - # template = self.templates.get_template('/students.html')  
245 - # return template.render(students=students, tags=self.tags, grades=grades)  
246 -  
247 - # --- RESULTS ------------------------------------------------------------  
248 - # @cherrypy.expose  
249 - # @require()  
250 - # def results(self):  
251 - # if self.testconf.get('practice', False):  
252 - # uid = cherrypy.session.get('userid')  
253 - # name = cherrypy.session.get('name')  
254 -  
255 - # r = self.database.test_grades(self.testconf['ref'])  
256 - # template = self.templates.get_template('/results.html')  
257 - # return template.render(t=self.testconf, results=r, name=name, uid=uid)  
258 - # else:  
259 - # raise cherrypy.HTTPRedirect('/')  
260 -  
261 - 213 + return self.template['admin'].render()
262 214
263 # ============================================================================ 215 # ============================================================================
264 def parse_arguments(): 216 def parse_arguments():
265 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.') 217 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.')
266 serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) 218 serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf'))
267 - argparser.add_argument('--server', default=serverconf_file, type=str, help='server configuration file') 219 + argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file')
268 # argparser.add_argument('--debug', action='store_true', 220 # argparser.add_argument('--debug', action='store_true',
269 # help='Show datastructures when rendering questions') 221 # help='Show datastructures when rendering questions')
270 # argparser.add_argument('--show_ref', action='store_true', 222 # argparser.add_argument('--show_ref', action='store_true',
@@ -301,29 +253,28 @@ if __name__ == &#39;__main__&#39;: @@ -301,29 +253,28 @@ if __name__ == &#39;__main__&#39;:
301 except: 253 except:
302 sys.exit(1) 254 sys.exit(1)
303 255
304 -  
305 - # testconf = test.TestFactory(filename, conf=vars(arg))  
306 - 256 + # --- create webserver
  257 + webapp = Root(app)
  258 + webapp.adminwebservice = AdminWebService(app)
307 259
308 # --- site wide configuration (valid for all apps) 260 # --- site wide configuration (valid for all apps)
309 cherrypy.tools.secureheaders = cherrypy.Tool('before_finalize', secureheaders, priority=60) 261 cherrypy.tools.secureheaders = cherrypy.Tool('before_finalize', secureheaders, priority=60)
310 - cherrypy.config.update({'tools.staticdir.root': SERVER_PATH})  
311 - cherrypy.config.update(arg.server) 262 +
  263 + cherrypy.config.update(arg.conf) # configuration file in /config
312 conf = { 264 conf = {
313 '/': { 265 '/': {
314 - # DO NOT DISABLE SESSIONS!  
315 'tools.sessions.on': True, 266 'tools.sessions.on': True,
316 - 'tools.sessions.timeout': 240,  
317 - 'tools.sessions.storage_type': 'ram',  
318 - 'tools.sessions.storage_path': 'sessions', 267 + 'tools.sessions.timeout': 240, # sessions last 4 hours
  268 + 'tools.sessions.storage_type': 'ram', # or 'file'
  269 + 'tools.sessions.storage_path': 'sessions', # if storage_type='file'
319 # tools.sessions.secure = True 270 # tools.sessions.secure = True
320 # tools.sessions.httponly = True 271 # tools.sessions.httponly = True
321 272
322 - # Authentication 273 + # Turn on authentication (required for check_auth to work)
323 'tools.auth.on': True, 274 'tools.auth.on': True,
324 275
325 'tools.secureheaders.on': True, 276 'tools.secureheaders.on': True,
326 - 277 + 'tools.staticdir.root': SERVER_PATH,
327 'tools.staticdir.dir': 'static', # where to get js,css,jpg,... 278 'tools.staticdir.dir': 'static', # where to get js,css,jpg,...
328 'tools.staticdir.on': True, 279 'tools.staticdir.on': True,
329 }, 280 },
@@ -333,16 +284,15 @@ if __name__ == &#39;__main__&#39;: @@ -333,16 +284,15 @@ if __name__ == &#39;__main__&#39;:
333 'tools.response_headers.headers': [('Content-Type', 'text/plain')], 284 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
334 }, 285 },
335 '/static': { 286 '/static': {
336 - 'tools.staticdir.dir': './static', # where to get js,css,jpg,... 287 + 'tools.auth.on': False, # everything in /static is public
337 'tools.staticdir.on': True, 288 'tools.staticdir.on': True,
338 - } 289 + 'tools.staticdir.dir': 'static', # where to get js,css,jpg,...
  290 + },
339 } 291 }
340 292
341 # --- app specific configuration 293 # --- app specific configuration
342 - webapp = Root(app)  
343 - webapp.adminwebservice = AdminWebService(app)  
344 294
345 - cherrypy.tree.mount(webapp, '/', conf) 295 + cherrypy.tree.mount(webapp, script_name='/', config=conf)
346 296
347 # logger.info('Webserver listening at {}:{}'.format( 297 # logger.info('Webserver listening at {}:{}'.format(
348 # cherrypy.config['server.socket_host'], 298 # cherrypy.config['server.socket_host'],
static/js/admin.js
1 $(document).ready(function() { 1 $(document).ready(function() {
2 - // button handler to allow all students  
3 - $("#allowall").click(function(e) {  
4 - alert('not implemented'); // FIXME  
5 - });  
6 -  
7 - // button handler to allow all students  
8 - $("#denyall").click(function(e) {  
9 - alert('not implemented'); // FIXME  
10 - }); 2 + // button handlers (runs once)
  3 + function define_buttons_handlers() {
  4 + $("#allow_all").click(
  5 + function() {
  6 + $(":checkbox").prop("checked", true).trigger('change');
  7 + }
  8 + );
  9 + $("#deny_all").click(
  10 + function() {
  11 + $(":checkbox").prop("checked", false).trigger('change');
  12 + }
  13 + );
  14 + $("#reset_password").click(
  15 + function () {
  16 + var number = $("#reset_number").val();
  17 + $.ajax({
  18 + type: "POST",
  19 + url: "/adminwebservice",
  20 + data: {"cmd": "reset", "name": number}
  21 + });
  22 + }
  23 + );
  24 + $("#novo_aluno").click(
  25 + function () {
  26 + alert('Não implementado!');
  27 + }
  28 + );
  29 + }
11 30
12 - // checkbox event handler to allow/deny students 31 + // checkbox handler to allow/deny students individually
13 function autorizeStudent(e) { 32 function autorizeStudent(e) {
14 $.ajax({ 33 $.ajax({
15 type: "POST", 34 type: "POST",
@@ -22,36 +41,75 @@ $(document).ready(function() { @@ -22,36 +41,75 @@ $(document).ready(function() {
22 $(this).parent().parent().removeClass("active"); 41 $(this).parent().parent().removeClass("active");
23 } 42 }
24 43
25 - // button handler to reset student password  
26 - function resetPassword(e) {  
27 - $.ajax({  
28 - type: "POST",  
29 - url: "/adminwebservice",  
30 - data: {"cmd": "reset", "name": this.value}  
31 - });  
32 - }  
33 -  
34 - function populateOnlineTable(online) {  
35 - $("#online-header").html(online.length + " Activo(s)"); 44 + function populateOnlineTable(students) {
  45 + var active = [];
36 var rows = ""; 46 var rows = "";
37 - $.each(online, function(i, r) {  
38 - rows += "<tr><td>" + r[0] + "</td><td>" + r[1] + "</td><td>" + r[2].slice(0,19) + "</td></tr>"; 47 + $.each(students, function(i, r) {
  48 + if (r[1]['start_time'] != '') {
  49 + active.push([r[0], r[1]['name'], r[1]['start_time']]);
  50 + }
39 }); 51 });
  52 + active.sort(function(a,b){return a[2] < b[2] ? -1 : (a[2] == b[2] ? 0 : 1);});
  53 + n = active.length;
  54 + for(var i = 0; i < n; i++) {
  55 + rows += "<tr><td>" + active[i][0] + "</td><td>" + active[i][1] + "</td><td>" + active[i][2].slice(0,10) + "</td><td>" + active[i][2].slice(11,19) + "</td></tr>";
  56 + }
40 $("#online_students").html(rows); 57 $("#online_students").html(rows);
  58 + $("#online-header").html(n + " Activo(s)");
  59 + }
  60 +
  61 + function generate_grade_bar(grade) {
  62 + var barcolor;
  63 + if (grade < 10) {
  64 + barcolor = 'progress-bar-danger';
  65 + }
  66 + else if (grade < 15) {
  67 + barcolor = 'progress-bar-warning';
  68 + }
  69 + else {
  70 + barcolor = 'progress-bar-success';
  71 + }
  72 +
  73 + var bar = '<div class="progress"><div class="progress-bar ' + barcolor + '" role="progressbar" aria-valuenow="' + grade + '" aria-valuemin="0" aria-valuemax="20" style="min-width: 2em; width: ' + (5*grade) + '%;">' + grade + '</div></div>';
  74 + return bar
41 } 75 }
42 76
43 - function populateOfflineTable(offline, allowed) {  
44 - $("#offline-header").html(offline.length + " Inactivo(s)") 77 + function populateStudentsTable(students) {
  78 + $("#students-header").html(students.length + " Alunos")
45 var rows = ""; 79 var rows = "";
46 - $.each(offline, function(i, r) {  
47 - rows += '<tr id="' + r[0] + '" + class="' + (allowed.indexOf(r[0]) > -1? 'active':'') + '">\  
48 - <td><input type="checkbox" name="' + r[0] + '" value="true"' + (allowed.indexOf(r[0]) > -1? 'checked':'') + '></td>\  
49 - <td>' + r[0] + '</td>\  
50 - <td>' + r[1] + '</td>\  
51 - <td><button name="reset" value="' + r[0] + '" class="btn btn-xs btn-danger">reset</button></td>\  
52 - </tr>'; 80 + students.sort(function(a,b){return a[0] - b[0]});
  81 + $.each(students, function(i, r) {
  82 + var uid = r[0];
  83 + var d = r[1]; // dictionary
  84 +
  85 + if (d['start_time'] != '') // test
  86 + rows += '<tr id="' + uid + '" + class="success">';
  87 + else if (d['online']) // online
  88 + rows += '<tr id="' + uid + '" + class="warning">';
  89 + else if (d['allowed']) // allowed
  90 + rows += '<tr id="' + uid + '" + class="active">';
  91 + else // offline
  92 + rows += '<tr id="' + uid + '" + class="">';
  93 +
  94 + rows += '\
  95 + <td><input type="checkbox" name="' + uid + '" value="true"' + (d['allowed'] ? 'checked' : '') + '></td>\
  96 + <td>' + uid + '</td>\
  97 + <td>' + d['name'] + '</td>\
  98 + <td>' +
  99 + (d['password_defined'] ? '<span class="label label-default">pw</span>' : '') +
  100 + // (d['online'] ? '<span class="label label-warning">online</span>' : '') +
  101 + (d['start_time']==''?'':'<span class="label label-success">teste</span>') +
  102 + '</td>\
  103 + <td>';
  104 + var g = d['grades'];
  105 + var glength = g.length;
  106 + for (var i=0; i < glength; i++) {
  107 + rows += '<div data-toggle="tooltip" data-placement="top" title="' + g[i][1].slice(0,19) + '">' + generate_grade_bar(g[i][0]) + '</div>';
  108 + }
  109 + rows += '</td></tr>';
53 }); 110 });
54 - $("#offline_students").html(rows); 111 + $("#students").html(rows);
  112 + $('[data-toggle="tooltip"]').tooltip();
55 } 113 }
56 114
57 function populate() { 115 function populate() {
@@ -59,17 +117,30 @@ $(document).ready(function() { @@ -59,17 +117,30 @@ $(document).ready(function() {
59 url: "/adminwebservice", 117 url: "/adminwebservice",
60 dataType: "json", 118 dataType: "json",
61 success: function(data) { 119 success: function(data) {
62 - populateOnlineTable(data["online"]);  
63 - populateOfflineTable(data["offline"], data["allowed"]); 120 + var t = new Date();
  121 + $('#currenttime').html(t.getHours() + (t.getMinutes() < 10 ? ':0' : ':') + t.getMinutes());
  122 + $("#title").html(data['test']['title']);
  123 + $("#ref").html(data['test']['ref']);
  124 + $("#database").html(data['test']['database']);
  125 + if (data['test']['save_answers']) {
  126 + $("#answers_dir").html(data['test']['answers_dir']);
  127 + }
  128 + else {
  129 + $("#answers_dir").html('--- not being saved ---');
  130 + }
  131 + $("#filename").html(data['test']['filename']);
  132 +
  133 + populateOnlineTable(data["students"]);
  134 + populateStudentsTable(data["students"])
64 135
65 // add event handlers 136 // add event handlers
66 $('input[type="checkbox"]').change(autorizeStudent); 137 $('input[type="checkbox"]').change(autorizeStudent);
67 - $('button[name="reset"]').click(resetPassword);  
68 }, 138 },
69 error: function() {alert("Servidor não responde.");} 139 error: function() {alert("Servidor não responde.");}
70 }); 140 });
71 } 141 }
72 142
73 - populate(); // run once when the page is loaded 143 + populate(); // run once when the page is loaded
  144 + define_buttons_handlers();
74 setInterval(populate, 5000); // poll server on 5s interval 145 setInterval(populate, 5000); // poll server on 5s interval
75 }); 146 });
templates/admin.html
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
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> List of students </title> 7 + <title> Admin </title>
8 <link rel="icon" href="favicon.ico"> 8 <link rel="icon" href="favicon.ico">
9 9
10 <!-- Bootstrap --> 10 <!-- Bootstrap -->
@@ -25,6 +25,10 @@ @@ -25,6 +25,10 @@
25 box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2); 25 box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2);
26 border-radius:5px; 26 border-radius:5px;
27 } 27 }
  28 + .progress {
  29 + /*display: inline-block;*/
  30 + margin-bottom: 0 !important;
  31 + }
28 </style> 32 </style>
29 33
30 <script src="/js/admin.js"></script> 34 <script src="/js/admin.js"></script>
@@ -32,11 +36,53 @@ @@ -32,11 +36,53 @@
32 <!-- ===================================================================== --> 36 <!-- ===================================================================== -->
33 <body> 37 <body>
34 38
  39 +<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
  40 + <div class="container-fluid drop-shadow">
  41 + <div class="navbar-header">
  42 + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
  43 + <!-- <span class="glyphicon glyphicon-menu-hamburger"></span> -->
  44 + <span class="icon-bar"></span>
  45 + <span class="icon-bar"></span>
  46 + <span class="icon-bar"></span>
  47 + </button>
  48 + <a class="navbar-brand" id="currenttime" href="#">
  49 + --:-- <!-- clock -->
  50 + </a>
  51 + </div>
  52 +
  53 + <div class="collapse navbar-collapse" id="myNavbar">
  54 +
  55 + <ul class="nav navbar-nav navbar-right">
  56 + <li class="dropdown">
  57 + <a class="dropdown-toggle" data-toggle="dropdown" href="#">
  58 + <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
  59 + <span class="caret"></span>
  60 + </a>
  61 + <ul class="dropdown-menu">
  62 + <li class="active"><a href="/test">Teste</a></li>
  63 + <li><a data-toggle="modal" data-target="#sair" id="form-button-submit"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li>
  64 + </ul>
  65 + </li>
  66 + </ul>
  67 + </div>
  68 + </div>
  69 +</nav>
  70 +
35 <div class="container"> 71 <div class="container">
36 72
37 - <div class="panel panel-success drop-shadow"> 73 + <div class="well drop-shadow">
  74 + <dl class="dl-horizontal">
  75 + <dt>Title</dt><dd id="title"></dd>
  76 + <dt>Ref</dt><dd><code id="ref"></code></dd>
  77 + <dt>Test</dt><dd><code id="filename"></code></dd>
  78 + <dt>Database</dt><dd><code id="database"></code></dd>
  79 + <dt>Saved tests</dt><dd><code id="answers_dir"></code></dd>
  80 + </dl>
  81 + </div>
  82 +
  83 + <div class="panel panel-primary drop-shadow">
38 <div id="online-header" class="panel-heading"> 84 <div id="online-header" class="panel-heading">
39 - Activo(s) 85 + <!-- to be populated -->
40 </div> 86 </div>
41 <div class="panel-body"> 87 <div class="panel-body">
42 <table class="table table-condensed"> 88 <table class="table table-condensed">
@@ -44,7 +90,8 @@ @@ -44,7 +90,8 @@
44 <tr> 90 <tr>
45 <th>Número</th> 91 <th>Número</th>
46 <th>Nome</th> 92 <th>Nome</th>
47 - <th>Início do teste</th> 93 + <th>Data de início</th>
  94 + <th>Hora de início</th>
48 </tr> 95 </tr>
49 </thead> 96 </thead>
50 <tbody id="online_students"> 97 <tbody id="online_students">
@@ -55,24 +102,45 @@ @@ -55,24 +102,45 @@
55 </div> 102 </div>
56 103
57 <div class="panel panel-primary drop-shadow"> 104 <div class="panel panel-primary drop-shadow">
58 - <div id="offline-header" class="panel-heading">  
59 - Inactivo(s) 105 + <div id="students-header" class="panel-heading">
  106 + <!-- to be populated -->
60 </div> 107 </div>
61 <div class="panel-body"> 108 <div class="panel-body">
62 <table class="table table-condensed"> 109 <table class="table table-condensed">
63 <thead> 110 <thead>
64 <tr> 111 <tr>
65 - <th>Autorizado</th> 112 + <th>Perm.</th>
66 <th>Número</th> 113 <th>Número</th>
67 <th>Nome</th> 114 <th>Nome</th>
68 - <th>Password</th> 115 + <th>Estado</th>
  116 + <th>Nota</th>
69 </tr> 117 </tr>
70 </thead> 118 </thead>
71 - <tbody id="offline_students"> 119 + <tbody id="students">
72 <!-- to be populated --> 120 <!-- to be populated -->
73 </tbody> 121 </tbody>
74 </table> 122 </table>
75 </div> 123 </div>
  124 + <div class="panel-footer">
  125 + <div class="row">
  126 + <div class="col-sm-4">
  127 + Permitir
  128 + <button id="allow_all" class="btn btn-xs btn-danger">Todos</button>
  129 + <button id="deny_all" class="btn btn-xs btn-danger">Nenhum</button>
  130 + </div>
  131 + <div class="col-sm-4">
  132 + <button id="novo_aluno" class="btn btn-xs btn-danger">Inserir novo aluno</button>
  133 + </div>
  134 + <div class="col-sm-4">
  135 + <div class="input-group input-group-sm">
  136 + <input id="reset_number" type="text" class="form-control" placeholder="Número">
  137 + <span class="input-group-btn">
  138 + <button id="reset_password" class="btn btn-danger" type="button">Reset password!</button>
  139 + </span>
  140 + </div><!-- /input-group -->
  141 + </div>
  142 + </div>
  143 + </div>
76 </div> 144 </div>
77 145
78 </div> <!-- container --> 146 </div> <!-- container -->