Commit fac28ea70d8bc4e99b371dc073b7e3c93a990b6b

Authored by Miguel Barao
1 parent 01b86197
Exists in master and in 1 other branch dev

- fixed threading issue with sqlalchemy.

- removed all sqlite3 code.
- fixed sorting students in admin.
- added logger configuration files.
1 1
2 # BUGS 2 # BUGS
3 3
4 -- criar sqlalchemy sessions dentro de app de modo a estarem associadas a requests. ver se é facil usar with db:(...) para criar e fechar sessão.  
5 - usar thread.Lock para aceder a variaveis de estado? 4 - usar thread.Lock para aceder a variaveis de estado?
6 - permitir adicionar imagens nas perguntas. 5 - permitir adicionar imagens nas perguntas.
7 - debug mode: log levels not working 6 - debug mode: log levels not working
@@ -23,6 +22,7 @@ @@ -23,6 +22,7 @@
23 22
24 # FIXED 23 # FIXED
25 24
  25 +- criar sqlalchemy sessions dentro de app de modo a estarem associadas a requests. ver se é facil usar with db:(...) para criar e fechar sessão.
26 - sqlalchemy queixa-se de threads. 26 - sqlalchemy queixa-se de threads.
27 - SQLAlchemy em vez da classe database. 27 - SQLAlchemy em vez da classe database.
28 - replace sys.exit calls 28 - replace sys.exit calls
1 1
2 2
3 -import logging  
4 from os import path 3 from os import path
5 -import sqlite3 4 +import logging
6 import bcrypt 5 import bcrypt
  6 +from sqlalchemy import create_engine
  7 +from sqlalchemy.orm import sessionmaker, scoped_session
  8 +from models import Base, Student, Test, Question
  9 +from contextlib import contextmanager # to create `with` statement for db sessions
7 10
8 import test 11 import test
9 -import database 12 +import threading
10 13
11 logger = logging.getLogger(__name__) 14 logger = logging.getLogger(__name__)
12 15
@@ -18,64 +21,85 @@ class App(object): @@ -18,64 +21,85 @@ class App(object):
18 def __init__(self, filename, conf): 21 def __init__(self, filename, conf):
19 # online = { 22 # online = {
20 # uid1: { 23 # uid1: {
21 - # 'student': {'number': 123, 'name': john},  
22 - # 'test': ... 24 + # 'student': {'number': 123, 'name': john, ...},
  25 + # 'test': {...}
23 # } 26 # }
24 # uid2: {...} 27 # uid2: {...}
25 # } 28 # }
26 logger.info('============= Running perguntations =============') 29 logger.info('============= Running perguntations =============')
27 self.online = dict() # {uid: {'student':{}}} 30 self.online = dict() # {uid: {'student':{}}}
28 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere 31 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere
  32 +
29 self.testfactory = test.TestFactory(filename, conf=conf) 33 self.testfactory = test.TestFactory(filename, conf=conf)
30 - self.db = database.Database(self.testfactory['database']) # FIXME 34 +
  35 + # database
  36 + engine = create_engine('sqlite:///{}'.format(self.testfactory['database']), echo=False)
  37 + Base.metadata.create_all(engine) # Criate schema if needed FIXME no student '0'
  38 + self.Session = scoped_session(sessionmaker(bind=engine))
  39 +
31 try: 40 try:
32 - n = self.db.get_count_students()  
33 - except sqlite3.OperationalError as e:  
34 - logger.critical('Database not usable {}.'.format(self.db.db)) 41 + with self.db_session() as s:
  42 + n = s.query(Student).filter(Student.id != '0').count()
  43 + except Exception as e:
  44 + logger.critical('Database not usable {}.'.format(self.testfactory['database']))
35 raise e 45 raise e
36 else: 46 else:
37 logger.info('Database has {} students registered.'.format(n)) 47 logger.info('Database has {} students registered.'.format(n))
38 48
39 49
  50 + # -----------------------------------------------------------------------
  51 + # helper to manage db sessions using the `with` statement, for example
  52 + # with self.db_session() as s: ...
  53 + @contextmanager
  54 + def db_session(self):
  55 + try:
  56 + yield self.Session()
  57 + finally:
  58 + self.Session.remove()
  59 +
  60 + # -----------------------------------------------------------------------
40 def exit(self): 61 def exit(self):
  62 + # FIXME what if there are online students?
41 logger.critical('----------- !!! Server terminated !!! -----------') 63 logger.critical('----------- !!! Server terminated !!! -----------')
42 64
43 - 65 + # -----------------------------------------------------------------------
44 def login(self, uid, try_pw): 66 def login(self, uid, try_pw):
45 if uid not in self.allowed and uid != '0': 67 if uid not in self.allowed and uid != '0':
46 # not allowed 68 # not allowed
47 logger.warning('Student {}: not allowed to login.'.format(uid)) 69 logger.warning('Student {}: not allowed to login.'.format(uid))
48 return False 70 return False
49 71
50 - student = self.db.get_student(uid)  
51 - if student is None:  
52 - # not found  
53 - logger.warning('Student {}: not found in database.'.format(uid))  
54 - return False  
55 -  
56 - # uid found in database  
57 - name, pw = student  
58 -  
59 - if pw == '':  
60 - # update password on first login  
61 - hashed_pw = bcrypt.hashpw(try_pw.encode('utf-8'), bcrypt.gensalt())  
62 - self.db.update_password(uid, hashed_pw)  
63 - logger.warning('Student {}: first login, password updated.'.format(uid))  
64 - elif bcrypt.hashpw(try_pw.encode('utf-8'), pw) != pw:  
65 - # wrong password  
66 - logger.info('Student {}: wrong password.'.format(uid))  
67 - return False 72 + with self.db_session() as s:
  73 + student = s.query(Student).filter(Student.id == uid).one_or_none()
  74 +
  75 + if student is None:
  76 + # not found
  77 + logger.warning('Student {}: not found in database.'.format(uid))
  78 + return False
  79 +
  80 + if student.password == '':
  81 + # update password on first login
  82 + hashed_pw = bcrypt.hashpw(try_pw.encode('utf-8'), bcrypt.gensalt())
  83 + student.password = hashed_pw
  84 + s.commit()
  85 + logger.warning('Student {}: first login, password updated.'.format(uid))
  86 +
  87 + elif bcrypt.hashpw(try_pw.encode('utf-8'), student.password) != student.password:
  88 + # wrong password
  89 + logger.info('Student {}: wrong password.'.format(uid))
  90 + return False
  91 +
  92 + # success
  93 + self.allowed.discard(uid)
  94 + if uid in self.online:
  95 + logger.warning('Student {}: already logged in.'.format(uid))
  96 + else:
  97 + self.online[uid] = {'student': {'name': student.name, 'number': uid}}
  98 + logger.info('Student {}: logged in.'.format(uid))
