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 | # 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 |
app.py
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=''): |
@@ -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 | + |
@@ -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 | - |
initdb.py
@@ -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.') |
@@ -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 | +# --------------------------------------------------------------------------- |
orm.py
@@ -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 | -# --------------------------------------------------------------------------- |
serve.py
@@ -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 |
test.py
@@ -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. |