diff --git a/BUGS.md b/BUGS.md index 49e3913..2ac7692 100644 --- a/BUGS.md +++ b/BUGS.md @@ -2,7 +2,6 @@ # BUGS - correct devia poder ser corrido mais que uma vez (por exemplo para alterar cotacoes, corrigir perguntas) -- nao esta a mostrar imagens?? internal server error? - guardar testes em JSON assim que sao atribuidos aos alunos (ou guardados inicialmente com um certo nome, e atribuidos posteriormente ao aluno). - cookies existe um perguntations_user e um user. De onde vem o user? - QuestionCode falta reportar nos comments os vários erros que podem ocorrer (timeout, etc) diff --git a/demo/questions/questions-tutorial.yaml b/demo/questions/questions-tutorial.yaml index e6292e8..4bc9897 100644 --- a/demo/questions/questions-tutorial.yaml +++ b/demo/questions/questions-tutorial.yaml @@ -431,6 +431,10 @@ pode estar previamente preenchida como neste caso (use `answer: texto`). correct: correct/correct-question.py timeout: 5 + tests_right: + - 'red green blue' + # tests_wrong: + # - 'blue gray yellow' # --------------------------------------------------------------------------- - type: information diff --git a/mypy.ini b/mypy.ini index ed65b3b..84e7e80 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,14 +1,14 @@ [mypy] -python_version = 3.8 +python_version = 3.9 -[mypy-setuptools.*] -ignore_missing_imports = True +; [mypy-setuptools.*] +; ignore_missing_imports = True -[mypy-sqlalchemy.*] -ignore_missing_imports = True +; [mypy-sqlalchemy.*] +; ignore_missing_imports = True -[mypy-pygments.*] -ignore_missing_imports = True +; [mypy-pygments.*] +; ignore_missing_imports = True -[mypy-mistune.*] -ignore_missing_imports = True +; [mypy-mistune.*] +; ignore_missing_imports = True diff --git a/perguntations/app.py b/perguntations/app.py index 82dab7e..c7d9f90 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -97,8 +97,20 @@ class App(): raise AppException(msg) from None logger.info('Database has %d students.', len(dbstudents)) - self._students = {uid: {'name': name, 'state': 'offline', 'test': None} - for uid, name in dbstudents} + self._students = {uid: { + 'name': name, + 'state': 'offline', + 'test': None, + } for uid, name in dbstudents} + + # ------------------------------------------------------------------------ + async def _assign_tests(self) -> None: + '''Generate tests for all students that don't yet have a test''' + logger.info('Generating tests...') + for student in self._students.values(): + if student.get('test', None) is None: + student['test'] = await self._testfactory.generate() + logger.info('Tests assigned to all students') # ------------------------------------------------------------------------ async def login(self, uid: str, password: str, headers: dict) -> Optional[str]: @@ -114,6 +126,7 @@ class App(): logger.warning('"%s" does not exist', uid) return 'nonexistent' + if uid != '0' and self._students[uid]['state'] != 'allowed': logger.warning('"%s" login not allowed', uid) return 'not allowed' @@ -127,16 +140,16 @@ class App(): # success if uid == '0': logger.info('Admin login from %s', headers['remote_ip']) - return - - # FIXME this should probably be done elsewhere - test = await self._testfactory.generate() - test.start(uid) - self._students[uid]['test'] = test - - self._students[uid]['state'] = 'waiting' - self._students[uid]['headers'] = headers - logger.info('"%s" login from %s.', uid, headers['remote_ip']) + await self._assign_tests() + else: + student = self._students[uid] + student['test'].start(uid) + student['state'] = 'online' + student['headers'] = headers + student['unfocus'] = False + student['area'] = 1.0 + logger.info('"%s" login from %s.', uid, headers['remote_ip']) + return None # ------------------------------------------------------------------------ async def set_password(self, uid: str, password: str) -> None: @@ -151,10 +164,14 @@ class App(): # ------------------------------------------------------------------------ def logout(self, uid: str) -> None: '''student logout''' - if uid in self._students: - self._students[uid]['test'] = None - self._students[uid]['state'] = 'offline' - logger.info('"%s" logged out.', uid) + student = self._students.get(uid, None) + if student is not None: + # student['test'] = None + student['state'] = 'offline' + student.pop('headers', None) + student.pop('unfocus', None) + student.pop('area', None) + logger.info('"%s" logged out.', uid) # ------------------------------------------------------------------------ def _make_test_factory(self, filename: str) -> None: @@ -187,9 +204,12 @@ class App(): ans is a dictionary {question_index: answer, ...} with the answers for the complete test. For example: {0:'hello', 1:[1,2]} ''' - logger.info('"%s" submitted %d answers.', uid, len(ans)) + if self._students[uid]['state'] != 'online': + logger.warning('"%s" INVALID SUBMISSION! STUDENT NOT ONLINE', uid) + return # --- submit answers and correct test + logger.info('"%s" submitted %d answers.', uid, len(ans)) test = self._students[uid]['test'] test.submit(ans) @@ -236,7 +256,6 @@ class App(): logger.info('"%s" database updated.', uid) # # ------------------------------------------------------------------------ - # FIXME not working # def _correct_tests(self): # with Session(self._engine, future=True) as session: # # Find which tests have to be corrected @@ -325,15 +344,15 @@ class App(): # return test # ------------------------------------------------------------------------ - def event_test(self, uid, cmd, value): + def register_event(self, uid, cmd, value): '''handles browser events the occur during the test''' - # if cmd == 'focus': - # if value: - # self._focus_student(uid) - # else: - # self._unfocus_student(uid) - # elif cmd == 'size': - # self._set_screen_area(uid, value) + if cmd == 'focus': + if value: + self._focus_student(uid) + else: + self._unfocus_student(uid) + elif cmd == 'size': + self._set_screen_area(uid, value) # ======================================================================== # GETTERS @@ -349,7 +368,7 @@ class App(): # ------------------------------------------------------------------------ def get_test_config(self) -> dict: - '''return brief test configuration''' + '''return brief test configuration to use as header in /admin''' return {'title': self._testfactory['title'], 'ref': self._testfactory['ref'], 'filename': self._testfactory['testfile'], @@ -426,15 +445,14 @@ class App(): # ------------------------------------------------------------------------ def get_students_state(self) -> list: - '''get list of states of every student''' + '''get list of states of every student to show in /admin page''' return [{ 'uid': uid, 'name': student['name'], 'allowed': student['state'] == 'allowed', 'online': student['state'] == 'online', - # 'start_time': student.get('test', {}).get('start_time', ''), - # 'password_defined': False, #pw != '', - # 'unfocus': False, - # 'area': '0.89', + 'start_time': student.get('test', {}).get('start_time', ''), + 'unfocus': student.get('unfocus', False), + 'area': student.get('area', 1.0), 'grades': self.get_grades(uid, self._testfactory['ref']) } for uid, student in self._students.items()] @@ -508,7 +526,7 @@ class App(): student['state'] = 'offline' # ------------------------------------------------------------------------ - def insert_new_student(self, uid: str, name: str) -> None: + async def insert_new_student(self, uid: str, name: str) -> None: '''insert new student into the database''' with Session(self._engine, future=True) as session: try: @@ -519,11 +537,16 @@ class App(): session.rollback() return logger.info('New student added: %s %s', uid, name) - self._students[uid] = {'name': name, 'state': 'offline', 'test': None} + self._students[uid] = { + 'name': name, + 'state': 'offline', + 'test': await self._testfactory.generate(), + } # ------------------------------------------------------------------------ def allow_from_list(self, filename: str) -> None: '''allow students listed in text file (one number per line)''' + # parse list of students to allow (one number per line) try: with open(filename, 'r', encoding='utf-8') as file: allowed = {line.strip() for line in file} @@ -533,6 +556,7 @@ class App(): logger.critical(error_msg) raise AppException(error_msg) from exc + # update allowed state (missing are students allowed that don't exist) missing = 0 for uid in allowed: try: @@ -545,20 +569,23 @@ class App(): if missing: logger.warning(' %d missing!', missing) - # def _focus_student(self, uid): - # '''set student in focus state''' - # self.unfocus.discard(uid) - # logger.info('"%s" focus', uid) - - # def _unfocus_student(self, uid): - # '''set student in unfocus state''' - # self.unfocus.add(uid) - # logger.info('"%s" unfocus', uid) - - # def _set_screen_area(self, uid, sizes): - # '''set current browser area as detected in resize event''' - # scr_y, scr_x, win_y, win_x = sizes - # area = win_x * win_y / (scr_x * scr_y) * 100 - # self.area[uid] = area - # logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', - # uid, area, win_x, win_y, scr_x, scr_y) + # ------------------------------------------------------------------------ + def _focus_student(self, uid): + '''set student in focus state''' + self._students[uid]['unfocus'] = False + logger.info('"%s" focus', uid) + + # ------------------------------------------------------------------------ + def _unfocus_student(self, uid): + '''set student in unfocus state''' + self._students[uid]['unfocus'] = True + logger.info('"%s" unfocus', uid) + + # ------------------------------------------------------------------------ + def _set_screen_area(self, uid, sizes): + '''set current browser area as detected in resize event''' + scr_y, scr_x, win_y, win_x = sizes + area = win_x * win_y / (scr_x * scr_y) * 100 + self._students[uid]['area'] = area + logger.info('"%s" area=%g%%, window=%dx%d, screen=%dx%d', + uid, area, win_x, win_y, scr_x, scr_y) diff --git a/perguntations/main.py b/perguntations/main.py index 67fbbf0..cf9c00c 100644 --- a/perguntations/main.py +++ b/perguntations/main.py @@ -76,7 +76,6 @@ def get_logger_config(debug=False) -> dict: if debug: level = 'DEBUG' - # fmt = '%(asctime)s | %(levelname)-8s | %(module)-16s:%(lineno)4d | %(thread)d | %(message)s' fmt = '%(asctime)s %(levelname)-8s %(module)-12s%(lineno)4d| %(message)s' dateformat = '' else: diff --git a/perguntations/models.py b/perguntations/models.py index c28609e..d1c6c07 100644 --- a/perguntations/models.py +++ b/perguntations/models.py @@ -3,17 +3,15 @@ perguntations/models.py SQLAlchemy ORM ''' +from typing import Any from sqlalchemy import Column, ForeignKey, Integer, Float, String from sqlalchemy.orm import declarative_base, relationship -# ============================================================================ -# Declare ORM -# FIXME Any is a workaround for mypy static type checking (see https://github.com/python/mypy/issues/6372) -from typing import Any +# FIXME Any is a workaround for static type checking +# (https://github.com/python/mypy/issues/6372) Base: Any = declarative_base() -# Base = declarative_base() # ---------------------------------------------------------------------------- diff --git a/perguntations/parser_markdown.py b/perguntations/parser_markdown.py index e04dbbd..f2cd3c8 100644 --- a/perguntations/parser_markdown.py +++ b/perguntations/parser_markdown.py @@ -137,9 +137,9 @@ class HighlightRenderer(mistune.Renderer): return '