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)

BUGS.md
1 1  
2 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 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 11 # TODO
11 12  
12 13 - implementar practice mode.
  14 +- botões allow all/deny all.
  15 +- enviar logs para web?
13 16 - SQLAlchemy em vez da classe database.
14   -- argumentos da linha de comando a funcionar.
15 17 - aviso na pagina principal para quem usa browser da treta
16 18 - permitir varios testes, aluno escolhe qual o teste que quer fazer.
17 19 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput
18 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 23 - Menu para professor com link para /results e /students
22 24 - fazer uma calculadora javascript e por no menu. surge como modal
23 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 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 32 - Não mostrar Professor nos activos em /admin
29 33 - /admin mostrar actualizações automaticamente?
30 34 - se no teste uma das "ref" nao existir nos ficheiros de perguntas, rebenta.
... ...
app.py
... ... @@ -23,7 +23,7 @@ class App(object):
23 23 # }
24 24 logger.info('============= Running perguntations =============')
25 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 27 self.testfactory = test.TestFactory(filename, conf=conf)
28 28 self.db = database.Database(self.testfactory['database']) # FIXME
29 29 try:
... ... @@ -117,20 +117,47 @@ class App(object):
117 117 return self.online[uid].get('test', default)
118 118 def get_test_qtypes(self, uid):
119 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 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 138 def get_offline_students(self):
131 139 # list of ('uid', 'name') sorted by number
132 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 161 # def get_this_students_grades(self):
135 162 # # list of ('uid', 'name') sorted by number
136 163 # return self.db.get_students_grades(self.testfactory['ref'])
... ...
database.py
... ... @@ -45,11 +45,16 @@ class Database(object):
45 45 # return grades.fetchall()
46 46  
47 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 49 with sqlite3.connect(self.db) as c:
50 50 grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid])
51 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 58 def save_test(self, t):
54 59 with sqlite3.connect(self.db) as c:
55 60 # save final grade of the test
... ...
serve.py
... ... @@ -75,23 +75,21 @@ def secureheaders():
75 75 # ============================================================================
76 76 class AdminWebService(object):
77 77 exposed = True
  78 + _cp_config = {
  79 + 'auth.require': [name_is('0')]
  80 + }
78 81  
79 82 def __init__(self, app):
80 83 self.app = app
81 84  
82 85 @cherrypy.tools.accept(media='application/json') # FIXME
83   - @require(name_is('0'))
84 86 def GET(self):
85 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 91 return json.dumps(data, default=str)
93 92  
94   - @require(name_is('0'))
95 93 def POST(self, **args):
96 94 # print('POST', args) # FIXME
97 95 if args['cmd'] == 'allow':
... ... @@ -117,20 +115,17 @@ class Root(object):
117 115 'admin': t.get_template('/admin.html'),
118 116 }
119 117  
  118 + # --- DEFAULT ------------------------------------------------------------
120 119 @cherrypy.expose
121 120 @require()
122   - def default(self, **args):
  121 + def default(self, *args, **kwargs):
123 122 uid = cherrypy.session.get(SESSION_KEY)
124 123 if uid == '0':
125 124 raise cherrypy.HTTPRedirect('/admin')
126 125 else:
127 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 129 @cherrypy.expose
135 130 def login(self, uid=None, pw=None):
136 131 if uid is None or pw is None: # first try
... ... @@ -138,11 +133,11 @@ class Root(object):
138 133  
139 134 if self.app.login(uid, pw): # ok
140 135 cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid
141   - raise cherrypy.HTTPRedirect('/admin') # FIXME
  136 + raise cherrypy.HTTPRedirect('/') # FIXME
142 137 else: # denied
143 138 return self.template['login'].render()
144 139  
145   -
  140 + # --- LOGOUT -------------------------------------------------------------
146 141 @cherrypy.expose
147 142 @require()
148 143 def logout(self):
... ... @@ -154,8 +149,7 @@ class Root(object):
154 149 self.app.logout(uid)
155 150 raise cherrypy.HTTPRedirect('/')
156 151  
157   -
158   - # --- TEST ---------------------------------------------------------------
  152 + # --- TEST ---------------------------------------------------------------
159 153 # Get student number and assigned questions from current session.
160 154 # If it's the first time, create instance of the test and register the
161 155 # time.
... ... @@ -200,71 +194,29 @@ class Root(object):
200 194 grade = self.app.correct_test(uid, ans)
201 195 self.app.logout(uid)
202 196  
203   - # ---- Expire session ----
  197 + # --- Expire session