68 99
69 - # success  
70 - self.allowed.discard(uid)  
71 - if uid in self.online:  
72 - logger.warning('Student {}: already logged in.'.format(uid))  
73 - else:  
74 - self.online[uid] = {'student': {'name': name, 'number': uid}}  
75 - logger.info('Student {}: logged in.'.format(uid))  
76 return True 100 return True
77 101
78 - 102 + # -----------------------------------------------------------------------
79 def logout(self, uid): 103 def logout(self, uid):
80 if uid not in self.online: 104 if uid not in self.online:
81 # this should never happen 105 # this should never happen
@@ -83,9 +107,10 @@ class App(object): @@ -83,9 +107,10 @@ class App(object):
83 return False 107 return False
84 else: 108 else:
85 logger.info('Student {}: logged out.'.format(uid)) 109 logger.info('Student {}: logged out.'.format(uid))
86 - del self.online[uid] # Nao está a gravar o teste como desistencia... 110 + del self.online[uid] # FIXME Nao está a gravar o teste como desistencia...
87 return True 111 return True
88 112
  113 + # -----------------------------------------------------------------------
89 def generate_test(self, uid): 114 def generate_test(self, uid):
90 if uid in self.online: 115 if uid in self.online:
91 logger.info('Student {}: generating new test.'.format(uid)) 116 logger.info('Student {}: generating new test.'.format(uid))
@@ -96,6 +121,7 @@ class App(object): @@ -96,6 +121,7 @@ class App(object):
96 logger.error('Student {}: offline, can''t generate test'.format(uid)) 121 logger.error('Student {}: offline, can''t generate test'.format(uid))
97 return None 122 return None
98 123
  124 + # -----------------------------------------------------------------------
99 def correct_test(self, uid, ans): 125 def correct_test(self, uid, ans):
100 t = self.online[uid]['test'] 126 t = self.online[uid]['test']
101 t.update_answers(ans) 127 t.update_answers(ans)
@@ -107,10 +133,25 @@ class App(object): @@ -107,10 +133,25 @@ class App(object):
107 fpath = path.abspath(path.join(t['answers_dir'], fname)) 133 fpath = path.abspath(path.join(t['answers_dir'], fname))
108 t.save_json(fpath) 134 t.save_json(fpath)
109 135
110 - self.db.save_test(t)  
111 - self.db.save_questions(t) 136 + with self.db_session() as s:
  137 + s.add(Test(
  138 + ref=t['ref'],
  139 + grade=t['grade'],
  140 + starttime=str(t['start_time']),
  141 + finishtime=str(t['finish_time']),
  142 + student_id=t['student']['number']))
  143 + s.add_all([Question(
  144 + ref=q['ref'],
  145 + grade=q['grade'],
  146 + starttime='',
  147 + finishtime=str(t['finish_time']),
  148 + student_id=t['student']['number'],
  149 + test_id=t['ref']) for q in t['questions'] if 'grade' in q])
  150 + s.commit()
  151 +
112 return grade 152 return grade
113 153
  154 + # -----------------------------------------------------------------------
114 155
115 # --- helpers (getters) 156 # --- helpers (getters)
116 def get_student_name(self, uid): 157 def get_student_name(self, uid):
@@ -120,51 +161,52 @@ class App(object): @@ -120,51 +161,52 @@ class App(object):
120 def get_test_qtypes(self, uid): 161 def get_test_qtypes(self, uid):
121 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']} 162 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']}
122 def get_student_grades_from_all_tests(self, uid): 163 def get_student_grades_from_all_tests(self, uid):
123 - return self.db.get_student_grades_from_all_tests(uid)  
124 -  
125 - # def get_student_grades_from_test(self, uid, testid):  
126 - # return self.db.get_student_grades_from_test(uid, testid)  
127 -  
128 -  
129 - # def get_online_students(self):  
130 - # # list of ('uid', 'name', 'start_time') sorted by start time  
131 - # return sorted(  
132 - # ((k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'),  
133 - # key=lambda k: k[2] # sort key  
134 - # ) 164 + with self.db_session() as s:
  165 + r = s.query(Test).filter(Student.id == uid).all()
  166 + return [(t.id, t.grade, t.finishtime) for t in r]
135 167
136 def get_online_students(self): 168 def get_online_students(self):
137 - # {'123': '2016-12-02 12:04:12.344243', ...} 169 + # [('uid', 'name', 'starttime')]
138 return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0'] 170 return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k,v in self.online.items() if k != '0']
139 171
140 def get_offline_students(self): 172 def get_offline_students(self):
141 - # list of ('uid', 'name') sorted by number  
142 - return sorted((s[:2] for s in self.db.get_all_students() if s[0] not in self.online), key=lambda k: k[0]) 173 + # list of ('uid', 'name') sorted by uid
  174 + return [u[:2] for u in self.get_all_students() if u[0] not in self.online]
