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 --> |