Commit 8584ceb0feb3a50227068967d9955635b2616568
1 parent
a1c9a10e
Exists in
master
and in
1 other branch
- /admin has been completly rebuilt (still missing: insert new student)
Showing
6 changed files
with
269 additions
and
144 deletions
Show diff stats
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__ == '__main__': |
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__ == '__main__': |
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 --> | ... | ... |