143 175
144 def get_all_students(self): 176 def get_all_students(self):
145 - # list of ('uid', 'name') sorted by number  
146 - return sorted((s[:2] for s in self.db.get_all_students() if s[0] != '0'), key=lambda k: k[0]) 177 + # list of all ('uid', 'name', 'password') sorted by uid
  178 + with self.db_session() as s:
  179 + r = s.query(Student).all()
  180 + return sorted(((u.id, u.name, u.password) for u in r if u.id != '0'), key=lambda k: k[0])
  181 +
  182 + def get_student_grades_from_test(self, uid, testid):
  183 + with self.db_session() as s:
  184 + r = s.query(Test).filter(Test.student_id==uid and Test.id==testid).all()
  185 + return [(u.grade, u.finishtime) for u in r]
147 186
148 def get_students_state(self): 187 def get_students_state(self):
149 - # {'123': {'name': 'John', 'start_time':'', 'grades':[10.2, 13.1], ...}}  
150 - d = {}  
151 - for s in self.db.get_all_students():  
152 - uid, name, pw = s  
153 - if uid == '0':  
154 - continue  
155 - d[uid] = {'name': name}  
156 - d[uid]['allowed'] = uid in self.allowed  
157 - d[uid]['online'] = uid in self.online  
158 - d[uid]['start_time'] = self.online.get(uid, {}).get('test', {}).get('start_time','')  
159 - d[uid]['password_defined'] = pw != ''  
160 - d[uid]['grades'] = self.db.get_student_grades_from_test(uid, self.testfactory['ref'])  
161 - d[uid]['ip_address'] = self.online.get(uid, {}).get('student', {}).get('ip_address','')  
162 - d[uid]['user_agent'] = self.online.get(uid, {}).get('student', {}).get('user_agent','')  
163 - return d  
164 -  
165 - # def get_this_students_grades(self):  
166 - # # list of ('uid', 'name') sorted by number  
167 - # return self.db.get_students_grades(self.testfactory['ref']) 188 + # [{
  189 + # 'uid' : '12345'
  190 + # 'name' : 'John Smith',
  191 + # 'start_time': '',
  192 + # 'grades' : [10.2, 13.1],
  193 + # ...
  194 + # }]
  195 + l = []
  196 + for u in self.get_all_students():
  197 + uid, name, pw = u
  198 + l.append({
  199 + 'uid': uid,
  200 + 'name': name,
  201 + 'allowed': uid in self.allowed,
  202 + 'online': uid in self.online,
  203 + 'start_time': self.online.get(uid, {}).get('test', {}).get('start_time',''),
  204 + 'password_defined': pw != '',
  205 + 'grades': self.get_student_grades_from_test(uid, self.testfactory['ref']),
  206 + 'ip_address': self.online.get(uid, {}).get('student', {}).get('ip_address',''),
  207 + 'user_agent': self.online.get(uid, {}).get('student', {}).get('user_agent','')
  208 + })
  209 + return l
168 210
169 def get_allowed_students(self): 211 def get_allowed_students(self):
170 # set of 'uid' allowed to login 212 # set of 'uid' allowed to login
@@ -180,7 +222,9 @@ class App(object): @@ -180,7 +222,9 @@ class App(object):
180 logger.info('Student {}: denied to login'.format(uid)) 222 logger.info('Student {}: denied to login'.format(uid))
181 223
182 def reset_password(self, uid): 224 def reset_password(self, uid):
183 - self.db.update_password(uid, pw='') 225 + with self.db_session() as s:
  226 + u = s.query(Student).filter(Student.id == uid).update({'password': ''})
  227 + s.commit()
184 logger.info('Student {}: password reset to ""'.format(uid)) 228 logger.info('Student {}: password reset to ""'.format(uid))
185 229
186 def set_user_agent(self, uid, user_agent=''): 230 def set_user_agent(self, uid, user_agent=''):
config/logger-debug.yaml 0 → 100644
@@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
  1 +
  2 +version: 1
  3 +
  4 +formatters:
  5 + void:
  6 + format: ''
  7 + standard:
  8 + format: '%(asctime)s | %(levelname)-8s | %(name)-14s | %(message)s'
  9 +
  10 +handlers:
  11 + default:
  12 + level: 'INFO'
  13 + class: 'logging.StreamHandler'
  14 + formatter: 'standard'
  15 + stream: 'ext://sys.stdout'
  16 +
  17 + cherrypy_console:
  18 + level: 'INFO'
  19 + class: 'logging.StreamHandler'
  20 + formatter: 'standard'
  21 + stream: 'ext://sys.stdout'
  22 +
  23 + cherrypy_access:
  24 + level: 'INFO'
  25 + class: 'logging.handlers.RotatingFileHandler'
  26 + formatter: 'void'
  27 + filename: 'logs/access.log'
  28 + maxBytes: 10485760
  29 + backupCount: 20
  30 + encoding: 'utf8'
  31 +
  32 + cherrypy_error:
  33 + level: 'INFO'
  34 + class: 'logging.handlers.RotatingFileHandler'
  35 + formatter: 'void'
  36 + filename: 'logs/errors.log'
  37 + maxBytes: 10485760
  38 + backupCount: 20
  39 + encoding: 'utf8'
  40 +
  41 +loggers:
  42 + '':
  43 + handlers: ['default']
  44 + level: 'DEBUG'
  45 +
  46 + 'cherrypy.access':
  47 + handlers: ['cherrypy_access']
  48 + level: 'DEBUG'
  49 + propagate: False
  50 +
  51 + 'cherrypy.error':
  52 + handlers: ['cherrypy_console', 'cherrypy_error']
  53 + level: 'DEBUG'
  54 + propagate: False
  55 +
  56 + 'app':
  57 + handlers: ['default']
  58 + level: 'DEBUG'
  59 + propagate: False
  60 +
  61 + 'test':
  62 + handlers: ['default']
  63 + level: 'DEBUG'
  64 + propagate: False
  65 +
  66 + 'questions':
  67 + handlers: ['default']
  68 + level: 'DEBUG'
  69 + propagate: False
  70 +
  71 + 'tools':
  72 + handlers: ['default']
  73 + level: 'DEBUG'
  74 + propagate: False
  75 +
