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 = '';
+ 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
+
+
+
+
@@ -44,7 +90,8 @@
Número |
Nome |
- Início do teste |
+ Data de início |
+ Hora de início |
@@ -55,24 +102,45 @@
-
--
libgit2 0.21.2