Commit 981734c09fbc38fd4e4e826596724810b0ba4db4
1 parent
7c49db42
Exists in
master
and in
1 other branch
irrelevant, for testing
Showing
4 changed files
with
145 additions
and
114 deletions
Show diff stats
mypy.ini
perguntations/app.py
@@ -6,7 +6,6 @@ Description: Main application logic. | @@ -6,7 +6,6 @@ Description: Main application logic. | ||
6 | 6 | ||
7 | # python standard libraries | 7 | # python standard libraries |
8 | import asyncio | 8 | import asyncio |
9 | -# from contextlib import contextmanager # `with` statement in db sessions | ||
10 | import csv | 9 | import csv |
11 | import io | 10 | import io |
12 | import json | 11 | import json |
@@ -17,8 +16,7 @@ from os import path | @@ -17,8 +16,7 @@ from os import path | ||
17 | import bcrypt | 16 | import bcrypt |
18 | from sqlalchemy import create_engine, select, func | 17 | from sqlalchemy import create_engine, select, func |
19 | from sqlalchemy.orm import Session | 18 | from sqlalchemy.orm import Session |
20 | -from sqlalchemy.exc import NoResultFound | ||
21 | -# from sqlalchemy.orm import sessionmaker | 19 | +# from sqlalchemy.exc import NoResultFound |
22 | 20 | ||
23 | # this project | 21 | # this project |
24 | from perguntations.models import Student, Test, Question | 22 | from perguntations.models import Student, Test, Question |
@@ -113,8 +111,6 @@ class App(): | @@ -113,8 +111,6 @@ class App(): | ||
113 | logger.info('Generating %d tests. May take awhile...', | 111 | logger.info('Generating %d tests. May take awhile...', |
114 | len(self.allowed)) | 112 | len(self.allowed)) |
115 | self._pregenerate_tests(len(self.allowed)) | 113 | self._pregenerate_tests(len(self.allowed)) |
116 | - else: | ||
117 | - logger.info('No tests generated yet.') | ||
118 | 114 | ||
119 | if conf['correct']: | 115 | if conf['correct']: |
120 | self._correct_tests() | 116 | self._correct_tests() |
@@ -139,94 +135,91 @@ class App(): | @@ -139,94 +135,91 @@ class App(): | ||
139 | 135 | ||
140 | logger.info('Database "%s" has %s students.', dbfile, num) | 136 | logger.info('Database "%s" has %s students.', dbfile, num) |
141 | 137 | ||
142 | - # ------------------------------------------------------------------------ | ||
143 | - def _correct_tests(self): | ||
144 | - with Session(self._engine, future=True) as session: | ||
145 | - # Find which tests have to be corrected | ||
146 | - dbtests = session.execute( | ||
147 | - select(Test). | ||
148 | - where(Test.ref == self.testfactory['ref']). | ||
149 | - where(Test.state == "SUBMITTED") | ||
150 | - ).all() | ||
151 | - # dbtests = session.query(Test)\ | ||
152 | - # .filter(Test.ref == self.testfactory['ref'])\ | ||
153 | - # .filter(Test.state == "SUBMITTED")\ | ||
154 | - # .all() | ||
155 | - | ||
156 | - logger.info('Correcting %d tests...', len(dbtests)) | ||
157 | - for dbtest in dbtests: | ||
158 | - try: | ||
159 | - with open(dbtest.filename) as file: | ||
160 | - testdict = json.load(file) | ||
161 | - except FileNotFoundError: | ||
162 | - logger.error('File not found: %s', dbtest.filename) | ||
163 | - continue | ||
164 | - | ||
165 | - # creates a class Test with the methods to correct it | ||
166 | - # the questions are still dictionaries, so we have to call | ||
167 | - # question_from() to produce Question() instances that can be | ||
168 | - # corrected. Finally the test can be corrected. | ||
169 | - test = perguntations.test.Test(testdict) | ||
170 | - test['questions'] = [question_from(q) for q in test['questions']] | ||
171 | - test.correct() | ||
172 | - logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) | ||
173 | - | ||
174 | - # save JSON file (overwriting the old one) | ||
175 | - uid = test['student']['number'] | ||
176 | - ref = test['ref'] | ||
177 | - finish_time = test['finish_time'] | ||
178 | - answers_dir = test['answers_dir'] | ||
179 | - fname = f'{uid}--{ref}--{finish_time}.json' | ||
180 | - fpath = path.join(answers_dir, fname) | ||
181 | - test.save_json(fpath) | ||
182 | - logger.info('%s saved JSON file.', uid) | ||
183 | - | ||
184 | - # update database | ||
185 | - dbtest.grade = test['grade'] | ||
186 | - dbtest.state = test['state'] | ||
187 | - dbtest.questions = [ | ||
188 | - Question( | ||
189 | - number=n, | ||
190 | - ref=q['ref'], | ||
191 | - grade=q['grade'], | ||
192 | - comment=q.get('comment', ''), | ||
193 | - starttime=str(test['start_time']), | ||
194 | - finishtime=str(test['finish_time']), | ||
195 | - test_id=test['ref'] | ||
196 | - ) | ||
197 | - for n, q in enumerate(test['questions']) | ||
198 | - ] | ||
199 | - logger.info('%s database updated.', uid) | 138 | +# # ------------------------------------------------------------------------ |
139 | +# # FIXME not working | ||
140 | +# def _correct_tests(self): | ||
141 | +# with Session(self._engine, future=True) as session: | ||
142 | +# # Find which tests have to be corrected | ||
143 | +# dbtests = session.execute( | ||
144 | +# select(Test). | ||
145 | +# where(Test.ref == self.testfactory['ref']). | ||
146 | +# where(Test.state == "SUBMITTED") | ||
147 | +# ).all() | ||
148 | +# # dbtests = session.query(Test)\ | ||
149 | +# # .filter(Test.ref == self.testfactory['ref'])\ | ||
150 | +# # .filter(Test.state == "SUBMITTED")\ | ||
151 | +# # .all() | ||
152 | + | ||
153 | +# logger.info('Correcting %d tests...', len(dbtests)) | ||
154 | +# for dbtest in dbtests: | ||
155 | +# try: | ||
156 | +# with open(dbtest.filename) as file: | ||
157 | +# testdict = json.load(file) | ||
158 | +# except FileNotFoundError: | ||
159 | +# logger.error('File not found: %s', dbtest.filename) | ||
160 | +# continue | ||
161 | + | ||
162 | +# # creates a class Test with the methods to correct it | ||
163 | +# # the questions are still dictionaries, so we have to call | ||
164 | +# # question_from() to produce Question() instances that can be | ||
165 | +# # corrected. Finally the test can be corrected. | ||
166 | +# test = perguntations.test.Test(testdict) | ||
167 | +# test['questions'] = [question_from(q) for q in test['questions']] | ||
168 | +# test.correct() | ||
169 | +# logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) | ||
170 | + | ||
171 | +# # save JSON file (overwriting the old one) | ||
172 | +# uid = test['student']['number'] | ||
173 | +# ref = test['ref'] | ||
174 | +# finish_time = test['finish_time'] | ||
175 | +# answers_dir = test['answers_dir'] | ||
176 | +# fname = f'{uid}--{ref}--{finish_time}.json' | ||
177 | +# fpath = path.join(answers_dir, fname) | ||
178 | +# test.save_json(fpath) | ||
179 | +# logger.info('%s saved JSON file.', uid) | ||
180 | + | ||
181 | +# # update database | ||
182 | +# dbtest.grade = test['grade'] | ||
183 | +# dbtest.state = test['state'] | ||
184 | +# dbtest.questions = [ | ||
185 | +# Question( | ||
186 | +# number=n, | ||
187 | +# ref=q['ref'], | ||
188 | +# grade=q['grade'], | ||
189 | +# comment=q.get('comment', ''), | ||
190 | +# starttime=str(test['start_time']), | ||
191 | +# finishtime=str(test['finish_time']), | ||
192 | +# test_id=test['ref'] | ||
193 | +# ) | ||
194 | +# for n, q in enumerate(test['questions']) | ||
195 | +# ] | ||
196 | +# logger.info('%s database updated.', uid) | ||
200 | 197 | ||
201 | # ------------------------------------------------------------------------ | 198 | # ------------------------------------------------------------------------ |
202 | - async def login(self, uid, try_pw, headers=None): | 199 | + async def login(self, uid, password, headers=None): |
203 | '''login authentication''' | 200 | '''login authentication''' |
204 | - if uid not in self.allowed and uid != '0': # not allowed | 201 | + if uid != '0' and uid not in self.allowed: # not allowed |
205 | logger.warning('"%s" unauthorized.', uid) | 202 | logger.warning('"%s" unauthorized.', uid) |
206 | return 'unauthorized' | 203 | return 'unauthorized' |
207 | 204 | ||
208 | with Session(self._engine, future=True) as session: | 205 | with Session(self._engine, future=True) as session: |
209 | - # with self._db_session() as sess: | ||
210 | - name, hashed_pw = session.execute( | 206 | + name, hashed = session.execute( |
211 | select(Student.name, Student.password). | 207 | select(Student.name, Student.password). |
212 | - where(id == uid) | 208 | + where(Student.id == uid) |
213 | ).one() | 209 | ).one() |
214 | - # name, hashed_pw = sess.query(Student.name, Student.password)\ | ||
215 | - # .filter_by(id=uid)\ | ||
216 | - # .one() | ||
217 | 210 | ||
218 | - if hashed_pw == '': # update password on first login | ||
219 | - await self.update_student_password(uid, try_pw) | ||
220 | - pw_ok = True | 211 | + if hashed == '': # update password on first login |
212 | + logger.info('First login "%s"', name) | ||
213 | + await self.update_password(uid, password) | ||
214 | + ok = True | ||
221 | else: # check password | 215 | else: # check password |
222 | loop = asyncio.get_running_loop() | 216 | loop = asyncio.get_running_loop() |
223 | - pw_ok = await loop.run_in_executor(None, | ||
224 | - bcrypt.checkpw, | ||
225 | - try_pw.encode('utf-8'), | ||
226 | - hashed_pw.password) | ||
227 | - # pw_ok = await check_password(try_pw, hashed_pw) # async bcrypt | 217 | + ok = await loop.run_in_executor(None, |
218 | + bcrypt.checkpw, | ||
219 | + password.encode('utf-8'), | ||
220 | + hashed) | ||
228 | 221 | ||
229 | - if not pw_ok: # wrong password | 222 | + if not ok: |
230 | logger.info('"%s" wrong password.', uid) | 223 | logger.info('"%s" wrong password.', uid) |
231 | return 'wrong_password' | 224 | return 'wrong_password' |
232 | 225 | ||
@@ -238,6 +231,7 @@ class App(): | @@ -238,6 +231,7 @@ class App(): | ||
238 | uid, headers['remote_ip']) | 231 | uid, headers['remote_ip']) |
239 | # FIXME invalidate previous login | 232 | # FIXME invalidate previous login |
240 | else: | 233 | else: |
234 | + # first login | ||
241 | self.online[uid] = {'student': { | 235 | self.online[uid] = {'student': { |
242 | 'name': name, | 236 | 'name': name, |
243 | 'number': uid, | 237 | 'number': uid, |
@@ -375,8 +369,9 @@ class App(): | @@ -375,8 +369,9 @@ class App(): | ||
375 | for n, q in enumerate(test['questions']) | 369 | for n, q in enumerate(test['questions']) |
376 | ] | 370 | ] |
377 | 371 | ||
378 | - with self._db_session() as sess: | ||
379 | - sess.add(test_row) | 372 | + with Session(self._engine, future=True) as session: |
373 | + session.add(test_row) | ||
374 | + session.commit() | ||
380 | logger.info('"%s" database updated.', uid) | 375 | logger.info('"%s" database updated.', uid) |
381 | 376 | ||
382 | # ------------------------------------------------------------------------ | 377 | # ------------------------------------------------------------------------ |
@@ -432,12 +427,22 @@ class App(): | @@ -432,12 +427,22 @@ class App(): | ||
432 | def get_questions_csv(self): | 427 | def get_questions_csv(self): |
433 | '''generates a CSV with the grades of the test''' | 428 | '''generates a CSV with the grades of the test''' |
434 | test_ref = self.testfactory['ref'] | 429 | test_ref = self.testfactory['ref'] |
435 | - with self._db_session() as sess: | ||
436 | - questions = sess.query(Test.id, Test.student_id, Test.starttime, | ||
437 | - Question.number, Question.grade)\ | ||
438 | - .join(Question)\ | ||
439 | - .filter(Test.ref == test_ref)\ | ||
440 | - .all() | 430 | + with Session(self._engine, future=True) as session: |
431 | + questions = session.execute( | ||
432 | + select(Test.id, Test.student_id, Test.starttime, | ||
433 | + Question.number, Question.grade). | ||
434 | + join(Question). | ||
435 | + where(Test.ref == test_ref) | ||
436 | + ).all() | ||
437 | + print(questions) | ||
438 | + | ||
439 | + | ||
440 | + | ||
441 | + # questions = sess.query(Test.id, Test.student_id, Test.starttime, | ||
442 | + # Question.number, Question.grade)\ | ||
443 | + # .join(Question)\ | ||
444 | + # .filter(Test.ref == test_ref)\ | ||
445 | + # .all() | ||
441 | 446 | ||
442 | qnums = set() # keeps track of all the questions in the test | 447 | qnums = set() # keeps track of all the questions in the test |
443 | tests = {} # {test_id: {student_id, starttime, 0: grade, ...}} | 448 | tests = {} # {test_id: {student_id, starttime, 0: grade, ...}} |
@@ -463,14 +468,21 @@ class App(): | @@ -463,14 +468,21 @@ class App(): | ||
463 | def get_test_csv(self): | 468 | def get_test_csv(self): |
464 | '''generates a CSV with the grades of the test currently running''' | 469 | '''generates a CSV with the grades of the test currently running''' |
465 | test_ref = self.testfactory['ref'] | 470 | test_ref = self.testfactory['ref'] |
466 | - with self._db_session() as sess: | ||
467 | - tests = sess.query(Test.student_id, | ||
468 | - Test.grade, | ||
469 | - Test.starttime, Test.finishtime)\ | ||
470 | - .filter(Test.ref == test_ref)\ | ||
471 | - .order_by(Test.student_id)\ | ||
472 | - .all() | ||
473 | - | 471 | + with Session(self._engine, future=True) as session: |
472 | + tests = session.execute( | ||
473 | + select(Test.student_id, Test.grade, Test.starttime, Test.finishtime). | ||
474 | + where(Test.ref == test_ref). | ||
475 | + order_by(Test.student_id) | ||
476 | + ).all() | ||
477 | + # with self._db_session() as sess: | ||
478 | + # tests = sess.query(Test.student_id, | ||
479 | + # Test.grade, | ||
480 | + # Test.starttime, Test.finishtime)\ | ||
481 | + # .filter(Test.ref == test_ref)\ | ||
482 | + # .order_by(Test.student_id)\ | ||
483 | + # .all() | ||
484 | + | ||
485 | + print(tests) | ||
474 | if not tests: | 486 | if not tests: |
475 | logger.warning('Empty CSV: there are no tests!') | 487 | logger.warning('Empty CSV: there are no tests!') |
476 | return test_ref, '' | 488 | return test_ref, '' |
@@ -603,21 +615,40 @@ class App(): | @@ -603,21 +615,40 @@ class App(): | ||
603 | logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', | 615 | logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', |
604 | uid, area, win_x, win_y, scr_x, scr_y) | 616 | uid, area, win_x, win_y, scr_x, scr_y) |
605 | 617 | ||
606 | - async def update_student_password(self, uid, password=''): | 618 | + async def update_password(self, uid, password=''): |
607 | '''change password on the database''' | 619 | '''change password on the database''' |
608 | if password: | 620 | if password: |
609 | - password = await hash_password(password) | ||
610 | - with self._db_session() as sess: | ||
611 | - student = sess.query(Student).filter_by(id=uid).one() | 621 | + # password = await hash_password(password) |
622 | + loop = asyncio.get_running_loop() | ||
623 | + password = await loop.run_in_executor(None, | ||
624 | + bcrypt.hashpw, | ||
625 | + password.encode('utf-8'), | ||
626 | + bcrypt.gensalt()) | ||
627 | + | ||
628 | + # with self._db_session() as sess: | ||
629 | + # student = sess.query(Student).filter_by(id=uid).one() | ||
630 | + with Session(self._engine, future=True) as session: | ||
631 | + student = session.execute( | ||
632 | + select(Student). | ||
633 | + where(Student.id == uid) | ||
634 | + ).scalar_one() | ||
612 | student.password = password | 635 | student.password = password |
613 | logger.info('"%s" password updated.', uid) | 636 | logger.info('"%s" password updated.', uid) |
614 | 637 | ||
615 | def insert_new_student(self, uid, name): | 638 | def insert_new_student(self, uid, name): |
616 | '''insert new student into the database''' | 639 | '''insert new student into the database''' |
617 | - try: | ||
618 | - with self._db_session() as sess: | ||
619 | - sess.add(Student(id=uid, name=name, password='')) | ||
620 | - except exc.SQLAlchemyError: | ||
621 | - logger.error('Insert failed: student %s already exists?', uid) | ||
622 | - else: | ||
623 | - logger.info('New student: "%s", "%s"', uid, name) | 640 | + with Session(self._engine, future=True) as session: |
641 | + session.add( | ||
642 | + Student(id=uid, name=name, password='') | ||
643 | + ) | ||
644 | + session.commit() | ||
645 | + # try: | ||
646 | + # with Session(self._engine, future=True) as session: | ||
647 | + # session.add( | ||
648 | + # Student(id=uid, name=name, password='') | ||
649 | + # ) | ||
650 | + # session.commit() | ||
651 | + # except Exception: | ||
652 | + # logger.error('Insert failed: student %s already exists?', uid) | ||
653 | + # else: | ||
654 | + # logger.info('New student: "%s", "%s"', uid, name) |
perguntations/initdb.py
@@ -194,7 +194,7 @@ def main(): | @@ -194,7 +194,7 @@ def main(): | ||
194 | if args.update_all: | 194 | if args.update_all: |
195 | all_students = session.execute( | 195 | all_students = session.execute( |
196 | select(Student).where(Student.id != '0') | 196 | select(Student).where(Student.id != '0') |
197 | - ).all() | 197 | + ).scalars().all() |
198 | 198 | ||
199 | print(f'Updating password of {len(all_students)} users', end='') | 199 | print(f'Updating password of {len(all_students)} users', end='') |
200 | for student in all_students: | 200 | for student in all_students: |
@@ -208,9 +208,12 @@ def main(): | @@ -208,9 +208,12 @@ def main(): | ||
208 | else: | 208 | else: |
209 | for student_id in args.update: | 209 | for student_id in args.update: |
210 | print(f'Updating password of {student_id}') | 210 | print(f'Updating password of {student_id}') |
211 | - student = session.execute(select(Student.id)) | ||
212 | - password = (args.pw or student_id).encode('utf-8') | ||
213 | - student.password = bcrypt.hashpw(password, bcrypt.gensalt()) | 211 | + student = session.execute( |
212 | + select(Student). | ||
213 | + where(Student.id == student_id) | ||
214 | + ).scalar_one() | ||
215 | + new_password = (args.pw or student_id).encode('utf-8') | ||
216 | + student.password = bcrypt.hashpw(new_password, bcrypt.gensalt()) | ||
214 | session.commit() | 217 | session.commit() |
215 | 218 | ||
216 | show_students_in_database(session, args.verbose) | 219 | show_students_in_database(session, args.verbose) |
update.sh