config/logger.yaml 0 → 100644
@@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
  1 +
  2 +version: 1
  3 +
  4 +formatters:
  5 + void:
  6 + format: ''
  7 + standard:
  8 + format: '%(asctime)s | %(levelname)-8s | %(name)-14s | %(message)s'
  9 +
  10 +handlers:
  11 + default:
  12 + level: 'INFO'
  13 + class: 'logging.StreamHandler'
  14 + formatter: 'standard'
  15 + stream: 'ext://sys.stdout'
  16 +
  17 + cherrypy_console:
  18 + level: 'INFO'
  19 + class: 'logging.StreamHandler'
  20 + formatter: 'standard'
  21 + stream: 'ext://sys.stdout'
  22 +
  23 + cherrypy_access:
  24 + level: 'INFO'
  25 + class: 'logging.handlers.RotatingFileHandler'
  26 + formatter: 'void'
  27 + filename: 'logs/access.log'
  28 + maxBytes: 10485760
  29 + backupCount: 20
  30 + encoding: 'utf8'
  31 +
  32 + cherrypy_error:
  33 + level: 'INFO'
  34 + class: 'logging.handlers.RotatingFileHandler'
  35 + formatter: 'void'
  36 + filename: 'logs/errors.log'
  37 + maxBytes: 10485760
  38 + backupCount: 20
  39 + encoding: 'utf8'
  40 +
  41 +loggers:
  42 + '':
  43 + handlers: ['default']
  44 + level: 'INFO'
  45 +
  46 + 'cherrypy.access':
  47 + handlers: ['cherrypy_access']
  48 + level: 'INFO'
  49 + propagate: False
  50 +
  51 + 'cherrypy.error':
  52 + handlers: ['cherrypy_console', 'cherrypy_error']
  53 + level: 'INFO'
  54 + propagate: False
  55 +
  56 + 'app':
  57 + handlers: ['default']
  58 + level: 'INFO'
  59 + propagate: False
  60 +
  61 + 'test':
  62 + handlers: ['default']
  63 + level: 'INFO'
  64 + propagate: False
  65 +
  66 + 'questions':
  67 + handlers: ['default']
  68 + level: 'INFO'
  69 + propagate: False
  70 +
  71 + 'tools':
  72 + handlers: ['default']
  73 + level: 'INFO'
  74 + propagate: False
  75 +
