Commit ffb53a93a1ec9a7e45b3e69dbdfe9d05b822c1f8
1 parent
4c5146e6
Exists in
master
and in
1 other branch
First version that seems to be working after update to
sqlalchemy1.4. Needs more testing.
Showing
15 changed files
with
243 additions
and
196 deletions
Show diff stats
BUGS.md
| ... | ... | @@ -2,7 +2,6 @@ |
| 2 | 2 | # BUGS |
| 3 | 3 | |
| 4 | 4 | - correct devia poder ser corrido mais que uma vez (por exemplo para alterar cotacoes, corrigir perguntas) |
| 5 | -- nao esta a mostrar imagens?? internal server error? | |
| 6 | 5 | - guardar testes em JSON assim que sao atribuidos aos alunos (ou guardados inicialmente com um certo nome, e atribuidos posteriormente ao aluno). |
| 7 | 6 | - cookies existe um perguntations_user e um user. De onde vem o user? |
| 8 | 7 | - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc) | ... | ... |
demo/questions/questions-tutorial.yaml
| ... | ... | @@ -431,6 +431,10 @@ |
| 431 | 431 | pode estar previamente preenchida como neste caso (use `answer: texto`). |
| 432 | 432 | correct: correct/correct-question.py |
| 433 | 433 | timeout: 5 |
| 434 | + tests_right: | |
| 435 | + - 'red green blue' | |
| 436 | + # tests_wrong: | |
| 437 | + # - 'blue gray yellow' | |
| 434 | 438 | |
| 435 | 439 | # --------------------------------------------------------------------------- |
| 436 | 440 | - type: information | ... | ... |
mypy.ini
| 1 | 1 | [mypy] |
| 2 | -python_version = 3.8 | |
| 2 | +python_version = 3.9 | |
| 3 | 3 | |
| 4 | -[mypy-setuptools.*] | |
| 5 | -ignore_missing_imports = True | |
| 4 | +; [mypy-setuptools.*] | |
| 5 | +; ignore_missing_imports = True | |
| 6 | 6 | |
| 7 | -[mypy-sqlalchemy.*] | |
| 8 | -ignore_missing_imports = True | |
| 7 | +; [mypy-sqlalchemy.*] | |
| 8 | +; ignore_missing_imports = True | |
| 9 | 9 | |
| 10 | -[mypy-pygments.*] | |
| 11 | -ignore_missing_imports = True | |
| 10 | +; [mypy-pygments.*] | |
| 11 | +; ignore_missing_imports = True | |
| 12 | 12 | |
| 13 | -[mypy-mistune.*] | |
| 14 | -ignore_missing_imports = True | |
| 13 | +; [mypy-mistune.*] | |
| 14 | +; ignore_missing_imports = True | ... | ... |
perguntations/app.py
| ... | ... | @@ -97,8 +97,20 @@ class App(): |
| 97 | 97 | raise AppException(msg) from None |
| 98 | 98 | logger.info('Database has %d students.', len(dbstudents)) |
| 99 | 99 | |
| 100 | - self._students = {uid: {'name': name, 'state': 'offline', 'test': None} | |
| 101 | - for uid, name in dbstudents} | |
| 100 | + self._students = {uid: { | |
| 101 | + 'name': name, | |
| 102 | + 'state': 'offline', | |
| 103 | + 'test': None, | |
| 104 | + } for uid, name in dbstudents} | |
| 105 | + | |
| 106 | + # ------------------------------------------------------------------------ | |
| 107 | + async def _assign_tests(self) -> None: | |
| 108 | + '''Generate tests for all students that don't yet have a test''' | |
| 109 | + logger.info('Generating tests...') | |
| 110 | + for student in self._students.values(): | |
| 111 | + if student.get('test', None) is None: | |
| 112 | + student['test'] = await self._testfactory.generate() | |
| 113 | + logger.info('Tests assigned to all students') | |
| 102 | 114 | |
| 103 | 115 | # ------------------------------------------------------------------------ |
| 104 | 116 | async def login(self, uid: str, password: str, headers: dict) -> Optional[str]: |
| ... | ... | @@ -114,6 +126,7 @@ class App(): |
| 114 | 126 | logger.warning('"%s" does not exist', uid) |
| 115 | 127 | return 'nonexistent' |
| 116 | 128 | |
| 129 | + | |
| 117 | 130 | if uid != '0' and self._students[uid]['state'] != 'allowed': |
| 118 | 131 | logger.warning('"%s" login not allowed', uid) |
| 119 | 132 | return 'not allowed' |
| ... | ... | @@ -127,16 +140,16 @@ class App(): |
| 127 | 140 | # success |
| 128 | 141 | if uid == '0': |
| 129 | 142 | logger.info('Admin login from %s', headers['remote_ip']) |
| 130 | - return | |
| 131 | - | |
| 132 | - # FIXME this should probably be done elsewhere | |
| 133 | - test = await self._testfactory.generate() | |
| 134 | - test.start(uid) | |
| 135 | - self._students[uid]['test'] = test | |
| 136 | - | |
| 137 | - self._students[uid]['state'] = 'waiting' | |
| 138 | - self._students[uid]['headers'] = headers | |
| 139 | - logger.info('"%s" login from %s.', uid, headers['remote_ip']) | |
| 143 | + await self._assign_tests() | |
| 144 | + else: | |
| 145 | + student = self._students[uid] | |
| 146 | + student['test'].start(uid) | |
| 147 | + student['state'] = 'online' | |
| 148 | + student['headers'] = headers | |
| 149 | + student['unfocus'] = False | |
| 150 | + student['area'] = 1.0 | |
| 151 | + logger.info('"%s" login from %s.', uid, headers['remote_ip']) | |
| 152 | + return None | |
| 140 | 153 | |
| 141 | 154 | # ------------------------------------------------------------------------ |
| 142 | 155 | async def set_password(self, uid: str, password: str) -> None: |
| ... | ... | @@ -151,10 +164,14 @@ class App(): |
| 151 | 164 | # ------------------------------------------------------------------------ |
| 152 | 165 | def logout(self, uid: str) -> None: |
| 153 | 166 | '''student logout''' |
| 154 | - if uid in self._students: | |
| 155 | - self._students[uid]['test'] = None | |
| 156 | - self._students[uid]['state'] = 'offline' | |
| 157 | - logger.info('"%s" logged out.', uid) | |
| 167 | + student = self._students.get(uid, None) | |
| 168 | + if student is not None: | |
| 169 | + # student['test'] = None | |
| 170 | + student['state'] = 'offline' | |
| 171 | + student.pop('headers', None) | |
| 172 | + student.pop('unfocus', None) | |
| 173 | + student.pop('area', None) | |
| 174 | + logger.info('"%s" logged out.', uid) | |
| 158 | 175 | |
| 159 | 176 | # ------------------------------------------------------------------------ |
| 160 | 177 | def _make_test_factory(self, filename: str) -> None: |
| ... | ... | @@ -187,9 +204,12 @@ class App(): |
| 187 | 204 | ans is a dictionary {question_index: answer, ...} with the answers for |
| 188 | 205 | the complete test. For example: {0:'hello', 1:[1,2]} |
| 189 | 206 | ''' |
| 190 | - logger.info('"%s" submitted %d answers.', uid, len(ans)) | |
| 207 | + if self._students[uid]['state'] != 'online': | |
| 208 | + logger.warning('"%s" INVALID SUBMISSION! STUDENT NOT ONLINE', uid) | |
| 209 | + return | |
| 191 | 210 | |
| 192 | 211 | # --- submit answers and correct test |
| 212 | + logger.info('"%s" submitted %d answers.', uid, len(ans)) | |
| 193 | 213 | test = self._students[uid]['test'] |
| 194 | 214 | test.submit(ans) |
| 195 | 215 | |
| ... | ... | @@ -236,7 +256,6 @@ class App(): |
| 236 | 256 | logger.info('"%s" database updated.', uid) |
| 237 | 257 | |
| 238 | 258 | # # ------------------------------------------------------------------------ |
| 239 | - # FIXME not working | |
| 240 | 259 | # def _correct_tests(self): |
| 241 | 260 | # with Session(self._engine, future=True) as session: |
| 242 | 261 | # # Find which tests have to be corrected |
| ... | ... | @@ -325,15 +344,15 @@ class App(): |
| 325 | 344 | # return test |
| 326 | 345 | |
| 327 | 346 | # ------------------------------------------------------------------------ |
| 328 | - def event_test(self, uid, cmd, value): | |
| 347 | + def register_event(self, uid, cmd, value): | |
| 329 | 348 | '''handles browser events the occur during the test''' |
| 330 | - # if cmd == 'focus': | |
| 331 | - # if value: | |
| 332 | - # self._focus_student(uid) | |
| 333 | - # else: | |
| 334 | - # self._unfocus_student(uid) | |
| 335 | - # elif cmd == 'size': | |
| 336 | - # self._set_screen_area(uid, value) | |
| 349 | + if cmd == 'focus': | |
| 350 | + if value: | |
| 351 | + self._focus_student(uid) | |
| 352 | + else: | |
| 353 | + self._unfocus_student(uid) | |
| 354 | + elif cmd == 'size': | |
| 355 | + self._set_screen_area(uid, value) | |
| 337 | 356 | |
| 338 | 357 | # ======================================================================== |
| 339 | 358 | # GETTERS |
| ... | ... | @@ -349,7 +368,7 @@ class App(): |
| 349 | 368 | |
| 350 | 369 | # ------------------------------------------------------------------------ |
| 351 | 370 | def get_test_config(self) -> dict: |
| 352 | - '''return brief test configuration''' | |
| 371 | + '''return brief test configuration to use as header in /admin''' | |
| 353 | 372 | return {'title': self._testfactory['title'], |
| 354 | 373 | 'ref': self._testfactory['ref'], |
| 355 | 374 | 'filename': self._testfactory['testfile'], |
| ... | ... | @@ -426,15 +445,14 @@ class App(): |
| 426 | 445 | |
| 427 | 446 | # ------------------------------------------------------------------------ |
| 428 | 447 | def get_students_state(self) -> list: |
| 429 | - '''get list of states of every student''' | |
| 448 | + '''get list of states of every student to show in /admin page''' | |
| 430 | 449 | return [{ 'uid': uid, |
| 431 | 450 | 'name': student['name'], |
| 432 | 451 | 'allowed': student['state'] == 'allowed', |
| 433 | 452 | 'online': student['state'] == 'online', |
| 434 | - # 'start_time': student.get('test', {}).get('start_time', ''), | |
| 435 | - # 'password_defined': False, #pw != '', | |
| 436 | - # 'unfocus': False, | |
| 437 | - # 'area': '0.89', | |
| 453 | + 'start_time': student.get('test', {}).get('start_time', ''), | |
| 454 | + 'unfocus': student.get('unfocus', False), | |
| 455 | + 'area': student.get('area', 1.0), | |
| 438 | 456 | 'grades': self.get_grades(uid, self._testfactory['ref']) } |
| 439 | 457 | for uid, student in self._students.items()] |
| 440 | 458 | |
| ... | ... | @@ -508,7 +526,7 @@ class App(): |
| 508 | 526 | student['state'] = 'offline' |
| 509 | 527 | |
| 510 | 528 | # ------------------------------------------------------------------------ |
| 511 | - def insert_new_student(self, uid: str, name: str) -> None: | |
| 529 | + async def insert_new_student(self, uid: str, name: str) -> None: | |
| 512 | 530 | '''insert new student into the database''' |
| 513 | 531 | with Session(self._engine, future=True) as session: |
| 514 | 532 | try: |
| ... | ... | @@ -519,11 +537,16 @@ class App(): |
| 519 | 537 | session.rollback() |
| 520 | 538 | return |
| 521 | 539 | logger.info('New student added: %s %s', uid, name) |
| 522 | - self._students[uid] = {'name': name, 'state': 'offline', 'test': None} | |
| 540 | + self._students[uid] = { | |
| 541 | + 'name': name, | |
| 542 | + 'state': 'offline', | |
| 543 | + 'test': await self._testfactory.generate(), | |
| 544 | + } | |
| 523 | 545 | |
| 524 | 546 | # ------------------------------------------------------------------------ |
| 525 | 547 | def allow_from_list(self, filename: str) -> None: |
| 526 | 548 | '''allow students listed in text file (one number per line)''' |
| 549 | + # parse list of students to allow (one number per line) | |
| 527 | 550 | try: |
| 528 | 551 | with open(filename, 'r', encoding='utf-8') as file: |
| 529 | 552 | allowed = {line.strip() for line in file} |
| ... | ... | @@ -533,6 +556,7 @@ class App(): |
| 533 | 556 | logger.critical(error_msg) |
| 534 | 557 | raise AppException(error_msg) from exc |
| 535 | 558 | |
| 559 | + # update allowed state (missing are students allowed that don't exist) | |
| 536 | 560 | missing = 0 |
| 537 | 561 | for uid in allowed: |
| 538 | 562 | try: |
| ... | ... | @@ -545,20 +569,23 @@ class App(): |
| 545 | 569 | if missing: |
| 546 | 570 | logger.warning(' %d missing!', missing) |
| 547 | 571 | |
| 548 | - # def _focus_student(self, uid): | |
| 549 | - # '''set student in focus state''' | |
| 550 | - # self.unfocus.discard(uid) | |
| 551 | - # logger.info('"%s" focus', uid) | |
| 552 | - | |
| 553 | - # def _unfocus_student(self, uid): | |
| 554 | - # '''set student in unfocus state''' | |
| 555 | - # self.unfocus.add(uid) | |
| 556 | - # logger.info('"%s" unfocus', uid) | |
| 557 | - | |
| 558 | - # def _set_screen_area(self, uid, sizes): | |
| 559 | - # '''set current browser area as detected in resize event''' | |
| 560 | - # scr_y, scr_x, win_y, win_x = sizes | |
| 561 | - # area = win_x * win_y / (scr_x * scr_y) * 100 | |
| 562 | - # self.area[uid] = area | |
| 563 | - # logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', | |
| 564 | - # uid, area, win_x, win_y, scr_x, scr_y) | |
| 572 | + # ------------------------------------------------------------------------ | |
| 573 | + def _focus_student(self, uid): | |
| 574 | + '''set student in focus state''' | |
| 575 | + self._students[uid]['unfocus'] = False | |
| 576 | + logger.info('"%s" focus', uid) | |
| 577 | + | |
| 578 | + # ------------------------------------------------------------------------ | |
| 579 | + def _unfocus_student(self, uid): | |
| 580 | + '''set student in unfocus state''' | |
| 581 | + self._students[uid]['unfocus'] = True | |
| 582 | + logger.info('"%s" unfocus', uid) | |
| 583 | + | |
| 584 | + # ------------------------------------------------------------------------ | |
| 585 | + def _set_screen_area(self, uid, sizes): | |
| 586 | + '''set current browser area as detected in resize event''' | |
| 587 | + scr_y, scr_x, win_y, win_x = sizes | |
| 588 | + area = win_x * win_y / (scr_x * scr_y) * 100 | |
| 589 | + self._students[uid]['area'] = area | |
| 590 | + logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', | |
| 591 | + uid, area, win_x, win_y, scr_x, scr_y) | ... | ... |
perguntations/main.py
| ... | ... | @@ -76,7 +76,6 @@ def get_logger_config(debug=False) -> dict: |
| 76 | 76 | |
| 77 | 77 | if debug: |
| 78 | 78 | level = 'DEBUG' |
| 79 | - # fmt = '%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s' | |
| 80 | 79 | fmt = '%(asctime)s %(levelname)-8s %(module)-12s%(lineno)4d| %(message)s' |
| 81 | 80 | dateformat = '' |
| 82 | 81 | else: | ... | ... |
perguntations/models.py
| ... | ... | @@ -3,17 +3,15 @@ perguntations/models.py |
| 3 | 3 | SQLAlchemy ORM |
| 4 | 4 | ''' |
| 5 | 5 | |
| 6 | +from typing import Any | |
| 6 | 7 | |
| 7 | 8 | from sqlalchemy import Column, ForeignKey, Integer, Float, String |
| 8 | 9 | from sqlalchemy.orm import declarative_base, relationship |
| 9 | 10 | |
| 10 | 11 | |
| 11 | -# ============================================================================ | |
| 12 | -# Declare ORM | |
| 13 | -# FIXME Any is a workaround for mypy static type checking (see https://github.com/python/mypy/issues/6372) | |
| 14 | -from typing import Any | |
| 12 | +# FIXME Any is a workaround for static type checking | |
| 13 | +# (https://github.com/python/mypy/issues/6372) | |
| 15 | 14 | Base: Any = declarative_base() |
| 16 | -# Base = declarative_base() | |
| 17 | 15 | |
| 18 | 16 | |
| 19 | 17 | # ---------------------------------------------------------------------------- | ... | ... |
perguntations/parser_markdown.py
| ... | ... | @@ -137,9 +137,9 @@ class HighlightRenderer(mistune.Renderer): |
| 137 | 137 | return '<table class="table table-sm"><thead class="thead-light">' \ |
| 138 | 138 | + header + '</thead><tbody>' + body + '</tbody></table>' |
| 139 | 139 | |
| 140 | - def image(self, src, title, alt): | |
| 140 | + def image(self, src, title, text): | |
| 141 | 141 | '''render image''' |
| 142 | - alt = mistune.escape(alt, quote=True) | |
| 142 | + alt = mistune.escape(text, quote=True) | |
| 143 | 143 | if title is not None: |
| 144 | 144 | if title: # not empty string, show as caption |
| 145 | 145 | title = mistune.escape(title, quote=True) | ... | ... |
perguntations/questions.py
| ... | ... | @@ -115,8 +115,7 @@ class QuestionRadio(Question): |
| 115 | 115 | # e.g. correct: 2 --> correct: [0,0,1,0,0] |
| 116 | 116 | if isinstance(self['correct'], int): |
| 117 | 117 | if not 0 <= self['correct'] < nopts: |
| 118 | - msg = (f'`correct` out of range 0..{nopts-1}. ' | |
| 119 | - f'In question "{self["ref"]}"') | |
| 118 | + msg = f'"{self["ref"]}": correct out of range 0..{nopts-1}' | |
| 120 | 119 | logger.error(msg) |
| 121 | 120 | raise QuestionException(msg) |
| 122 | 121 | |
| ... | ... | @@ -126,8 +125,7 @@ class QuestionRadio(Question): |
| 126 | 125 | elif isinstance(self['correct'], list): |
| 127 | 126 | # must match number of options |
| 128 | 127 | if len(self['correct']) != nopts: |
| 129 | - msg = (f'{nopts} options vs {len(self["correct"])} correct. ' | |
| 130 | - f'In question "{self["ref"]}"') | |
| 128 | + msg = f'"{self["ref"]}": number of options/correct mismatch' | |
| 131 | 129 | logger.error(msg) |
| 132 | 130 | raise QuestionException(msg) |
| 133 | 131 | |
| ... | ... | @@ -135,23 +133,20 @@ class QuestionRadio(Question): |
| 135 | 133 | try: |
| 136 | 134 | self['correct'] = [float(x) for x in self['correct']] |
| 137 | 135 | except (ValueError, TypeError) as exc: |
| 138 | - msg = ('`correct` must be list of numbers or booleans.' | |
| 139 | - f'In "{self["ref"]}"') | |
| 136 | + msg = f'"{self["ref"]}": correct must contain floats or bools' | |
| 140 | 137 | logger.error(msg) |
| 141 | 138 | raise QuestionException(msg) from exc |
| 142 | 139 | |
| 143 | 140 | # check grade boundaries |
| 144 | 141 | if self['discount'] and not all(0.0 <= x <= 1.0 |
| 145 | 142 | for x in self['correct']): |
| 146 | - msg = ('`correct` values must be in the interval [0.0, 1.0]. ' | |
| 147 | - f'In "{self["ref"]}"') | |
| 143 | + msg = f'"{self["ref"]}": correct must be in [0.0, 1.0]' | |
| 148 | 144 | logger.error(msg) |
| 149 | 145 | raise QuestionException(msg) |
| 150 | 146 | |
| 151 | 147 | # at least one correct option |
| 152 | 148 | if all(x < 1.0 for x in self['correct']): |
| 153 | - msg = ('At least one correct option is required. ' | |
| 154 | - f'In "{self["ref"]}"') | |
| 149 | + msg = f'"{self["ref"]}": has no correct options' | |
| 155 | 150 | logger.error(msg) |
| 156 | 151 | raise QuestionException(msg) |
| 157 | 152 | |
| ... | ... | @@ -678,7 +673,7 @@ class QFactory(): |
| 678 | 673 | # which will print a valid question in yaml format to stdout. This |
| 679 | 674 | # output is then yaml parsed into a dictionary `q`. |
| 680 | 675 | if qdict['type'] == 'generator': |
| 681 | - logger.debug(' \\_ Running "%s".', qdict['script']) | |
| 676 | + logger.debug(' \\_ Running "%s"', qdict['script']) | |
| 682 | 677 | qdict.setdefault('args', []) |
| 683 | 678 | qdict.setdefault('stdin', '') |
| 684 | 679 | script = path.join(qdict['path'], qdict['script']) | ... | ... |
perguntations/serve.py
| ... | ... | @@ -17,6 +17,7 @@ import re |
| 17 | 17 | import signal |
| 18 | 18 | import sys |
| 19 | 19 | from timeit import default_timer as timer |
| 20 | +from typing import Dict, Tuple | |
| 20 | 21 | import uuid |
| 21 | 22 | |
| 22 | 23 | # user installed libraries |
| ... | ... | @@ -67,8 +68,8 @@ def admin_only(func): |
| 67 | 68 | ''' |
| 68 | 69 | Decorator to restrict access to the administrator: |
| 69 | 70 | |
| 70 | - @admin_only | |
| 71 | - def get(self): ... | |
| 71 | + @admin_only | |
| 72 | + def get(self): | |
| 72 | 73 | ''' |
| 73 | 74 | @functools.wraps(func) |
| 74 | 75 | async def wrapper(self, *args, **kwargs): |
| ... | ... | @@ -111,7 +112,6 @@ class LoginHandler(BaseHandler): |
| 111 | 112 | _prefix = re.compile(r'[a-z]') |
| 112 | 113 | _error_msg = { |
| 113 | 114 | 'wrong_password': 'Senha errada', |
| 114 | - # 'already_online': 'Já está online, não pode entrar duas vezes', | |
| 115 | 115 | 'not allowed': 'Não está autorizado a fazer o teste', |
| 116 | 116 | 'nonexistent': 'Número de aluno inválido' |
| 117 | 117 | } |
| ... | ... | @@ -122,7 +122,6 @@ class LoginHandler(BaseHandler): |
| 122 | 122 | |
| 123 | 123 | async def post(self): |
| 124 | 124 | '''Authenticates student and login.''' |
| 125 | - # uid = self._prefix.sub('', self.get_body_argument('uid')) | |
| 126 | 125 | uid = self.get_body_argument('uid') |
| 127 | 126 | password = self.get_body_argument('pw') |
| 128 | 127 | headers = { |
| ... | ... | @@ -148,8 +147,8 @@ class LogoutHandler(BaseHandler): |
| 148 | 147 | @tornado.web.authenticated |
| 149 | 148 | def get(self): |
| 150 | 149 | '''Logs out a user.''' |
| 151 | - self.clear_cookie('perguntations_user') | |
| 152 | 150 | self.testapp.logout(self.current_user) |
| 151 | + self.clear_cookie('perguntations_user') | |
| 153 | 152 | self.render('login.html', error='') |
| 154 | 153 | |
| 155 | 154 | |
| ... | ... | @@ -159,7 +158,7 @@ class LogoutHandler(BaseHandler): |
| 159 | 158 | # pylint: disable=abstract-method |
| 160 | 159 | class RootHandler(BaseHandler): |
| 161 | 160 | ''' |
| 162 | - Generates test to student. | |
| 161 | + Presents test to student. | |
| 163 | 162 | Receives answers, corrects the test and sends back the grade. |
| 164 | 163 | Redirects user 0 to /admin. |
| 165 | 164 | ''' |
| ... | ... | @@ -172,7 +171,6 @@ class RootHandler(BaseHandler): |
| 172 | 171 | 'text-regex': 'question-text.html', |
| 173 | 172 | 'numeric-interval': 'question-text.html', |
| 174 | 173 | 'textarea': 'question-textarea.html', |
| 175 | - 'code': 'question-textarea.html', | |
| 176 | 174 | # -- information panels -- |
| 177 | 175 | 'information': 'question-information.html', |
| 178 | 176 | 'success': 'question-information.html', |
| ... | ... | @@ -188,19 +186,16 @@ class RootHandler(BaseHandler): |
| 188 | 186 | Sends test to student or redirects 0 to admin page. |
| 189 | 187 | Multiple calls to this function will return the same test. |
| 190 | 188 | ''' |
| 191 | - | |
| 192 | 189 | uid = self.current_user |
| 193 | 190 | logger.debug('"%s" GET /', uid) |
| 194 | 191 | |
| 195 | 192 | if uid == '0': |
| 196 | 193 | self.redirect('/admin') |
| 197 | - return | |
| 198 | - | |
| 199 | - test = self.testapp.get_test(uid) | |
| 200 | - name = self.testapp.get_name(uid) | |
| 201 | - self.render('test.html', | |
| 202 | - t=test, uid=uid, name=name, md=md_to_html, templ=self._templates) | |
| 203 | - | |
| 194 | + else: | |
| 195 | + test = self.testapp.get_test(uid) | |
| 196 | + name = self.testapp.get_name(uid) | |
| 197 | + self.render('test.html', t=test, uid=uid, name=name, md=md_to_html, | |
| 198 | + templ=self._templates) | |
| 204 | 199 | |
| 205 | 200 | # --- POST |
| 206 | 201 | @tornado.web.authenticated |
| ... | ... | @@ -210,8 +205,8 @@ class RootHandler(BaseHandler): |
| 210 | 205 | renders the grade. |
| 211 | 206 | |
| 212 | 207 | self.request.arguments = {'answered-0': [b'on'], '0': [b'13.45']} |
| 213 | - builds dictionary ans={0: 'answer0', 1:, 'answer1', ...} | |
| 214 | - unanswered questions not included. | |
| 208 | + builds dictionary ans = {0: 'answer0', 1:, 'answer1', ...} | |
| 209 | + unanswered questions are not included. | |
| 215 | 210 | ''' |
| 216 | 211 | starttime = timer() # performance timer |
| 217 | 212 | |
| ... | ... | @@ -226,17 +221,14 @@ class RootHandler(BaseHandler): |
| 226 | 221 | ans = {} |
| 227 | 222 | for i, question in enumerate(test['questions']): |
| 228 | 223 | qid = str(i) |
| 229 | - if 'answered-' + qid in self.request.arguments: | |
| 224 | + if f'answered-{qid}' in self.request.arguments: | |
| 230 | 225 | ans[i] = self.get_body_arguments(qid) |
| 231 | 226 | |
| 232 | 227 | # remove enclosing list in some question types |
| 233 | 228 | if question['type'] == 'radio': |
| 234 | - if not ans[i]: | |
| 235 | - ans[i] = None | |
| 236 | - else: | |
| 237 | - ans[i] = ans[i][0] | |
| 229 | + ans[i] = ans[i][0] if ans[i] else None | |
| 238 | 230 | elif question['type'] in ('text', 'text-regex', 'textarea', |
| 239 | - 'numeric-interval', 'code'): | |
| 231 | + 'numeric-interval'): | |
| 240 | 232 | ans[i] = ans[i][0] |
| 241 | 233 | |
| 242 | 234 | # submit answered questions, correct |
| ... | ... | @@ -253,8 +245,8 @@ class RootHandler(BaseHandler): |
| 253 | 245 | # pylint: disable=abstract-method |
| 254 | 246 | class StudentWebservice(BaseHandler): |
| 255 | 247 | ''' |
| 256 | - Receive ajax from students in the test in response from focus, unfocus and | |
| 257 | - resize events. | |
| 248 | + Receive ajax from students during the test in response to the events | |
| 249 | + focus, unfocus and resize. | |
| 258 | 250 | ''' |
| 259 | 251 | |
| 260 | 252 | @tornado.web.authenticated |
| ... | ... | @@ -262,8 +254,9 @@ class StudentWebservice(BaseHandler): |
| 262 | 254 | '''handle ajax post''' |
| 263 | 255 | uid = self.current_user |
| 264 | 256 | cmd = self.get_body_argument('cmd', None) |
| 265 | - value = json.loads(self.get_body_argument('value', None)) | |
| 266 | - self.testapp.event_test(uid, cmd, value) | |
| 257 | + value = self.get_body_argument('value', None) | |
| 258 | + if cmd is not None and value is not None: | |
| 259 | + self.testapp.register_event(uid, cmd, json.loads(value)) | |
| 267 | 260 | |
| 268 | 261 | |
| 269 | 262 | # ---------------------------------------------------------------------------- |
| ... | ... | @@ -287,8 +280,7 @@ class AdminWebservice(BaseHandler): |
| 287 | 280 | f'attachment; filename={test_ref}.csv') |
| 288 | 281 | self.write(data) |
| 289 | 282 | await self.flush() |
| 290 | - | |
| 291 | - if cmd == 'questionscsv': | |
| 283 | + elif cmd == 'questionscsv': | |
| 292 | 284 | test_ref, data = self.testapp.get_detailed_grades_csv() |
| 293 | 285 | self.set_header('Content-Type', 'text/csv') |
| 294 | 286 | self.set_header('content-Disposition', |
| ... | ... | @@ -344,11 +336,8 @@ class AdminHandler(BaseHandler): |
| 344 | 336 | await self.testapp.set_password(uid=value, pw='') |
| 345 | 337 | elif cmd == 'insert_student' and value is not None: |
| 346 | 338 | student = json.loads(value) |
| 347 | - self.testapp.insert_new_student(uid=student['number'], | |
| 348 | - name=student['name']) | |
| 349 | - | |
| 350 | - else: | |
| 351 | - logger.error('Unknown command: "%s"', cmd) | |
| 339 | + await self.testapp.insert_new_student(uid=student['number'], | |
| 340 | + name=student['name']) | |
| 352 | 341 | |
| 353 | 342 | |
| 354 | 343 | # ---------------------------------------------------------------------------- |
| ... | ... | @@ -360,7 +349,7 @@ class FileHandler(BaseHandler): |
| 360 | 349 | Handles static files from questions like images, etc. |
| 361 | 350 | ''' |
| 362 | 351 | |
| 363 | - _filecache = {} | |
| 352 | + _filecache: Dict[Tuple[str, str], bytes] = {} | |
| 364 | 353 | |
| 365 | 354 | @tornado.web.authenticated |
| 366 | 355 | async def get(self): |
| ... | ... | @@ -390,10 +379,10 @@ class FileHandler(BaseHandler): |
| 390 | 379 | test = self.testapp.get_test(uid) |
| 391 | 380 | except KeyError: |
| 392 | 381 | logger.warning('Could not get test to serve image file') |
| 393 | - raise tornado.web.HTTPError(404) # Not Found | |
| 382 | + raise tornado.web.HTTPError(404) from None # Not Found | |
| 394 | 383 | |
| 384 | + # search for the question that contains the image | |
| 395 | 385 | for question in test['questions']: |
| 396 | - # search for the question that contains the image | |
| 397 | 386 | if question['ref'] == ref: |
| 398 | 387 | filepath = path.join(question['path'], 'public', image) |
| 399 | 388 | |
| ... | ... | @@ -402,13 +391,13 @@ class FileHandler(BaseHandler): |
| 402 | 391 | data = file.read() |
| 403 | 392 | except OSError: |
| 404 | 393 | logger.error('Error reading file "%s"', filepath) |
| 405 | - break | |
| 394 | + return | |
| 406 | 395 | self._filecache[(ref, image)] = data |
| 407 | 396 | self.write(data) |
| 408 | 397 | if content_type is not None: |
| 409 | 398 | self.set_header("Content-Type", content_type) |
| 410 | 399 | await self.flush() |
| 411 | - break | |
| 400 | + return | |
| 412 | 401 | |
| 413 | 402 | |
| 414 | 403 | # --- REVIEW ----------------------------------------------------------------- |
| ... | ... | @@ -425,7 +414,6 @@ class ReviewHandler(BaseHandler): |
| 425 | 414 | 'text-regex': 'review-question-text.html', |
| 426 | 415 | 'numeric-interval': 'review-question-text.html', |
| 427 | 416 | 'textarea': 'review-question-text.html', |
| 428 | - 'code': 'review-question-text.html', | |
| 429 | 417 | # -- information panels -- |
| 430 | 418 | 'information': 'review-question-information.html', |
| 431 | 419 | 'success': 'review-question-information.html', |
| ... | ... | @@ -460,7 +448,6 @@ class ReviewHandler(BaseHandler): |
| 460 | 448 | |
| 461 | 449 | uid = test['student'] |
| 462 | 450 | name = self.testapp.get_name(uid) |
| 463 | - | |
| 464 | 451 | self.render('review.html', t=test, uid=uid, name=name, |
| 465 | 452 | md=md_to_html, templ=self._templates) |
| 466 | 453 | ... | ... |
perguntations/static/js/admin.js
| ... | ... | @@ -117,16 +117,13 @@ $(document).ready(function() { |
| 117 | 117 | d = json.data[i]; |
| 118 | 118 | var uid = d['uid']; |
| 119 | 119 | var checked = d['allowed'] ? 'checked' : ''; |
| 120 | - var password_defined = d['password_defined'] ? ' <span class="badge badge-secondary"><i class="fa fa-key" aria-hidden="true"></i></span>' : ''; | |
| 120 | + // var password_defined = d['password_defined'] ? ' <span class="badge badge-secondary"><i class="fa fa-key" aria-hidden="true"></i></span>' : ''; | |
| 121 | 121 | var hora_inicio = d['start_time'] ? ' <span class="badge badge-success"><i class="fas fa-hourglass-start"></i> ' + d['start_time'].slice(11,16) + '</span>': ''; |
| 122 | 122 | var unfocus = d['unfocus'] ? ' <span class="badge badge-danger">unfocus</span>' : ''; |
| 123 | - var area = ''; | |
| 124 | - if (d['start_time'] ) { | |
| 125 | - if (d['area'] > 75) | |
| 126 | - area = ' <span class="badge badge-success"><i class="fas fa-desktop"></i> ' + Math.round(d['area']) + '%</span>'; | |
| 127 | - else | |
| 128 | - area = ' <span class="badge badge-danger"><i class="fas fa-desktop"></i> ' + Math.round(d['area']) + '%</span>'; | |
| 129 | - }; | |
| 123 | + if (d['area'] > 75) | |
| 124 | + area = ' <span class="badge badge-success"><i class="fas fa-desktop"></i>' + Math.round(d['area']) + '%</span>'; | |
| 125 | + else | |
| 126 | + area = ' <span class="badge badge-danger"><i class="fas fa-desktop"></i> ' + Math.round(d['area']) + '%</span>'; | |
| 130 | 127 | var g = d['grades']; |
| 131 | 128 | |
| 132 | 129 | t[i] = []; |
| ... | ... | @@ -134,7 +131,7 @@ $(document).ready(function() { |
| 134 | 131 | t[i][1] = '<input type="checkbox" name="' + uid + '" value="true"' + checked + '> '; |
| 135 | 132 | t[i][2] = uid; |
| 136 | 133 | t[i][3] = d['name']; |
| 137 | - t[i][4] = password_defined + hora_inicio + area + unfocus; | |
| 134 | + t[i][4] = d['online'] ? hora_inicio + area + unfocus : ''; | |
| 138 | 135 | |
| 139 | 136 | var gbar = ''; |
| 140 | 137 | for (var j=0; j < g.length; j++) | ... | ... |
perguntations/templates/admin.html
| ... | ... | @@ -53,8 +53,8 @@ |
| 53 | 53 | Acções |
| 54 | 54 | </a> |
| 55 | 55 | <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownAluno"> |
| 56 | - <a class="dropdown-item" href="#" id="novo_aluno" data-toggle="modal" data-target="#novo_aluno_modal">Inserir novo aluno...</a> | |
| 57 | - <a class="dropdown-item" href="#" id="reset_password_menu" data-toggle="modal" data-target="#reset_password_modal">Reset password do aluno...</a> | |
| 56 | + <a class="dropdown-item" href="#" id="novo_aluno" data-toggle="modal" data-target="#novo_aluno_modal">Novo aluno...</a> | |
| 57 | + <a class="dropdown-item" href="#" id="reset_password_menu" data-toggle="modal" data-target="#reset_password_modal">Limpar password...</a> | |
| 58 | 58 | <a class="dropdown-item" href="#" id="allow_all">Autorizar todos</a> |
| 59 | 59 | <a class="dropdown-item" href="#" id="deny_all">Desautorizar todos</a> |
| 60 | 60 | <div class="dropdown-divider"></div> | ... | ... |
perguntations/test.py
| ... | ... | @@ -8,7 +8,7 @@ import json |
| 8 | 8 | import logging |
| 9 | 9 | from math import nan |
| 10 | 10 | |
| 11 | -# Logger configuration | |
| 11 | + | |
| 12 | 12 | logger = logging.getLogger(__name__) |
| 13 | 13 | |
| 14 | 14 | |
| ... | ... | @@ -19,7 +19,7 @@ class Test(dict): |
| 19 | 19 | ''' |
| 20 | 20 | |
| 21 | 21 | # ------------------------------------------------------------------------ |
| 22 | - def __init__(self, d): | |
| 22 | + def __init__(self, d: dict): | |
| 23 | 23 | super().__init__(d) |
| 24 | 24 | self['grade'] = nan |
| 25 | 25 | self['comment'] = '' |
| ... | ... | @@ -46,14 +46,14 @@ class Test(dict): |
| 46 | 46 | self['questions'][ref].set_answer(ans) |
| 47 | 47 | |
| 48 | 48 | # ------------------------------------------------------------------------ |
| 49 | - def submit(self, answers_dict) -> None: | |
| 49 | + def submit(self, answers: dict) -> None: | |
| 50 | 50 | ''' |
| 51 | 51 | Given a dictionary ans={'ref': 'some answer'} updates the answers of |
| 52 | 52 | multiple questions in the test. |
| 53 | 53 | Only affects the questions referred in the dictionary. |
| 54 | 54 | ''' |
| 55 | 55 | self['finish_time'] = datetime.now() |
| 56 | - for ref, ans in answers_dict.items(): | |
| 56 | + for ref, ans in answers.items(): | |
| 57 | 57 | self['questions'][ref].set_answer(ans) |
| 58 | 58 | self['state'] = 'SUBMITTED' |
| 59 | 59 | |
| ... | ... | @@ -93,9 +93,9 @@ class Test(dict): |
| 93 | 93 | self['grade'] = 0.0 |
| 94 | 94 | |
| 95 | 95 | # ------------------------------------------------------------------------ |
| 96 | - def save_json(self, pathfile) -> None: | |
| 96 | + def save_json(self, filename: str) -> None: | |
| 97 | 97 | '''save test in JSON format''' |
| 98 | - with open(pathfile, 'w') as file: | |
| 98 | + with open(filename, 'w', encoding='utf-8') as file: | |
| 99 | 99 | json.dump(self, file, indent=2, default=str) # str for datetime |
| 100 | 100 | |
| 101 | 101 | # ------------------------------------------------------------------------ | ... | ... |
perguntations/testfactory.py
| ... | ... | @@ -7,7 +7,7 @@ from os import path |
| 7 | 7 | import random |
| 8 | 8 | import logging |
| 9 | 9 | import re |
| 10 | -from typing import Any, Dict | |
| 10 | +from typing import TypedDict | |
| 11 | 11 | |
| 12 | 12 | # this project |
| 13 | 13 | from perguntations.questions import QFactory, QuestionException, QDict |
| ... | ... | @@ -17,6 +17,10 @@ from perguntations.tools import load_yaml |
| 17 | 17 | # Logger configuration |
| 18 | 18 | logger = logging.getLogger(__name__) |
| 19 | 19 | |
| 20 | +ConfigDict = TypedDict('ConfigDict', { | |
| 21 | + 'title': str | |
| 22 | + # TODO add other fields | |
| 23 | + }) | |
| 20 | 24 | |
| 21 | 25 | # ============================================================================ |
| 22 | 26 | class TestFactoryException(Exception): |
| ... | ... | @@ -32,7 +36,7 @@ class TestFactory(dict): |
| 32 | 36 | ''' |
| 33 | 37 | |
| 34 | 38 | # ------------------------------------------------------------------------ |
| 35 | - def __init__(self, conf: Dict[str, Any]) -> None: | |
| 39 | + def __init__(self, conf: ConfigDict) -> None: | |
| 36 | 40 | ''' |
| 37 | 41 | Loads configuration from yaml file, then overrides some configurations |
| 38 | 42 | using the conf argument. |
| ... | ... | @@ -47,7 +51,7 @@ class TestFactory(dict): |
| 47 | 51 | 'duration': 0, # 0=infinite |
| 48 | 52 | 'autosubmit': False, |
| 49 | 53 | 'autocorrect': True, |
| 50 | - 'debug': False, # FIXME not property of a test... | |
| 54 | + # 'debug': False, # FIXME not property of a test... | |
| 51 | 55 | 'show_ref': False, |
| 52 | 56 | }) |
| 53 | 57 | self.update(conf) |
| ... | ... | @@ -87,19 +91,19 @@ class TestFactory(dict): |
| 87 | 91 | logger.warning('Missing ref set to "%s"', question["ref"]) |
| 88 | 92 | |
| 89 | 93 | # check for duplicate refs |
| 90 | - if question['ref'] in self['question_factory']: | |
| 91 | - other = self['question_factory'][question['ref']] | |
| 94 | + qref = question['ref'] | |
| 95 | + if qref in self['question_factory']: | |
| 96 | + other = self['question_factory'][qref] | |
| 92 | 97 | otherfile = path.join(other.question['path'], |
| 93 | 98 | other.question['filename']) |
| 94 | - msg = (f'Duplicate reference "{question["ref"]}" in files ' | |
| 95 | - f'"{otherfile}" and "{fullpath}".') | |
| 99 | + msg = f'Duplicate "{qref}" in {otherfile} and {fullpath}' | |
| 96 | 100 | raise TestFactoryException(msg) |
| 97 | 101 | |
| 98 | 102 | # make factory only for the questions used in the test |
| 99 | - if question['ref'] in qrefs: | |
| 103 | + if qref in qrefs: | |
| 100 | 104 | question.update(zip(('path', 'filename', 'index'), |
| 101 | 105 | path.split(fullpath) + (i,))) |
| 102 | - self['question_factory'][question['ref']] = QFactory(QDict(question)) | |
| 106 | + self['question_factory'][qref] = QFactory(QDict(question)) | |
| 103 | 107 | |
| 104 | 108 | qmissing = qrefs.difference(set(self['question_factory'].keys())) |
| 105 | 109 | if qmissing: |
| ... | ... | @@ -137,7 +141,7 @@ class TestFactory(dict): |
| 137 | 141 | '''Answers directory must be writable''' |
| 138 | 142 | testfile = path.join(path.expanduser(self['answers_dir']), 'REMOVE-ME') |
| 139 | 143 | try: |
| 140 | - with open(testfile, 'w') as file: | |
| 144 | + with open(testfile, 'w', encoding='utf-8') as file: | |
| 141 | 145 | file.write('You can safely remove this file.') |
| 142 | 146 | except OSError as exc: |
| 143 | 147 | msg = f'Cannot write answers to directory "{self["answers_dir"]}"' |
| ... | ... | @@ -223,44 +227,44 @@ class TestFactory(dict): |
| 223 | 227 | raise TestFactoryException(msg) from exc |
| 224 | 228 | else: |
| 225 | 229 | logger.info('%4d. %s: Ok', i, qref) |
| 226 | - # logger.info(' generate Ok') | |
| 227 | - | |
| 228 | - if question['type'] in ('code', 'textarea'): | |
| 229 | - if 'tests_right' in question: | |
| 230 | - for tnum, right_answer in enumerate(question['tests_right']): | |
| 231 | - try: | |
| 232 | - question.set_answer(right_answer) | |
| 233 | - question.correct() | |
| 234 | - except Exception as exc: | |
| 235 | - msg = f'Failed to correct "{qref}"' | |
| 236 | - raise TestFactoryException(msg) from exc | |
| 237 | - | |
| 238 | - if question['grade'] == 1.0: | |
| 239 | - logger.info(' test %i Ok', tnum) | |
| 240 | - else: | |
| 241 | - logger.error(' TEST %i IS WRONG!!!', tnum) | |
| 242 | - elif 'tests_wrong' in question: | |
| 243 | - for tnum, wrong_answer in enumerate(question['tests_wrong']): | |
| 244 | - try: | |
| 245 | - question.set_answer(wrong_answer) | |
| 246 | - question.correct() | |
| 247 | - except Exception as exc: | |
| 248 | - msg = f'Failed to correct "{qref}"' | |
| 249 | - raise TestFactoryException(msg) from exc | |
| 250 | - | |
| 251 | - if question['grade'] < 1.0: | |
| 252 | - logger.info(' test %i Ok', tnum) | |
| 253 | - else: | |
| 254 | - logger.error(' TEST %i IS WRONG!!!', tnum) | |
| 255 | - else: | |
| 256 | - try: | |
| 257 | - question.set_answer('') | |
| 258 | - question.correct() | |
| 259 | - except Exception as exc: | |
| 260 | - msg = f'Failed to correct "{qref}"' | |
| 261 | - raise TestFactoryException(msg) from exc | |
| 262 | - else: | |
| 263 | - logger.info(' correct Ok but no tests to run') | |
| 230 | + | |
| 231 | + if question['type'] == 'textarea': | |
| 232 | + _runtests_textarea(qref, question) | |
| 233 | + # if 'tests_right' in question: | |
| 234 | + # for tnum, right_answer in enumerate(question['tests_right']): | |
| 235 | + # try: | |
| 236 | + # question.set_answer(right_answer) | |
| 237 | + # question.correct() | |
| 238 | + # except Exception as exc: | |
| 239 | + # msg = f'Failed to correct "{qref}"' | |
| 240 | + # raise TestFactoryException(msg) from exc | |
| 241 | + | |
| 242 | + # if question['grade'] == 1.0: | |
| 243 | + # logger.info(' test %i Ok', tnum) | |
| 244 | + # else: | |
| 245 | + # logger.error(' TEST %i IS WRONG!!!', tnum) | |
| 246 | + # elif 'tests_wrong' in question: | |
| 247 | + # for tnum, wrong_answer in enumerate(question['tests_wrong']): | |
| 248 | + # try: | |
| 249 | + # question.set_answer(wrong_answer) | |
| 250 | + # question.correct() | |
| 251 | + # except Exception as exc: | |
| 252 | + # msg = f'Failed to correct "{qref}"' | |
| 253 | + # raise TestFactoryException(msg) from exc | |
| 254 | + | |
| 255 | + # if question['grade'] < 1.0: | |
| 256 | + # logger.info(' test %i Ok', tnum) | |
| 257 | + # else: | |
| 258 | + # logger.error(' TEST %i IS WRONG!!!', tnum) | |
| 259 | + # else: | |
| 260 | + # try: | |
| 261 | + # question.set_answer('') | |
| 262 | + # question.correct() | |
| 263 | + # except Exception as exc: | |
| 264 | + # msg = f'Failed to correct "{qref}"' | |
| 265 | + # raise TestFactoryException(msg) from exc | |
| 266 | + # else: | |
| 267 | + # logger.info(' correct Ok but no tests to run') | |
| 264 | 268 | |
| 265 | 269 | # ------------------------------------------------------------------------ |
| 266 | 270 | async def generate(self): |
| ... | ... | @@ -322,9 +326,8 @@ class TestFactory(dict): |
| 322 | 326 | inherit = {'ref', 'title', 'database', 'answers_dir', |
| 323 | 327 | 'questions_dir', 'files', |
| 324 | 328 | 'duration', 'autosubmit', 'autocorrect', |
| 325 | - 'scale', 'show_points', | |
| 326 | - 'show_ref', 'debug', } | |
| 327 | - # NOT INCLUDED: testfile, allow_all, review | |
| 329 | + 'scale', 'show_points', 'show_ref'} | |
| 330 | + # NOT INCLUDED: testfile, allow_all, review, debug | |
| 328 | 331 | |
| 329 | 332 | return Test({'questions': questions, **{k:self[k] for k in inherit}}) |
| 330 | 333 | |
| ... | ... | @@ -332,3 +335,42 @@ class TestFactory(dict): |
| 332 | 335 | def __repr__(self): |
| 333 | 336 | testsettings = '\n'.join(f' {k:14s}: {v}' for k, v in self.items()) |
| 334 | 337 | return 'TestFactory({\n' + testsettings + '\n})' |
| 338 | + | |
| 339 | +# ============================================================================ | |
| 340 | +def _runtests_textarea(qref, question): | |
| 341 | + ''' | |
| 342 | + Checks if correction script works and runs tests if available | |
| 343 | + ''' | |
| 344 | + try: | |
| 345 | + question.set_answer('') | |
| 346 | + question.correct() | |
| 347 | + except Exception as exc: | |
| 348 | + msg = f'Failed to correct "{qref}"' | |
| 349 | + raise TestFactoryException(msg) from exc | |
| 350 | + logger.info(' correction works') | |
| 351 | + | |
| 352 | + for tnum, right_answer in enumerate(question.get('tests_right', {})): | |
| 353 | + try: | |
| 354 | + question.set_answer(right_answer) | |
| 355 | + question.correct() | |
| 356 | + except Exception as exc: | |
| 357 | + msg = f'Failed to correct "{qref}"' | |
| 358 | + raise TestFactoryException(msg) from exc | |
| 359 | + | |
| 360 | + if question['grade'] == 1.0: | |
| 361 | + logger.info(' tests_right[%i] Ok', tnum) | |
| 362 | + else: | |
| 363 | + logger.error(' tests_right[%i] FAILED!!!', tnum) | |
| 364 | + | |
| 365 | + for tnum, wrong_answer in enumerate(question.get('tests_wrong', {})): | |
| 366 | + try: | |
| 367 | + question.set_answer(wrong_answer) | |
| 368 | + question.correct() | |
| 369 | + except Exception as exc: | |
| 370 | + msg = f'Failed to correct "{qref}"' | |
| 371 | + raise TestFactoryException(msg) from exc | |
| 372 | + | |
| 373 | + if question['grade'] < 1.0: | |
| 374 | + logger.info(' tests_wrong[%i] Ok', tnum) | |
| 375 | + else: | |
| 376 | + logger.error(' tests_wrong[%i] FAILED!!!', tnum) | ... | ... |
perguntations/tools.py
| ... | ... | @@ -35,7 +35,7 @@ def run_script(script: str, |
| 35 | 35 | The script is run in another process but this function blocks waiting |
| 36 | 36 | for its termination. |
| 37 | 37 | ''' |
| 38 | - logger.info('run_script "%s"', script) | |
| 38 | + logger.debug('run_script "%s"', script) | |
| 39 | 39 | |
| 40 | 40 | output = None |
| 41 | 41 | script = path.expanduser(script) | ... | ... |
setup.py
| ... | ... | @@ -22,15 +22,14 @@ setup( |
| 22 | 22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", |
| 23 | 23 | packages=find_packages(), |
| 24 | 24 | include_package_data=True, # install files from MANIFEST.in |
| 25 | - python_requires='>=3.7.*', | |
| 25 | + python_requires='>=3.8.*', | |
| 26 | 26 | install_requires=[ |
| 27 | - 'tornado>=6.0', | |
| 28 | - 'mistune', | |
| 27 | + 'tornado>=6.1', | |
| 28 | + 'mistune<2.0', | |
| 29 | 29 | 'pyyaml>=5.1', |
| 30 | 30 | 'pygments', |
| 31 | - # 'sqlalchemy', | |
| 32 | - 'sqlalchemy[asyncio]', | |
| 33 | - 'aiosqlite', | |
| 31 | + 'sqlalchemy>=1.4', | |
| 32 | + # 'sqlalchemy[asyncio,mypy]>=1.4', | |
| 34 | 33 | 'bcrypt>=3.1' |
| 35 | 34 | ], |
| 36 | 35 | entry_points={ | ... | ... |