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.
BUGS.md
1 1  
2 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 4 - usar thread.Lock para aceder a variaveis de estado?
6 5 - permitir adicionar imagens nas perguntas.
7 6 - debug mode: log levels not working
... ... @@ -23,6 +22,7 @@
23 22  
24 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 26 - sqlalchemy queixa-se de threads.
27 27 - SQLAlchemy em vez da classe database.
28 28 - replace sys.exit calls
... ...
app.py
1 1  
2 2  
3   -import logging
4 3 from os import path
5   -import sqlite3
  4 +import logging
6 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 11 import test
9   -import database
  12 +import threading
10 13  
11 14 logger = logging.getLogger(__name__)
12 15  
... ... @@ -18,64 +21,85 @@ class App(object):
18 21 def __init__(self, filename, conf):
19 22 # online = {
20 23 # uid1: {
21   - # 'student': {'number': 123, 'name': john},
22   - # 'test': ...
  24 + # 'student': {'number': 123, 'name': john, ...},
  25 + # 'test': {...}
23 26 # }
24 27 # uid2: {...}
25 28 # }
26 29 logger.info('============= Running perguntations =============')
27 30 self.online = dict() # {uid: {'student':{}}}
28 31 self.allowed = set([]) # '0' is hardcoded to allowed elsewhere
  32 +
29 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 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 45 raise e
36 46 else:
37 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 61 def exit(self):
  62 + # FIXME what if there are online students?
41 63 logger.critical('----------- !!! Server terminated !!! -----------')
42 64  
43   -
  65 + # -----------------------------------------------------------------------
44 66 def login(self, uid, try_pw):
45 67 if uid not in self.allowed and uid != '0':
46 68 # not allowed
47 69 logger.warning('Student {}: not allowed to login.'.format(uid))
48 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 100 return True
77 101  
78   -
  102 + # -----------------------------------------------------------------------
79 103 def logout(self, uid):
80 104 if uid not in self.online:
81 105 # this should never happen
... ... @@ -83,9 +107,10 @@ class App(object):
83 107 return False
84 108 else:
85 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 111 return True
88 112  
  113 + # -----------------------------------------------------------------------
89 114 def generate_test(self, uid):
90 115 if uid in self.online:
91 116 logger.info('Student {}: generating new test.'.format(uid))
... ... @@ -96,6 +121,7 @@ class App(object):
96 121 logger.error('Student {}: offline, can''t generate test'.format(uid))
97 122 return None
98 123  
  124 + # -----------------------------------------------------------------------
99 125 def correct_test(self, uid, ans):
100 126 t = self.online[uid]['test']
101 127 t.update_answers(ans)
... ... @@ -107,10 +133,25 @@ class App(object):
107 133 fpath = path.abspath(path.join(t['answers_dir'], fname))
108 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 152 return grade
113 153  
  154 + # -----------------------------------------------------------------------
114 155  
115 156 # --- helpers (getters)
116 157 def get_student_name(self, uid):
... ... @@ -120,51 +161,52 @@ class App(object):
120 161 def get_test_qtypes(self, uid):
121 162 return {q['ref']:q['type'] for q in self.online[uid]['test']['questions']}
122 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 168 def get_online_students(self):
137   - # {'123': '2016-12-02 12:04:12.344243', ...}
  169 + # [('uid', 'name', 'starttime')]
138 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 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 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 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 211 def get_allowed_students(self):
170 212 # set of 'uid' allowed to login
... ... @@ -180,7 +222,9 @@ class App(object):
180 222 logger.info('Student {}: denied to login'.format(uid))
181 223  
182 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 228 logger.info('Student {}: password reset to ""'.format(uid))
185 229  
186 230 def set_user_agent(self, uid, user_agent=''):
... ...
config/logger-debug.yaml 0 → 100644
... ... @@ -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 @@
  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   -
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   -
initdb.py
... ... @@ -9,7 +9,7 @@ import sys
9 9 from sqlalchemy import create_engine
10 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 14 # SIIUE names have alien strings like "(TE)" and are sometimes capitalized
15 15 # We remove them so that students dont keep asking what it means
... ... @@ -55,8 +55,14 @@ try:
55 55 session.add_all([Student(id=r['N.º'], name=fix(r['Nome']), password='') for r in csvreader])
56 56  
57 57 session.commit()
  58 +