database.py
@@ -1,181 +0,0 @@ @@ -1,181 +0,0 @@
1 -  
2 -import logging  
3 -from sqlalchemy import create_engine  
4 -from sqlalchemy.orm import sessionmaker, scoped_session  
5 -from orm import Base, Student, Test, Question  
6 -  
7 -logger = logging.getLogger(__name__)  
8 -  
9 -#----------------------------------------------------------------------------  
10 -class Database(object):  
11 - def __init__(self, db):  
12 - self.db = db # sqlite3 filename  
13 -  
14 - engine = create_engine('sqlite:///{}'.format(db), echo=False)  
15 - Base.metadata.create_all(engine) # Criate schema if needed  
16 - self.Session = scoped_session(sessionmaker(bind=engine))  
17 -  
18 - #-------------------------------------------------------------------------  
19 - def get_count_students(self):  
20 - s = self.Session()  
21 - return s.query(Student).filter(Student.id != '0').count()  
22 -  
23 - # with sqlite3.connect(self.db) as c:  
24 - # sql = 'SELECT COUNT(*) FROM students'  
25 - # return c.execute(sql).fetchone()[0]  
26 -  
27 - #-------------------------------------------------------------------------  
28 - def update_password(self, uid, pw=''):  
29 - s = self.Session()  
30 - try:  
31 - u = s.query(Student).filter(Student.id == uid).one()  
32 - except:  
33 - pass  
34 - else:  
35 - u.password = pw  
36 - s.commit()  
37 -  
38 - # saves pw as is (should be already hashed)  
39 - # with sqlite3.connect(self.db) as c:  
40 - # sql = 'UPDATE students SET password=? WHERE id=?'  
41 - # c.execute(sql, (pw, uid))  
42 -  
43 - #-------------------------------------------------------------------------  
44 - def get_student(self, uid):  
45 - s = self.Session()  
46 - r = s.query(Student).filter(Student.id == uid).one_or_none()  
47 - return r.name, r.password  
48 -  
49 - # with sqlite3.connect(self.db) as c:  
50 - # sql = 'SELECT name,password FROM students WHERE id=?'  
51 - # try:  
52 - # name, pw = c.execute(sql, [uid]).fetchone()  
53 - # except:  
54 - # return None  
55 - # else:  
56 - # return (name, pw)  
57 -  
58 - #-------------------------------------------------------------------------  
59 - def get_all_students(self):  
60 - s = self.Session()  
61 - r = s.query(Student).all()  
62 - return [(x.id, x.name, x.password) for x in r]  
63 -  
64 - # with sqlite3.connect(self.db) as c:  
65 - # sql = 'SELECT id,name,password FROM students ORDER BY id ASC'  
66 - # students = c.execute(sql).fetchall()  
67 - # return students  
68 -  
69 - # get all results for a particular test. If a student has submited more than  
70 - # one test, returns the highest grade FIXME not tested, not used  
71 - # def get_students_grades(self, testid):  
72 - # with sqlite3.connect(self.db) as c:  
73 - # grades = c.execute('SELECT student_id,MAX(grade) FROM tests WHERE test_id==?', [testid])  
74 - # return grades.fetchall()  
75 -  
76 - #-------------------------------------------------------------------------  
77 - # get results from previous tests of a student  
78 - def get_student_grades_from_all_tests(self, uid):  
79 - s = self.Session()  
80 - r = s.query(Test).filter(Student.id == uid).all()  
81 - return [(x.id, x.grade, x.finishtime) for x in r]  
82 -  
83 - # with sqlite3.connect(self.db) as c:  
84 - # grades = c.execute('SELECT id,grade,finishtime FROM tests WHERE student_id==?', [uid])  
85 - # return grades.fetchall()  
86 -  
87 - #-------------------------------------------------------------------------  
88 - def get_student_grades_from_test(self, uid, testid):  
89 - s = self.Session()  
90 - r = s.query(Test).filter(Test.student_id==uid and Test.id==testid).all()  
91 - return [(x.grade, x.finishtime) for x in r]  
92 -  
93 - # with sqlite3.connect(self.db) as c:  
94 - # grades = c.execute('SELECT grade,finishtime FROM tests WHERE student_id==? and id==?', [uid, testid])  
95 - # return grades.fetchall()  
96 -  
97 - #-------------------------------------------------------------------------  
98 - def save_test(self, test):  
99 - t = Test(  
100 - ref=test['ref'],  
101 - grade=test['grade'],  
102 - starttime=str(test['start_time']),  
103 - finishtime=str(test['finish_time']),  
104 - student_id=test['student']['number']  
105 - )  
106 - s = self.Session()  
107 - s.add(t)  
108 - s.commit()  
109 -  
110 - # with sqlite3.connect(self.db) as c:  
111 - # # save final grade of the test  
112 - # sql = 'INSERT INTO tests VALUES (?,?,?,?,?)'  
113 - # test = (t['ref'], t['student']['number'], t['grade'], str(t['start_time']), str(t['finish_time']))  
114 - # c.execute(sql, test)  
115 -  
116 - #-------------------------------------------------------------------------  
117 - def save_questions(self, test):  
118 - s = self.Session()  
119 - questions = [Question(  
120 - ref=q['ref'],  
121 - grade=q['grade'],  
122 - starttime='',  
123 - finishtime=str(test['finish_time']),  
124 - student_id=test['student']['number'],  
125 - test_id=test['ref']) for q in test['questions'] if 'grade' in q]  
126 - s.add_all(questions)  
127 - s.commit()  
128 -  
129 - # with sqlite3.connect(self.db) as c:  
130 - # # save grades of all the questions (omits questions without grade)  
131 - # sql = 'INSERT INTO questions VALUES (?,?,?,?,?)'  
132 - # questions = [(t['ref'], q['ref'], t['student']['number'], q['grade'], str(t['finish_time'])) for q in t['questions'] if 'grade' in q]  
133 - # c.executemany(sql, questions)  
134 -  
135 -  
136 -  
137 -  
138 - # def insert_student(self, number, name, password=''): # FIXME testar...  
139 - # with sqlite3.connect(self.db) as c:  
140 - # if password != '':  
141 - # password = sha256(password.encode('utf-8')).hexdigest() # FIXME bcrypt  
142 - # cmd = 'INSERT INTO students VALUES (?, ?, ?);'  
143 - # c.execute(cmd, number, name, password)  
144 -  
145 -  
146 -  
147 -  
148 - # # return list of students and their results for a given test  
149 - # def get_test_grades(self, test_id):  
150 - # with sqlite3.connect(self.db) as c:  
151 - # # with all tests done by each student:  
152 - # # cmd = 'SELECT student_id,name,grade FROM students INNER JOIN tests ON students.number=tests.student_id WHERE test_id==? ORDER BY grade DESC;'  
153 -  
154 - # # only the best result for each student  
155 - # cmd = '''  
156 - # SELECT student_id, name, MAX(grade), finish_time  
157 - # FROM students INNER JOIN tests  
158 - # ON students.number=tests.student_id  
159 - # WHERE test_id==? AND student_id!=0  
160 - # GROUP BY student_id  
161 - # ORDER BY grade DESC, finish_time DESC;'''  
162 - # return c.execute(cmd, [test_id]).fetchall()  
163 -  
164 - # # return list of students and their results for a given test  
165 - # def test_grades2(self, test_id):  
166 - # with sqlite3.connect(self.db) as c:  
167 - # # with all tests done by each student:  
168 - # # cmd = 'SELECT student_id,name,grade FROM students INNER JOIN tests ON students.number=tests.student_id WHERE test_id==? ORDER BY grade DESC;'  
169 -  
170 - # # only the best result for each student  
171 - # cmd = '''  
172 - # SELECT student_id, name, grade, start_time, finish_time  
173 - # FROM students INNER JOIN tests  
174 - # ON students.number=tests.student_id  
175 - # WHERE test_id==?  
176 - # ORDER BY finish_time ASC;'''  
177 - # return c.execute(cmd, [test_id]).fetchall()  
178 -  
179 -  
180 - # the following methods update de database data  
181 -  
@@ -9,7 +9,7 @@ import sys @@ -9,7 +9,7 @@ import sys
9 from sqlalchemy import create_engine 9 from sqlalchemy import create_engine
10 from sqlalchemy.orm import sessionmaker 10 from sqlalchemy.orm import sessionmaker
11 11
12 -from orm import Base, Student, Test, Question 12 +from models import Base, Student, Test, Question
13 13
14 # SIIUE names have alien strings like "(TE)" and are sometimes capitalized 14 # SIIUE names have alien strings like "(TE)" and are sometimes capitalized
15 # We remove them so that students dont keep asking what it means 15 # We remove them so that students dont keep asking what it means
@@ -55,8 +55,14 @@ try: @@ -55,8 +55,14 @@ try:
55 session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader]) 55 session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader])
56 56
57 session.commit() 57 session.commit()
  58 +
58 except Exception: 59 except Exception:
59 session.rollback() 60 session.rollback()
60 print('Erro: Dados já existentes na base de dados?') 61 print('Erro: Dados já existentes na base de dados?')
61 sys.exit(1) 62 sys.exit(1)
  63 +
  64 +else:
  65 + n = session.query(Student).count()
  66 + print('Base de dados inicializada com {} utilizadores.'.format(n))
  67 +
