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 | # 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. |
app.py
@@ -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']) |
database.py
@@ -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 |
serve.py
@@ -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__ == '__main__': | @@ -301,29 +253,28 @@ if __name__ == '__main__': | ||
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__ == '__main__': | @@ -333,16 +284,15 @@ if __name__ == '__main__': | ||
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 --> |