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