62 # --- end session --- 68 # --- end session ---
initdb_from_csv.py
@@ -1,95 +0,0 @@ @@ -1,95 +0,0 @@
1 -#!/usr/bin/env python3  
2 -# -*- coding: utf-8 -*-  
3 -  
4 -import sys  
5 -import sqlite3  
6 -import csv  
7 -import argparse  
8 -import bcrypt  
9 -import os  
10 -import string  
11 -import re  
12 -  
13 -# SIIUE names have alien strings like "(TE)" and are sometimes capitalized  
14 -# We remove them so that students dont keep asking what it means  
15 -def fixname(s):  
16 - return string.capwords(re.sub('\(.*\)', '', s).strip())  
17 -  
18 -def genstudent(reader, pw=''):  
19 - for i, r in enumerate(reader):  
20 - num = r['N.º']  
21 - name = fixname(r['Nome'])  
22 - yield (r['N.º'], fixname(r['Nome']), '')  
23 - print('{} students inserted.'.format(i+1))  
24 -  
25 -# ---- DATABASE SCHEMA ----  
26 -sql_cmd = '''PRAGMA foreign_keys = ON;  
27 - CREATE TABLE students (  
28 - number TEXT PRIMARY KEY,  
29 - name TEXT,  
30 - password TEXT  
31 - );  
32 - CREATE TABLE tests (  
33 - test_id TEXT NOT NULL,  
34 - student_id TEXT NOT NULL,  
35 - grade REAL,  
36 - start_time TEXT,  
37 - finish_time TEXT,  
38 - FOREIGN KEY(student_id) REFERENCES students(number)  
39 - );  
40 - CREATE TABLE questions (  
41 - test_id TEXT NOT NULL,  
42 - question_id TEXT NOT NULL,  
43 - student_id TEXT NOT NULL,  
44 - grade REAL,  
45 - time TEXT,  
46 - FOREIGN KEY(student_id) REFERENCES students(number)  
47 - );'''  
48 -  
49 -# --------- Parse command line options -----------  
50 -argparser = argparse.ArgumentParser(description='Create new database from a CSV file (SIIUE format)')  
51 -argparser.add_argument('--db', default='students.db', type=str, help='database filename')  
52 -argparser.add_argument('csvfile', nargs='?', type=str, default='', help='CSV filename')  
53 -args = argparser.parse_args()  
54 -  
55 -db_exists = os.path.exists(args.db)  
56 -  
57 -with sqlite3.connect(args.db) as c:  
58 - # use existing or create new database schema  
59 - if db_exists:  
60 - print('-> Using previous database "{}"...'.format(args.db))  
61 - else:  
62 - print('-> Creating new database "{}"...'.format(args.db))  
63 - c.executescript(sql_cmd)  
64 -  
65 - # get students  
66 - if args.csvfile:  
67 - csvfile = open(args.csvfile, encoding='iso-8859-1')  
68 - print('-> Using students from CSV file "{}"...'.format(args.csvfile))  
69 - students = genstudent(csv.DictReader(csvfile, delimiter=';', quotechar='"'))  
70 - else:  
71 - print('-> Creating fake students numbered 1 to 5...'.format(args.csvfile))  
72 - students = [  
73 - ('1', 'Student1', ''),  
74 - ('2', 'Student2', ''),  
75 - ('3', 'Student3', ''),  
76 - ('4', 'Student4', ''),  
77 - ('5', 'Student5', '')  
78 - ]  
79 -  
80 - # insert students into database  
81 - print('-> Inserting students into database... ')  
82 - try:  
83 - c.executemany('INSERT INTO students VALUES (?,?,?)', students)  
84 - except sqlite3.IntegrityError:  
85 - print('** ERROR ** Students already exist. Aborted!')  
86 - sys.exit(1)  
87 -  
88 - # insert professor into database  
89 - print('-> Inserting professor (id=0)...')  
90 - try:  
91 - c.execute('INSERT INTO students VALUES (?,?,?)', ('0', 'Professor', ''))  
92 - except sqlite3.IntegrityError:  
93 - print('** WARNING ** Professor already exists.')  
94 -  
95 -print('Done.')  
models.py 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +
  2 +
  3 +from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime
  4 +from sqlalchemy.ext.declarative import declarative_base
  5 +from sqlalchemy.orm import relationship
  6 +
  7 +
  8 +# ===========================================================================
  9 +# Declare ORM
  10 +Base = declarative_base()
  11 +
  12 +# ---------------------------------------------------------------------------
  13 +class Student(Base):
  14 + __tablename__ = 'students'
  15 + id = Column(String, primary_key=True)
  16 + name = Column(String)
  17 + password = Column(String)
  18 +
  19 + # ---
  20 + tests = relationship('Test', back_populates='student')
  21 + questions = relationship('Question', back_populates='student')
  22 +
  23 + # def __repr__(self):
  24 + # return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password)
  25 +
  26 +
  27 +# ---------------------------------------------------------------------------
  28 +class Test(Base):
  29 + __tablename__ = 'tests'
  30 + id = Column(Integer, primary_key=True) # auto_increment
  31 + ref = Column(String)
  32 + grade = Column(Float)
  33 + starttime = Column(String)
  34 + finishtime = Column(String)
  35 + student_id = Column(String, ForeignKey('students.id'))
  36 +
  37 + # ---
  38 + student = relationship('Student', back_populates='tests')
  39 + questions = relationship('Question', back_populates='test')
  40 +
  41 + # def __repr__(self):
  42 + # return 'Test:\n id: "{0}"\n ref="{1}"\n grade="{2}"\n starttime="{3}"\n finishtime="{4}"\n student_id="{5}"'.format(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id)
  43 +
  44 +
  45 +# ---------------------------------------------------------------------------
  46 +class Question(Base):
  47 + __tablename__ = 'questions'
  48 + id = Column(Integer, primary_key=True) # auto_increment
  49 + ref = Column(String)
  50 + grade = Column(Float)
  51 + starttime = Column(String)
  52 + finishtime = Column(String)
  53 + student_id = Column(String, ForeignKey('students.id'))
  54 + test_id = Column(String, ForeignKey('tests.id'))
  55 +
  56 + # ---
  57 + student = relationship('Student', back_populates='questions')
  58 + test = relationship('Test', back_populates='questions')
  59 +
  60 +# def __repr__(self):
  61 +# return '''
  62 +# Question:
  63 +# id: "{0}"
  64 +# ref: "{1}"
  65 +# grade: "{2}"
  66 +# starttime: "{3}"
  67 +# finishtime: "{4}"
  68 +# student_id: "{5}"
  69 +# test_id: "{6}"
  70 +# '''.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id)
  71 +
  72 +
  73 +# ---------------------------------------------------------------------------
