diff --git a/perguntations/app.py b/perguntations/app.py index debc8a3..8068013 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -88,6 +88,7 @@ class App(): self.allowed = set() # '0' is hardcoded to allowed elsewhere self.unfocus = set() # set of students that have no browser focus self.area = dict() # {uid: percent_area} + self.pregenerated_tests = [] # list of tests to give to students self._make_test_factory(conf) @@ -99,11 +100,15 @@ class App(): try: with self.db_session() as sess: num = sess.query(Student).filter(Student.id != '0').count() - except Exception: - raise AppException(f'Database unusable {dbfile}.') - + except Exception as exc: + raise AppException(f'Database unusable {dbfile}.') from exc logger.info('Database "%s" has %s students.', dbfile, num) + # pre-generate tests + logger.info('Generating tests for %d students:', num) + self._pregenerate_tests(num) + logger.info('Tests are ready.') + # command line option --allow-all if conf['allow_all']: self.allow_all_students() @@ -159,10 +164,12 @@ class App(): try: testconf = load_yaml(conf['testfile']) except Exception as exc: - logger.critical('Error loading test configuration YAML.') - raise AppException(exc) + msg = 'Error loading test configuration YAML.' + logger.critical(msg) + raise AppException(msg) from exc - testconf.update(conf) # command line options override configuration + # command line options override configuration + testconf.update(conf) # start test factory logger.info('Making test factory...') @@ -170,23 +177,38 @@ class App(): self.testfactory = TestFactory(testconf) except TestFactoryException as exc: logger.critical(exc) - raise AppException('Failed to create test factory!') + raise AppException('Failed to create test factory!') from exc logger.info('Test factory ready. No errors found.') # ------------------------------------------------------------------------ + def _pregenerate_tests(self, n): + for _ in range(n): + event_loop = asyncio.get_event_loop() + test = event_loop.run_until_complete(self.testfactory.generate()) + self.pregenerated_tests.append(test) + print(test) + + # ------------------------------------------------------------------------ async def generate_test(self, uid): '''generate a test for a given student''' if uid in self.online: - logger.info('"%s" generating new test.', uid) + try: + test = self.pregenerated_tests.pop() + except IndexError: + logger.info('"%s" generating new test.', uid) + test = await self.testfactory.generate() # student_id) FIXME + else: + logger.info('"%s" using pregenerated test.', uid) + student_id = self.online[uid]['student'] # {number, name} - test = await self.testfactory.generate(student_id) + test.start(student_id) self.online[uid]['test'] = test logger.info('"%s" test is ready.', uid) return self.online[uid]['test'] - # this implies an error in the code. should never be here! - logger.critical('"%s" offline, can\'t generate test', uid) + # this implies an error in the program, code should be unreachable! + logger.critical('"%s" is offline, can\'t generate test', uid) # ------------------------------------------------------------------------ async def correct_test(self, uid, ans): @@ -410,7 +432,8 @@ class App(): def allow_all_students(self): '''allow all students to login''' - self.allowed.update(s[0] for s in self._get_all_students()) + all_students = self._get_all_students() + self.allowed.update(s[0] for s in all_students) logger.info('Allowed all students.') def deny_all_students(self): diff --git a/perguntations/serve.py b/perguntations/serve.py index a02c663..bafce34 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -383,6 +383,7 @@ class TestHandler(BaseHandler): ''' _templates = { + # -- question templates -- 'radio': 'question-radio.html', 'checkbox': 'question-checkbox.html', 'text': 'question-text.html', @@ -406,6 +407,7 @@ class TestHandler(BaseHandler): test = self.testapp.get_student_test(uid) # reloading returns same test if test is None: test = await self.testapp.generate_test(uid) + self.render('test.html', t=test, md=md_to_html, templ=self._templates) # --- POST diff --git a/perguntations/test.py b/perguntations/test.py index 3f53e31..4f248c7 100644 --- a/perguntations/test.py +++ b/perguntations/test.py @@ -152,9 +152,9 @@ class TestFactory(dict): try: with open(testfile, 'w') as file: file.write('You can safely remove this file.') - except OSError: + except OSError as exc: msg = f'Cannot write answers to directory "{self["answers_dir"]}"' - raise TestFactoryException(msg) + raise TestFactoryException(msg) from exc def check_questions_directory(self): '''Check if questions directory is missing or not accessible.''' @@ -223,16 +223,16 @@ class TestFactory(dict): self.check_grade_scaling() # ------------------------------------------------------------------------ - async def generate(self, student): + async def generate(self): #, student): ''' Given a dictionary with a student dict {'name':'john', 'number': 123} returns instance of Test() for that particular student ''' # make list of questions - test = [] - qnum = 1 # track question number - nerr = 0 # count errors generating questions + questions = [] + qnum = 1 # track question number + nerr = 0 # count errors during questions generation for qlist in self['questions']: # choose one question variant @@ -255,28 +255,28 @@ class TestFactory(dict): question['number'] = qnum # counter for non informative panels qnum += 1 - test.append(question) + questions.append(question) # setup scale - total_points = sum(q['points'] for q in test) + total_points = sum(q['points'] for q in questions) if total_points > 0: # normalize question points to scale if self['scale'] is not None: scale_min, scale_max = self['scale'] - for question in test: + for question in questions: question['points'] *= (scale_max - scale_min) / total_points else: self['scale'] = [0, total_points] else: logger.warning('Total points is **ZERO**.') if self['scale'] is None: - self['scale'] = [0, 20] + self['scale'] = [0, 20] # default if nerr > 0: logger.error('%s errors found!', nerr) - # these will be copied to the test instance + # copy these from the test configuratoin to each test instance inherit = {'ref', 'title', 'database', 'answers_dir', 'questions_dir', 'files', 'duration', 'autosubmit', @@ -284,9 +284,7 @@ class TestFactory(dict): 'show_ref', 'debug', } # NOT INCLUDED: testfile, allow_all, review - return Test({ - **{'student': student, 'questions': test}, - **{k:self[k] for k in inherit}}) + return Test({'questions': questions, **{k:self[k] for k in inherit}}) # ------------------------------------------------------------------------ def __repr__(self): @@ -303,6 +301,13 @@ class Test(dict): # ------------------------------------------------------------------------ def __init__(self, d): super().__init__(d) + + # ------------------------------------------------------------------------ + def start(self, student): + ''' + Write student id in the test and register start time + ''' + self['student'] = student self['start_time'] = datetime.now() self['finish_time'] = None self['state'] = 'ACTIVE' @@ -346,5 +351,12 @@ class Test(dict): self['finish_time'] = datetime.now() self['state'] = 'QUIT' self['grade'] = 0.0 - logger.info('Student %s: gave up.', self["student"]["number"]) + # logger.info('Student %s: gave up.', self["student"]["number"]) return self['grade'] + + # ------------------------------------------------------------------------ + def __str__(self): + return ('Test:\n' + f' student: {self.get("student", "--")}\n' + f' start_time: {self.get("start_time", "--")}\n' + f' questions: {", ".join(q["ref"] for q in self["questions"])}\n') -- libgit2 0.21.2