Commit fac28ea70d8bc4e99b371dc073b7e3c93a990b6b
1 parent
01b86197
Exists in
master
and in
1 other branch
- fixed threading issue with sqlalchemy.
- removed all sqlite3 code. - fixed sorting students in admin. - added logger configuration files.
Showing
12 changed files
with
374 additions
and
443 deletions
Show diff stats
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=''): | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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.') |
... | ... | @@ -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 | +# --------------------------------------------------------------------------- | ... | ... |
orm.py
... | ... | @@ -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. | ... | ... |