@@ -1,73 +0,0 @@ @@ -1,73 +0,0 @@
1 -  
2 -  
3 -from sqlalchemy import Table, Column, ForeignKey, Integer, Float, String, DateTime  
4 -from sqlalchemy.ext.declarative import declarative_base  
5 -from sqlalchemy.orm import relationship  
6 -  
7 -  
8 -# ===========================================================================  
9 -# Declare ORM  
10 -Base = declarative_base()  
11 -  
12 -# ---------------------------------------------------------------------------  
13 -class Student(Base):  
14 - __tablename__ = 'students'  
15 - id = Column(String, primary_key=True)  
16 - name = Column(String)  
17 - password = Column(String)  
18 -  
19 - # ---  
20 - tests = relationship('Test', back_populates='student')  
21 - questions = relationship('Question', back_populates='student')  
22 -  
23 - # def __repr__(self):  
24 - # return 'Student:\n id: "{0}"\n name: "{1}"\n password: "{2}"'.format(self.id, self.name, self.password)  
25 -  
26 -  
27 -# ---------------------------------------------------------------------------  
28 -class Test(Base):  
29 - __tablename__ = 'tests'  
30 - id = Column(Integer, primary_key=True) # auto_increment  
31 - ref = Column(String)  
32 - grade = Column(Float)  
33 - starttime = Column(String)  
34 - finishtime = Column(String)  
35 - student_id = Column(String, ForeignKey('students.id'))  
36 -  
37 - # ---  
38 - student = relationship('Student', back_populates='tests')  
39 - questions = relationship('Question', back_populates='test')  
40 -  
41 - # def __repr__(self):  
42 - # return 'Test:\n id: "{0}"\n ref="{1}"\n grade="{2}"\n starttime="{3}"\n finishtime="{4}"\n student_id="{5}"'.format(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id)  
43 -  
44 -  
45 -# ---------------------------------------------------------------------------  
46 -class Question(Base):  
47 - __tablename__ = 'questions'  
48 - id = Column(Integer, primary_key=True) # auto_increment  
49 - ref = Column(String)  
50 - grade = Column(Float)  
51 - starttime = Column(String)  
52 - finishtime = Column(String)  
53 - student_id = Column(String, ForeignKey('students.id'))  
54 - test_id = Column(String, ForeignKey('tests.id'))  
55 -  
56 - # ---  
57 - student = relationship('Student', back_populates='questions')  
58 - test = relationship('Test', back_populates='questions')  
59 -  
60 -# def __repr__(self):  
61 -# return '''  
62 -# Question:  
63 -# id: "{0}"  
64 -# ref: "{1}"  
65 -# grade: "{2}"  
66 -# starttime: "{3}"  
67 -# finishtime: "{4}"  
68 -# student_id: "{5}"  
69 -# test_id: "{6}"  
70 -# '''.fotmat(self.id, self.ref, self.grade, self.starttime, self.finishtime, self.student_id, self.test_id)  
71 -  
72 -  
73 -# ---------------------------------------------------------------------------  
@@ -83,7 +83,7 @@ class AdminWebService(object): @@ -83,7 +83,7 @@ class AdminWebService(object):
83 @cherrypy.tools.accept(media='application/json') # FIXME 83 @cherrypy.tools.accept(media='application/json') # FIXME
84 def GET(self): 84 def GET(self):
85 data = { 85 data = {
86 - 'students': list(self.app.get_students_state().items()), 86 + 'students': self.app.get_students_state(),
87 'test': self.app.testfactory 87 'test': self.app.testfactory
88 } 88 }
89 return json.dumps(data, default=str) 89 return json.dumps(data, default=str)
@@ -246,7 +246,7 @@ if __name__ == '__main__': @@ -246,7 +246,7 @@ if __name__ == '__main__':
246 try: 246 try:
247 app = App(filename, vars(arg)) 247 app = App(filename, vars(arg))
248 except Exception as e: 248 except Exception as e:
249 - logging.critical('Cannot start application.') 249 + logging.critical('Can\'t start application.')
250 raise e # FIXME just for testing 250 raise e # FIXME just for testing
251 sys.exit(1) 251 sys.exit(1)
252 252
static/js/admin.js
@@ -28,6 +28,7 @@ $(document).ready(function() { @@ -28,6 +28,7 @@ $(document).ready(function() {
28 ); 28 );
29 } 29 }
30 30
  31 + // ----------------------------------------------------------------------