204 198 cherrypy.lib.sessions.expire() # session coockie expires client side
205 199 cherrypy.session[SESSION_KEY] = cherrypy.request.login = None
206 200  
207   - # ---- Show result to student ----
  201 + # --- Show result to student
208 202 return self.template['grade'].render(
209 203 title=title,
210 204 student_id=uid + ' - ' + student_name,
211 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 210 @cherrypy.expose
219 211 @require(name_is('0'))
220 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 216 def parse_arguments():
265 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 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 220 # argparser.add_argument('--debug', action='store_true',
269 221 # help='Show datastructures when rendering questions')
270 222 # argparser.add_argument('--show_ref', action='store_true',
... ... @@ -301,29 +253,28 @@ if __name__ == &#39;__main__&#39;:
301 253 except:
302 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 260 # --- site wide configuration (valid for all apps)
309 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 264 conf = {
313 265 '/': {
314   - # DO NOT DISABLE SESSIONS!
315 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 270 # tools.sessions.secure = True
320 271 # tools.sessions.httponly = True
321 272  
322   - # Authentication
  273 + # Turn on authentication (required for check_auth to work)
323 274 'tools.auth.on': True,
324 275  
325 276 'tools.secureheaders.on': True,
326   -
  277 + 'tools.staticdir.root': SERVER_PATH,
327 278 'tools.staticdir.dir': 'static', # where to get js,css,jpg,...
328 279 'tools.staticdir.on': True,
329 280 },
... ... @@ -333,16 +284,15 @@ if __name__ == &#39;__main__&#39;:
333 284 'tools.response_headers.headers': [('Content-Type', 'text/plain')],
334 285 },
335 286 '/static': {
336   - 'tools.staticdir.dir': './static', # where to get js,css,jpg,...
  287 + 'tools.auth.on': False, # everything in /static is public
337 288 'tools.staticdir.on': True,
338   - }
  289 + 'tools.staticdir.dir': 'static', # where to get js,css,jpg,...
  290 + },
339 291 }
340 292  
341 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 297 # logger.info('Webserver listening at {}:{}'.format(
348 298 # cherrypy.config['server.socket_host'],
... ...
static/js/admin.js
1 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 32 function autorizeStudent(e) {
14 33 $.ajax({
15 34 type: "POST",
... ... @@ -22,36 +41,75 @@ $(document).ready(function() {
22 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 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 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 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 115 function populate() {
... ... @@ -59,17 +117,30 @@ $(document).ready(function() {
59 117 url: "/adminwebservice",
60 118 dataType: "json",
61 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 136 // add event handlers
66 137 $('input[type="checkbox"]').change(autorizeStudent);
67   - $('button[name="reset"]').click(resetPassword);
68 138 },
69 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 145 setInterval(populate, 5000); // poll server on 5s interval
75 146 });
... ...
templates/admin.html
... ... @@ -4,7 +4,7 @@
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> List of students </title>
  7 + <title> Admin </title>
8 8 <link rel="icon" href="favicon.ico">
9 9  
10 10 <!-- Bootstrap -->
... ... @@ -25,6 +25,10 @@
25 25 box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2);
26 26 border-radius:5px;
27 27 }
  28 + .progress {
  29 + /*display: inline-block;*/
  30 + margin-bottom: 0 !important;
  31 + }
28 32 </style>
29 33  
30 34 <script src="/js/admin.js"></script>
... ... @@ -32,11 +36,53 @@
32 36 <!-- ===================================================================== -->
33 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 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 84 <div id="online-header" class="panel-heading">
39   - Activo(s)
  85 + <!-- to be populated -->
40 86 </div>
41 87 <div class="panel-body">
42 88 <table class="table table-condensed">
... ... @@ -44,7 +90,8 @@
44 90 <tr>
45 91 <th>Número</th>
46 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 95 </tr>
49 96 </thead>
50 97 <tbody id="online_students">
... ... @@ -55,24 +102,45 @@
55 102 </div>
56 103  
57 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 107 </div>
61 108 <div class="panel-body">
62 109 <table class="table table-condensed">
63 110 <thead>
64 111 <tr>
65   - <th>Autorizado</th>
  112 + <th>Perm.</th>
66 113 <th>Número</th>
67 114 <th>Nome</th>
68   - <th>Password</th>
  115 + <th>Estado</th>
  116 + <th>Nota</th>
69 117 </tr>
70 118 </thead>
71   - <tbody id="offline_students">
  119 + <tbody id="students">
72 120 <!-- to be populated -->
73 121 </tbody>
74 122 </table>
75 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 144 </div>
77 145  
78 146 </div> <!-- container -->
... ...