58 59 except Exception:
59 60 session.rollback()
60 61 print('Erro: Dados já existentes na base de dados?')
61 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 68 # --- end session ---
... ...
initdb_from_csv.py
... ... @@ -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 @@
  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   -
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   -# ---------------------------------------------------------------------------
serve.py
... ... @@ -83,7 +83,7 @@ class AdminWebService(object):
83 83 @cherrypy.tools.accept(media='application/json') # FIXME
84 84 def GET(self):
85 85 data = {
86   - 'students': list(self.app.get_students_state().items()),
  86 + 'students': self.app.get_students_state(),
87 87 'test': self.app.testfactory
88 88 }
89 89 return json.dumps(data, default=str)
... ... @@ -246,7 +246,7 @@ if __name__ == '__main__':
246 246 try:
247 247 app = App(filename, vars(arg))
248 248 except Exception as e:
249   - logging.critical('Cannot start application.')
  249 + logging.critical('Can\'t start application.')
250 250 raise e # FIXME just for testing
251 251 sys.exit(1)
252 252  
... ...
static/js/admin.js
... ... @@ -28,6 +28,7 @@ $(document).ready(function() {
28 28 );
29 29 }
30 30  
  31 + // ----------------------------------------------------------------------
31 32 // checkbox handler to allow/deny students individually
32 33 function autorizeStudent(e) {
33 34 $.ajax({
... ... @@ -41,12 +42,14 @@ $(document).ready(function() {
41 42 $(this).parent().parent().removeClass("active");
42 43 }
43 44  
  45 + // ----------------------------------------------------------------------
44 46 function populateOnlineTable(students) {
45   - var active = [];
46 47 var rows = "";
  48 + // make list of online students
  49 + var active = [];
47 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 55 // sort by start time
... ... @@ -65,6 +68,7 @@ $(document).ready(function() {
65 68 $("#online-header").html(n + " Activo(s)");
66 69 }
67 70  
  71 + // ----------------------------------------------------------------------
68 72 function generate_grade_bar(grade) {
69 73 var barcolor;
70 74 if (grade < 10) {
... ... @@ -81,13 +85,13 @@ $(document).ready(function() {
81 85 return bar
82 86 }
83 87  
  88 + // ----------------------------------------------------------------------
84 89 function populateStudentsTable(students) {
85   - $("#students-header").html(students.length + " Alunos")
  90 + var n = students.length;
  91 + $("#students-header").html(n + " Alunos")
86 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 96 if (d['start_time'] != '') // test
93 97 rows += '<tr id="' + uid + '" + class="success">';
... ... @@ -119,15 +123,20 @@ $(document).ready(function() {
119 123 $('[data-toggle="tooltip"]').tooltip();
120 124 }
121 125  
  126 + // ----------------------------------------------------------------------
122 127 function populate() {
123 128 $.ajax({
124 129 url: "/adminwebservice",
125 130 dataType: "json",
126 131 success: function(data) {
  132 + // show clock on upper left corner
127 133 var t = new Date();
128 134 $('#currenttime').html(t.getHours() + (t.getMinutes() < 10 ? ':0' : ':') + t.getMinutes());
  135 +
  136 + // fill jumbotron data
129 137 $("#title").html(data['test']['title']);
130 138 $("#ref").html(data['test']['ref']);
  139 + $("#filename").html(data['test']['filename']);
131 140 $("#database").html(data['test']['database']);
132 141 if (data['test']['save_answers']) {
133 142 $("#answers_dir").html(data['test']['answers_dir']);
... ... @@ -135,8 +144,8 @@ $(document).ready(function() {
135 144 else {
136 145 $("#answers_dir").html('--- not being saved ---');
137 146 }
138   - $("#filename").html(data['test']['filename']);
139 147  
  148 + // fill online and student tables
140 149 populateOnlineTable(data["students"]);
141 150 populateStudentsTable(data["students"])
142 151  
... ...
test.py
... ... @@ -3,14 +3,13 @@ from os import path, listdir
3 3 import sys, fnmatch
4 4 import random
5 5 from datetime import datetime
6   -import sqlite3
7 6 import logging
8 7  
9 8 # Logger configuration
10 9 logger = logging.getLogger(__name__)
11 10  
12 11 try:
13   - import yaml
  12 + # import yaml
14 13 import json
15 14 except ImportError:
16 15 logger.critical('Python package missing. See README.md for instructions.')
... ... @@ -18,7 +17,6 @@ except ImportError:
18 17  
19 18 # my code
20 19 import questions
21   -import database
22 20 from tools import load_yaml
23 21  
24 22 # ===========================================================================
... ... @@ -100,11 +98,11 @@ class TestFactory(dict):
100 98 self['answers_dir'] = path.abspath(path.expanduser(self['answers_dir']))
101 99  
102 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 102 raise TestFactoryException()
105 103  
106 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 106 raise TestFactoryException()
109 107  
110 108 # make sure we have a list of question files.
... ...