31 // checkbox handler to allow/deny students individually 32 // checkbox handler to allow/deny students individually
32 function autorizeStudent(e) { 33 function autorizeStudent(e) {
33 $.ajax({ 34 $.ajax({
@@ -41,12 +42,14 @@ $(document).ready(function() { @@ -41,12 +42,14 @@ $(document).ready(function() {
41 $(this).parent().parent().removeClass("active"); 42 $(this).parent().parent().removeClass("active");
42 } 43 }
43 44
  45 + // ----------------------------------------------------------------------
44 function populateOnlineTable(students) { 46 function populateOnlineTable(students) {
45 - var active = [];  
46 var rows = ""; 47 var rows = "";
  48 + // make list of online students
  49 + var active = [];
47 $.each(students, function(i, r) { 50 $.each(students, function(i, r) {
48 - if (r[1]['start_time'] != '') {  
49 - active.push([r[0], r[1]['name'], r[1]['start_time'], r[1]['ip_address'], r[1]['user_agent']]); 51 + if (r['start_time'] != '') {
  52 + active.push([r['uid'], r['name'], r['start_time'], r['ip_address'], r['user_agent']]);
50 } 53 }
51 }); 54 });
52 // sort by start time 55 // sort by start time
@@ -65,6 +68,7 @@ $(document).ready(function() { @@ -65,6 +68,7 @@ $(document).ready(function() {
65 $("#online-header").html(n + " Activo(s)"); 68 $("#online-header").html(n + " Activo(s)");
66 } 69 }
67 70
  71 + // ----------------------------------------------------------------------
68 function generate_grade_bar(grade) { 72 function generate_grade_bar(grade) {
69 var barcolor; 73 var barcolor;
70 if (grade < 10) { 74 if (grade < 10) {
@@ -81,13 +85,13 @@ $(document).ready(function() { @@ -81,13 +85,13 @@ $(document).ready(function() {
81 return bar 85 return bar
82 } 86 }
83 87
  88 + // ----------------------------------------------------------------------
84 function populateStudentsTable(students) { 89 function populateStudentsTable(students) {
85 - $("#students-header").html(students.length + " Alunos") 90 + var n = students.length;
  91 + $("#students-header").html(n + " Alunos")
86 var rows = ""; 92 var rows = "";
87 - students.sort(function(a,b){return a[0] - b[0]});  
88 - $.each(students, function(i, r) {  
89 - var uid = r[0];  
90 - var d = r[1]; // dictionary 93 + $.each(students, function(i, d) {
  94 + var uid = d['uid'];
91 95
92 if (d['start_time'] != '') // test 96 if (d['start_time'] != '') // test
93 rows += '<tr id="' + uid + '" + class="success">'; 97 rows += '<tr id="' + uid + '" + class="success">';
@@ -119,15 +123,20 @@ $(document).ready(function() { @@ -119,15 +123,20 @@ $(document).ready(function() {
119 $('[data-toggle="tooltip"]').tooltip(); 123 $('[data-toggle="tooltip"]').tooltip();
120 } 124 }
121 125
  126 + // ----------------------------------------------------------------------
122 function populate() { 127 function populate() {
123 $.ajax({ 128 $.ajax({
124 url: "/adminwebservice", 129 url: "/adminwebservice",
125 dataType: "json", 130 dataType: "json",
126 success: function(data) { 131 success: function(data) {
  132 + // show clock on upper left corner
127 var t = new Date(); 133 var t = new Date();
128 $('#currenttime').html(t.getHours() + (t.getMinutes() < 10 ? ':0' : ':') + t.getMinutes()); 134 $('#currenttime').html(t.getHours() + (t.getMinutes() < 10 ? ':0' : ':') + t.getMinutes());
  135 +
  136 + // fill jumbotron data
129 $("#title").html(data['test']['title']); 137 $("#title").html(data['test']['title']);
130 $("#ref").html(data['test']['ref']); 138 $("#ref").html(data['test']['ref']);
  139 + $("#filename").html(data['test']['filename']);
131 $("#database").html(data['test']['database']); 140 $("#database").html(data['test']['database']);
132 if (data['test']['save_answers']) { 141 if (data['test']['save_answers']) {
133 $("#answers_dir").html(data['test']['answers_dir']); 142 $("#answers_dir").html(data['test']['answers_dir']);
@@ -135,8 +144,8 @@ $(document).ready(function() { @@ -135,8 +144,8 @@ $(document).ready(function() {
135 else { 144 else {
136 $("#answers_dir").html('--- not being saved ---'); 145 $("#answers_dir").html('--- not being saved ---');
137 } 146 }
138 - $("#filename").html(data['test']['filename']);  
139 147
  148 + // fill online and student tables
140 populateOnlineTable(data["students"]); 149 populateOnlineTable(data["students"]);
141 populateStudentsTable(data["students"]) 150 populateStudentsTable(data["students"])
142 151
@@ -3,14 +3,13 @@ from os import path, listdir @@ -3,14 +3,13 @@ from os import path, listdir
3 import sys, fnmatch 3 import sys, fnmatch
4 import random 4 import random
5 from datetime import datetime 5 from datetime import datetime
6 -import sqlite3  
7 import logging 6 import logging
8 7
9 # Logger configuration 8 # Logger configuration
10 logger = logging.getLogger(__name__) 9 logger = logging.getLogger(__name__)
11 10
12 try: 11 try:
13 - import yaml 12 + # import yaml
14 import json 13 import json
15 except ImportError: 14 except ImportError:
16 logger.critical('Python package missing. See README.md for instructions.') 15 logger.critical('Python package missing. See README.md for instructions.')
@@ -18,7 +17,6 @@ except ImportError: @@ -18,7 +17,6 @@ except ImportError:
18 17
19 # my code 18 # my code
20 import questions 19 import questions
21 -import database  
22 from tools import load_yaml 20 from tools import load_yaml
23 21
24 # =========================================================================== 22 # ===========================================================================
@@ -100,11 +98,11 @@ class TestFactory(dict): @@ -100,11 +98,11 @@ class TestFactory(dict):
100 self['answers_dir'] = path.abspath(path.expanduser(self['answers_dir'])) 98 self['answers_dir'] = path.abspath(path.expanduser(self['answers_dir']))
101 99
102 if not path.isfile(self['database']): 100 if not path.isfile(self['database']):
103 - logger.critical('Cannot find database "{}"'.format(self['database'])) 101 + logger.critical('Can\'t find database "{}"'.format(self['database']))
104 raise TestFactoryException() 102 raise TestFactoryException()
105 103
106 if not path.isdir(self['questions_dir']): 104 if not path.isdir(self['questions_dir']):
107 - logger.critical('Cannot find questions directory "{}"'.format(self['questions_dir'])) 105 + logger.critical('Can\'t find questions directory "{}"'.format(self['questions_dir']))
108 raise TestFactoryException() 106 raise TestFactoryException()
109 107
110 # make sure we have a list of question files. 108 # make sure we have a list of question files.