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 | 6 | |
| 7 | 7 | # python standard libraries |
| 8 | 8 | import asyncio |
| 9 | -# from contextlib import contextmanager # `with` statement in db sessions | |
| 10 | 9 | import csv |
| 11 | 10 | import io |
| 12 | 11 | import json |
| ... | ... | @@ -17,8 +16,7 @@ from os import path |
| 17 | 16 | import bcrypt |
| 18 | 17 | from sqlalchemy import create_engine, select, func |
| 19 | 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 | 21 | # this project |
| 24 | 22 | from perguntations.models import Student, Test, Question |
| ... | ... | @@ -113,8 +111,6 @@ class App(): |
| 113 | 111 | logger.info('Generating %d tests. May take awhile...', |
| 114 | 112 | len(self.allowed)) |
| 115 | 113 | self._pregenerate_tests(len(self.allowed)) |
| 116 | - else: | |
| 117 | - logger.info('No tests generated yet.') | |
| 118 | 114 | |
| 119 | 115 | if conf['correct']: |
| 120 | 116 | self._correct_tests() |
| ... | ... | @@ -139,94 +135,91 @@ class App(): |
| 139 | 135 | |
| 140 | 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 | 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 | 202 | logger.warning('"%s" unauthorized.', uid) |
| 206 | 203 | return 'unauthorized' |
| 207 | 204 | |
| 208 | 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 | 207 | select(Student.name, Student.password). |
| 212 | - where(id == uid) | |
| 208 | + where(Student.id == uid) | |
| 213 | 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 | 215 | else: # check password |
| 222 | 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 | 223 | logger.info('"%s" wrong password.', uid) |
| 231 | 224 | return 'wrong_password' |
| 232 | 225 | |
| ... | ... | @@ -238,6 +231,7 @@ class App(): |
| 238 | 231 | uid, headers['remote_ip']) |
| 239 | 232 | # FIXME invalidate previous login |
| 240 | 233 | else: |
| 234 | + # first login | |
| 241 | 235 | self.online[uid] = {'student': { |
| 242 | 236 | 'name': name, |
| 243 | 237 | 'number': uid, |
| ... | ... | @@ -375,8 +369,9 @@ class App(): |
| 375 | 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 | 375 | logger.info('"%s" database updated.', uid) |
| 381 | 376 | |
| 382 | 377 | # ------------------------------------------------------------------------ |
| ... | ... | @@ -432,12 +427,22 @@ class App(): |
| 432 | 427 | def get_questions_csv(self): |
| 433 | 428 | '''generates a CSV with the grades of the test''' |
| 434 | 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 | 447 | qnums = set() # keeps track of all the questions in the test |
| 443 | 448 | tests = {} # {test_id: {student_id, starttime, 0: grade, ...}} |
| ... | ... | @@ -463,14 +468,21 @@ class App(): |
| 463 | 468 | def get_test_csv(self): |
| 464 | 469 | '''generates a CSV with the grades of the test currently running''' |
| 465 | 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 | 486 | if not tests: |
| 475 | 487 | logger.warning('Empty CSV: there are no tests!') |
| 476 | 488 | return test_ref, '' |
| ... | ... | @@ -603,21 +615,40 @@ class App(): |
| 603 | 615 | logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', |
| 604 | 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 | 619 | '''change password on the database''' |
| 608 | 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 | 635 | student.password = password |
| 613 | 636 | logger.info('"%s" password updated.', uid) |
| 614 | 637 | |
| 615 | 638 | def insert_new_student(self, uid, name): |
| 616 | 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 | 194 | if args.update_all: |
| 195 | 195 | all_students = session.execute( |
| 196 | 196 | select(Student).where(Student.id != '0') |
| 197 | - ).all() | |
| 197 | + ).scalars().all() | |
| 198 | 198 | |
| 199 | 199 | print(f'Updating password of {len(all_students)} users', end='') |
| 200 | 200 | for student in all_students: |
| ... | ... | @@ -208,9 +208,12 @@ def main(): |
| 208 | 208 | else: |
| 209 | 209 | for student_id in args.update: |
| 210 | 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 | 217 | session.commit() |
| 215 | 218 | |
| 216 | 219 | show_students_in_database(session, args.verbose) | ... | ... |
update.sh