From 8584ceb0feb3a50227068967d9955635b2616568 Mon Sep 17 00:00:00 2001 From: Miguel Barao Date: Thu, 2 Jun 2016 16:26:48 +0100 Subject: [PATCH] - /admin has been completly rebuilt (still missing: insert new student) --- BUGS.md | 20 ++++++++++++-------- app.py | 43 +++++++++++++++++++++++++++++++++++-------- database.py | 7 ++++++- serve.py | 112 +++++++++++++++++++++++++++++++--------------------------------------------------------------------------------- static/js/admin.js | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------- templates/admin.html | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 6 files changed, 269 insertions(+), 144 deletions(-) diff --git a/BUGS.md b/BUGS.md index e235aae..6aace46 100644 --- a/BUGS.md +++ b/BUGS.md @@ -1,30 +1,34 @@ # BUGS -- pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static... -- usar thread.Lock para aceder a variaveis de estado. -- permitir adicionar imagens nas perguntas -- browser e ip usados gravado no test. +- alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) - configuracao dos logs cherrypy para se darem bem com os outros +- browser e ip usados gravado no test. +- usar thread.Lock para aceder a variaveis de estado. +- permitir adicionar imagens nas perguntas. +- argumentos da linha de comando a funcionar. # TODO - implementar practice mode. +- botões allow all/deny all. +- enviar logs para web? - SQLAlchemy em vez da classe database. -- argumentos da linha de comando a funcionar. - aviso na pagina principal para quem usa browser da treta - permitir varios testes, aluno escolhe qual o teste que quer fazer. - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput - perguntas para professor corrigir mais tarde. -- single page web no frontend -- criar script json2md.py ou outra forma de visualizar um teste ja realizado +- single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). +- visualizar um teste ja realizado na página de administração - Menu para professor com link para /results e /students - fazer uma calculadora javascript e por no menu. surge como modal - GeoIP? -- mostrar botão de reset apenas para alunos com password definida? +- mostrar botão de reset apenas no final da pagina, com edit para escrever o número. # FIXED +- 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. +- pagina de login nao esta a apresentar bem. parece que precisa de autorizacao para aceder a /static... - Não mostrar Professor nos activos em /admin - /admin mostrar actualizações automaticamente? - se no teste uma das "ref" nao existir nos ficheiros de perguntas, rebenta. diff --git a/app.py b/app.py index 69e5e37..cd87db3 100644 --- a/app.py +++ b/app.py @@ -23,7 +23,7 @@ class App(object): # } logger.info('============= Running perguntations =============') self.online = dict() # {uid: {'student':{}}} - self.allowed = set([13361,13682]) # '0' is hardcoded to allowed elsewhere + self.allowed = set([]) # '0' is hardcoded to allowed elsewhere FIXME self.testfactory = test.TestFactory(filename, conf=conf) self.db = database.Database(self.testfactory['database']) # FIXME try: @@ -117,20 +117,47 @@ class App(object): return self.online[uid].get('test', default) def get_test_qtypes(self, uid): return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} - def get_student_grades(self, uid): - return self.db.get_student_grades(uid) + def get_student_grades_from_all_tests(self, uid): + return self.db.get_student_grades_from_all_tests(uid) + + # def get_student_grades_from_test(self, uid, testid): + # return self.db.get_student_grades_from_test(uid, testid) + + + # def get_online_students(self): + # # list of ('uid', 'name', 'start_time') sorted by start time + # return sorted( + # ((k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'), + # key=lambda k: k[2] # sort key + # ) def get_online_students(self): - # list of ('uid', 'name', 'start_time') sorted by start time - return sorted( - ((k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'), - key=lambda k: k[2] # sort key - ) + # {'123': '2016-12-02 12:04:12.344243', ...} + return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'] def get_offline_students(self): # list of ('uid', 'name') sorted by number return sorted((s[:2] for s in self.db.get_all_students() if s[0] not in self.online), key=lambda k: k[0]) + def get_all_students(self): + # list of ('uid', 'name') sorted by number + return sorted((s[:2] for s in self.db.get_all_students() if s[0] != '0'), key=lambda k: k[0]) + + def get_students_state(self): + # {'123': {'name': 'John', 'start_time':'', 'grades':[10.2, 13.1], ...}} + d = {} + for s in self.db.get_all_students(): + uid, name, pw = s + if uid == '0': + continue + d[uid] = {'name': name} + d[uid]['allowed'] = uid in self.allowed + d[uid]['online'] = uid in self.online + d[uid]['start_time'] = self.online.get(uid, {}).get('test', {}).get('start_time','') + d[uid]['password_defined'] = pw != '' + d[uid]['grades'] = self.db.get_student_grades_from_test(uid, self.testfactory['ref']) + return d + # def get_this_students_grades(self): # # list of ('uid', 'name') sorted by number # return self.db.get_students_grades(self.testfactory['ref']) diff --git a/database.py b/database.py index 74ad141..d45132e 100644 --- a/database.py +++ b/database.py @@ -45,11 +45,16 @@ class Database(object): # return grades.fetchall() # get results from previous tests of a student - def get_student_grades(self, uid): + def get_student_grades_from_all_tests(self, uid): with sqlite3.connect(self.db) as c: grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid]) return grades.fetchall() + def get_student_grades_from_test(self, uid, testid): + with sqlite3.connect(self.db) as c: + grades = c.execute('SELECT grade,finish_time FROM tests WHERE student_id==? and test_id==?', [uid, testid]) + return grades.fetchall() + def save_test(self, t): with sqlite3.connect(self.db) as c: # save final grade of the test diff --git a/serve.py b/serve.py index 00949c6..b2ebf75 100755 --- a/serve.py +++ b/serve.py @@ -75,23 +75,21 @@ def secureheaders(): # ============================================================================ class AdminWebService(object): exposed = True + _cp_config = { + 'auth.require': [name_is('0')] + } def __init__(self, app): self.app = app @cherrypy.tools.accept(media='application/json') # FIXME - @require(name_is('0')) def GET(self): data = { - 'online': self.app.get_online_students(), - 'offline': self.app.get_offline_students(), - 'allowed': list(self.app.get_allowed_students()), - # 'finished': self.app.get_this_students_grades() + 'students': list(self.app.get_students_state().items()), + 'test': self.app.testfactory } - # print(dict(data['finished'])) return json.dumps(data, default=str) - @require(name_is('0')) def POST(self, **args): # print('POST', args) # FIXME if args['cmd'] == 'allow': @@ -117,20 +115,17 @@ class Root(object): 'admin': t.get_template('/admin.html'), } + # --- DEFAULT ------------------------------------------------------------ @cherrypy.expose @require() - def default(self, **args): + def default(self, *args, **kwargs): uid = cherrypy.session.get(SESSION_KEY) if uid == '0': raise cherrypy.HTTPRedirect('/admin') else: raise cherrypy.HTTPRedirect('/test') - # # FIXME - # title = self.app.testfactory['title'] - # return '''Start test here: {}'''.format(title) - # # raise cherrypy.HTTPRedirect('/test') - + # --- LOGIN -------------------------------------------------------------- @cherrypy.expose def login(self, uid=None, pw=None): if uid is None or pw is None: # first try @@ -138,11 +133,11 @@ class Root(object): if self.app.login(uid, pw): # ok cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid - raise cherrypy.HTTPRedirect('/admin') # FIXME + raise cherrypy.HTTPRedirect('/') # FIXME else: # denied return self.template['login'].render() - + # --- LOGOUT ------------------------------------------------------------- @cherrypy.expose @require() def logout(self): @@ -154,8 +149,7 @@ class Root(object): self.app.logout(uid) raise cherrypy.HTTPRedirect('/') - - # --- TEST --------------------------------------------------------------- + # --- TEST --------------------------------------------------------------- # Get student number and assigned questions from current session. # If it's the first time, create instance of the test and register the # time. @@ -200,71 +194,29 @@ class Root(object): grade = self.app.correct_test(uid, ans) self.app.logout(uid) - # ---- Expire session ---- + # --- Expire session cherrypy.lib.sessions.expire() # session coockie expires client side cherrypy.session[SESSION_KEY] = cherrypy.request.login = None - # ---- Show result to student ---- + # --- Show result to student return self.template['grade'].render( title=title, student_id=uid + ' - ' + student_name, grade=grade, - allgrades=self.app.get_student_grades(uid) + allgrades=self.app.get_student_grades_from_all_tests(uid) ) - - - # --- STUDENTS ----------------------------------------------------------- + # --- ADMIN -------------------------------------------------------------- @cherrypy.expose @require(name_is('0')) def admin(self, **reset_pw): - return self.template['admin'].render( - online_students=self.app.get_online_students(), - offline_students=self.app.get_offline_students(), - allowed_students=self.app.get_allowed_students() - ) - # t = TemplateLookup(directories=[TEMPLATES_DIR], input_encoding='utf-8').get_template('admin.html') - # return t.render(online_students=online_students, - # offline_students=offline_students, - # allowed_students=allowed_students) - - - - - - # def students(self, **reset_pw): - # if reset_pw: - # self.database.student_reset_pw(reset_pw) - # for num in reset_pw: - # cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION') - - # grades = self.database.test_grades2(self.testconf['ref']) - # students = self.database.get_students() - - # template = self.templates.get_template('/students.html') - # return template.render(students=students, tags=self.tags, grades=grades) - - # --- RESULTS ------------------------------------------------------------ - # @cherrypy.expose - # @require() - # def results(self): - # if self.testconf.get('practice', False): - # uid = cherrypy.session.get('userid') - # name = cherrypy.session.get('name') - - # r = self.database.test_grades(self.testconf['ref']) - # template = self.templates.get_template('/results.html') - # return template.render(t=self.testconf, results=r, name=name, uid=uid) - # else: - # raise cherrypy.HTTPRedirect('/') - - + return self.template['admin'].render() # ============================================================================ def parse_arguments(): 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.') serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) - argparser.add_argument('--server', default=serverconf_file, type=str, help='server configuration file') + argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file') # argparser.add_argument('--debug', action='store_true', # help='Show datastructures when rendering questions') # argparser.add_argument('--show_ref', action='store_true', @@ -301,29 +253,28 @@ if __name__ == '__main__': except: sys.exit(1) - - # testconf = test.TestFactory(filename, conf=vars(arg)) - + # --- create webserver + webapp = Root(app) + webapp.adminwebservice = AdminWebService(app) # --- site wide configuration (valid for all apps) cherrypy.tools.secureheaders = cherrypy.Tool('before_finalize', secureheaders, priority=60) - cherrypy.config.update({'tools.staticdir.root': SERVER_PATH}) - cherrypy.config.update(arg.server) + + cherrypy.config.update(arg.conf) # configuration file in /config conf = { '/': { - # DO NOT DISABLE SESSIONS! 'tools.sessions.on': True, - 'tools.sessions.timeout': 240, - 'tools.sessions.storage_type': 'ram', - 'tools.sessions.storage_path': 'sessions', + 'tools.sessions.timeout': 240, # sessions last 4 hours + 'tools.sessions.storage_type': 'ram', # or 'file' + 'tools.sessions.storage_path': 'sessions', # if storage_type='file' # tools.sessions.secure = True # tools.sessions.httponly = True - # Authentication + # Turn on authentication (required for check_auth to work) 'tools.auth.on': True, 'tools.secureheaders.on': True, - + 'tools.staticdir.root': SERVER_PATH, 'tools.staticdir.dir': 'static', # where to get js,css,jpg,... 'tools.staticdir.on': True, }, @@ -333,16 +284,15 @@ if __name__ == '__main__': 'tools.response_headers.headers': [('Content-Type', 'text/plain')], }, '/static': { - 'tools.staticdir.dir': './static', # where to get js,css,jpg,... + 'tools.auth.on': False, # everything in /static is public 'tools.staticdir.on': True, - } + 'tools.staticdir.dir': 'static', # where to get js,css,jpg,... + }, } # --- app specific configuration - webapp = Root(app) - webapp.adminwebservice = AdminWebService(app) - cherrypy.tree.mount(webapp, '/', conf) + cherrypy.tree.mount(webapp, script_name='/', config=conf) # logger.info('Webserver listening at {}:{}'.format( # cherrypy.config['server.socket_host'], diff --git a/static/js/admin.js b/static/js/admin.js index 6e21063..c5cc1ae 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,15 +1,34 @@ $(document).ready(function() { - // button handler to allow all students - $("#allowall").click(function(e) { - alert('not implemented'); // FIXME - }); - - // button handler to allow all students - $("#denyall").click(function(e) { - alert('not implemented'); // FIXME - }); + // button handlers (runs once) + function define_buttons_handlers() { + $("#allow_all").click( + function() { + $(":checkbox").prop("checked", true).trigger('change'); + } + ); + $("#deny_all").click( + function() { + $(":checkbox").prop("checked", false).trigger('change'); + } + ); + $("#reset_password").click( + function () { + var number = $("#reset_number").val(); + $.ajax({ + type: "POST", + url: "/adminwebservice", + data: {"cmd": "reset", "name": number} + }); + } + ); + $("#novo_aluno").click( + function () { + alert('Não implementado!'); + } + ); + } - // checkbox event handler to allow/deny students + // checkbox handler to allow/deny students individually function autorizeStudent(e) { $.ajax({ type: "POST", @@ -22,36 +41,75 @@ $(document).ready(function() { $(this).parent().parent().removeClass("active"); } - // button handler to reset student password - function resetPassword(e) { - $.ajax({ - type: "POST", - url: "/adminwebservice", - data: {"cmd": "reset", "name": this.value} - }); - } - - function populateOnlineTable(online) { - $("#online-header").html(online.length + " Activo(s)"); + function populateOnlineTable(students) { + var active = []; var rows = ""; - $.each(online, function(i, r) { - rows += "" + r[0] + "" + r[1] + "" + r[2].slice(0,19) + ""; + $.each(students, function(i, r) { + if (r[1]['start_time'] != '') { + active.push([r[0], r[1]['name'], r[1]['start_time']]); + } }); + active.sort(function(a,b){return a[2] < b[2] ? -1 : (a[2] == b[2] ? 0 : 1);}); + n = active.length; + for(var i = 0; i < n; i++) { + rows += "" + active[i][0] + "" + active[i][1] + "" + active[i][2].slice(0,10) + "" + active[i][2].slice(11,19) + ""; + } $("#online_students").html(rows); + $("#online-header").html(n + " Activo(s)"); + } + + function generate_grade_bar(grade) { + var barcolor; + if (grade < 10) { + barcolor = 'progress-bar-danger'; + } + else if (grade < 15) { + barcolor = 'progress-bar-warning'; + } + else { + barcolor = 'progress-bar-success'; + } + + var bar = '
' + grade + '
'; + return bar } - function populateOfflineTable(offline, allowed) { - $("#offline-header").html(offline.length + " Inactivo(s)") + function populateStudentsTable(students) { + $("#students-header").html(students.length + " Alunos") var rows = ""; - $.each(offline, function(i, r) { - rows += '\ - -1? 'checked':'') + '>\ - ' + r[0] + '\ - ' + r[1] + '\ - \ - '; + students.sort(function(a,b){return a[0] - b[0]}); + $.each(students, function(i, r) { + var uid = r[0]; + var d = r[1]; // dictionary + + if (d['start_time'] != '') // test + rows += ''; + else if (d['online']) // online + rows += ''; + else if (d['allowed']) // allowed + rows += ''; + else // offline + rows += ''; + + rows += '\ + \ + ' + uid + '\ + ' + d['name'] + '\ + ' + + (d['password_defined'] ? 'pw' : '') + + // (d['online'] ? 'online' : '') + + (d['start_time']==''?'':'teste') + + '\ + '; + var g = d['grades']; + var glength = g.length; + for (var i=0; i < glength; i++) { + rows += '
' + generate_grade_bar(g[i][0]) + '
'; + } + rows += ''; }); - $("#offline_students").html(rows); + $("#students").html(rows); + $('[data-toggle="tooltip"]').tooltip(); } function populate() { @@ -59,17 +117,30 @@ $(document).ready(function() { url: "/adminwebservice", dataType: "json", success: function(data) { - populateOnlineTable(data["online"]); - populateOfflineTable(data["offline"], data["allowed"]); + var t = new Date(); + $('#currenttime').html(t.getHours() + (t.getMinutes() < 10 ? ':0' : ':') + t.getMinutes()); + $("#title").html(data['test']['title']); + $("#ref").html(data['test']['ref']); + $("#database").html(data['test']['database']); + if (data['test']['save_answers']) { + $("#answers_dir").html(data['test']['answers_dir']); + } + else { + $("#answers_dir").html('--- not being saved ---'); + } + $("#filename").html(data['test']['filename']); + + populateOnlineTable(data["students"]); + populateStudentsTable(data["students"]) // add event handlers $('input[type="checkbox"]').change(autorizeStudent); - $('button[name="reset"]').click(resetPassword); }, error: function() {alert("Servidor não responde.");} }); } - populate(); // run once when the page is loaded + populate(); // run once when the page is loaded + define_buttons_handlers(); setInterval(populate, 5000); // poll server on 5s interval }); diff --git a/templates/admin.html b/templates/admin.html index 61aeccd..3b35efb 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -4,7 +4,7 @@ - List of students + Admin @@ -25,6 +25,10 @@ box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2); border-radius:5px; } + .progress { + /*display: inline-block;*/ + margin-bottom: 0 !important; + } @@ -32,11 +36,53 @@ + +
-
+
+
+
Title
+
Ref
+
Test
+
Database
+
Saved tests
+
+
+ +
- Activo(s) +
@@ -44,7 +90,8 @@ - + + @@ -55,24 +102,45 @@
-
- Inactivo(s) +
+
Número NomeInício do testeData de inícioHora de início
- + - + + - +
AutorizadoPerm. Número NomePasswordEstadoNota
+
-- libgit2 0.21.2