Commit 524327c3fcf1b0e3004b4eed3531ce0987cd7972
1 parent
2e43c7a2
Exists in
master
and in
1 other branch
pregenerates tests for all students
use raise from pattern to chain exception stack trace add function to print test (incomplete)
Showing
3 changed files
with
64 additions
and
27 deletions
Show diff stats
perguntations/app.py
| @@ -88,6 +88,7 @@ class App(): | @@ -88,6 +88,7 @@ class App(): | ||
| 88 | self.allowed = set() # '0' is hardcoded to allowed elsewhere | 88 | self.allowed = set() # '0' is hardcoded to allowed elsewhere |
| 89 | self.unfocus = set() # set of students that have no browser focus | 89 | self.unfocus = set() # set of students that have no browser focus |
| 90 | self.area = dict() # {uid: percent_area} | 90 | self.area = dict() # {uid: percent_area} |
| 91 | + self.pregenerated_tests = [] # list of tests to give to students | ||
| 91 | 92 | ||
| 92 | self._make_test_factory(conf) | 93 | self._make_test_factory(conf) |
| 93 | 94 | ||
| @@ -99,11 +100,15 @@ class App(): | @@ -99,11 +100,15 @@ class App(): | ||
| 99 | try: | 100 | try: |
| 100 | with self.db_session() as sess: | 101 | with self.db_session() as sess: |
| 101 | num = sess.query(Student).filter(Student.id != '0').count() | 102 | num = sess.query(Student).filter(Student.id != '0').count() |
| 102 | - except Exception: | ||
| 103 | - raise AppException(f'Database unusable {dbfile}.') | ||
| 104 | - | 103 | + except Exception as exc: |
| 104 | + raise AppException(f'Database unusable {dbfile}.') from exc | ||
| 105 | logger.info('Database "%s" has %s students.', dbfile, num) | 105 | logger.info('Database "%s" has %s students.', dbfile, num) |
| 106 | 106 | ||
| 107 | + # pre-generate tests | ||
| 108 | + logger.info('Generating tests for %d students:', num) | ||
| 109 | + self._pregenerate_tests(num) | ||
| 110 | + logger.info('Tests are ready.') | ||
| 111 | + | ||
| 107 | # command line option --allow-all | 112 | # command line option --allow-all |
| 108 | if conf['allow_all']: | 113 | if conf['allow_all']: |
| 109 | self.allow_all_students() | 114 | self.allow_all_students() |
| @@ -159,10 +164,12 @@ class App(): | @@ -159,10 +164,12 @@ class App(): | ||
| 159 | try: | 164 | try: |
| 160 | testconf = load_yaml(conf['testfile']) | 165 | testconf = load_yaml(conf['testfile']) |
| 161 | except Exception as exc: | 166 | except Exception as exc: |
| 162 | - logger.critical('Error loading test configuration YAML.') | ||
| 163 | - raise AppException(exc) | 167 | + msg = 'Error loading test configuration YAML.' |
| 168 | + logger.critical(msg) | ||
| 169 | + raise AppException(msg) from exc | ||
| 164 | 170 | ||
| 165 | - testconf.update(conf) # command line options override configuration | 171 | + # command line options override configuration |
| 172 | + testconf.update(conf) | ||
| 166 | 173 | ||
| 167 | # start test factory | 174 | # start test factory |
| 168 | logger.info('Making test factory...') | 175 | logger.info('Making test factory...') |
| @@ -170,23 +177,38 @@ class App(): | @@ -170,23 +177,38 @@ class App(): | ||
| 170 | self.testfactory = TestFactory(testconf) | 177 | self.testfactory = TestFactory(testconf) |
| 171 | except TestFactoryException as exc: | 178 | except TestFactoryException as exc: |
| 172 | logger.critical(exc) | 179 | logger.critical(exc) |
| 173 | - raise AppException('Failed to create test factory!') | 180 | + raise AppException('Failed to create test factory!') from exc |
| 174 | 181 | ||
| 175 | logger.info('Test factory ready. No errors found.') | 182 | logger.info('Test factory ready. No errors found.') |
| 176 | 183 | ||
| 177 | # ------------------------------------------------------------------------ | 184 | # ------------------------------------------------------------------------ |
| 185 | + def _pregenerate_tests(self, n): | ||
| 186 | + for _ in range(n): | ||
| 187 | + event_loop = asyncio.get_event_loop() | ||
| 188 | + test = event_loop.run_until_complete(self.testfactory.generate()) | ||
| 189 | + self.pregenerated_tests.append(test) | ||
| 190 | + print(test) | ||
| 191 | + | ||
| 192 | + # ------------------------------------------------------------------------ | ||
| 178 | async def generate_test(self, uid): | 193 | async def generate_test(self, uid): |
| 179 | '''generate a test for a given student''' | 194 | '''generate a test for a given student''' |
| 180 | if uid in self.online: | 195 | if uid in self.online: |
| 181 | - logger.info('"%s" generating new test.', uid) | 196 | + try: |
| 197 | + test = self.pregenerated_tests.pop() | ||
| 198 | + except IndexError: | ||
| 199 | + logger.info('"%s" generating new test.', uid) | ||
| 200 | + test = await self.testfactory.generate() # student_id) FIXME | ||
| 201 | + else: | ||
| 202 | + logger.info('"%s" using pregenerated test.', uid) | ||
| 203 | + | ||
| 182 | student_id = self.online[uid]['student'] # {number, name} | 204 | student_id = self.online[uid]['student'] # {number, name} |
| 183 | - test = await self.testfactory.generate(student_id) | 205 | + test.start(student_id) |
| 184 | self.online[uid]['test'] = test | 206 | self.online[uid]['test'] = test |
| 185 | logger.info('"%s" test is ready.', uid) | 207 | logger.info('"%s" test is ready.', uid) |
| 186 | return self.online[uid]['test'] | 208 | return self.online[uid]['test'] |
| 187 | 209 | ||
| 188 | - # this implies an error in the code. should never be here! | ||
| 189 | - logger.critical('"%s" offline, can\'t generate test', uid) | 210 | + # this implies an error in the program, code should be unreachable! |
| 211 | + logger.critical('"%s" is offline, can\'t generate test', uid) | ||
| 190 | 212 | ||
| 191 | # ------------------------------------------------------------------------ | 213 | # ------------------------------------------------------------------------ |
| 192 | async def correct_test(self, uid, ans): | 214 | async def correct_test(self, uid, ans): |
| @@ -410,7 +432,8 @@ class App(): | @@ -410,7 +432,8 @@ class App(): | ||
| 410 | 432 | ||
| 411 | def allow_all_students(self): | 433 | def allow_all_students(self): |
| 412 | '''allow all students to login''' | 434 | '''allow all students to login''' |
| 413 | - self.allowed.update(s[0] for s in self._get_all_students()) | 435 | + all_students = self._get_all_students() |
| 436 | + self.allowed.update(s[0] for s in all_students) | ||
| 414 | logger.info('Allowed all students.') | 437 | logger.info('Allowed all students.') |
| 415 | 438 | ||
| 416 | def deny_all_students(self): | 439 | def deny_all_students(self): |
perguntations/serve.py
| @@ -383,6 +383,7 @@ class TestHandler(BaseHandler): | @@ -383,6 +383,7 @@ class TestHandler(BaseHandler): | ||
| 383 | ''' | 383 | ''' |
| 384 | 384 | ||
| 385 | _templates = { | 385 | _templates = { |
| 386 | + # -- question templates -- | ||
| 386 | 'radio': 'question-radio.html', | 387 | 'radio': 'question-radio.html', |
| 387 | 'checkbox': 'question-checkbox.html', | 388 | 'checkbox': 'question-checkbox.html', |
| 388 | 'text': 'question-text.html', | 389 | 'text': 'question-text.html', |
| @@ -406,6 +407,7 @@ class TestHandler(BaseHandler): | @@ -406,6 +407,7 @@ class TestHandler(BaseHandler): | ||
| 406 | test = self.testapp.get_student_test(uid) # reloading returns same test | 407 | test = self.testapp.get_student_test(uid) # reloading returns same test |
| 407 | if test is None: | 408 | if test is None: |
| 408 | test = await self.testapp.generate_test(uid) | 409 | test = await self.testapp.generate_test(uid) |
| 410 | + | ||
| 409 | self.render('test.html', t=test, md=md_to_html, templ=self._templates) | 411 | self.render('test.html', t=test, md=md_to_html, templ=self._templates) |
| 410 | 412 | ||
| 411 | # --- POST | 413 | # --- POST |
perguntations/test.py
| @@ -152,9 +152,9 @@ class TestFactory(dict): | @@ -152,9 +152,9 @@ class TestFactory(dict): | ||
| 152 | try: | 152 | try: |
| 153 | with open(testfile, 'w') as file: | 153 | with open(testfile, 'w') as file: |
| 154 | file.write('You can safely remove this file.') | 154 | file.write('You can safely remove this file.') |
| 155 | - except OSError: | 155 | + except OSError as exc: |
| 156 | msg = f'Cannot write answers to directory "{self["answers_dir"]}"' | 156 | msg = f'Cannot write answers to directory "{self["answers_dir"]}"' |
| 157 | - raise TestFactoryException(msg) | 157 | + raise TestFactoryException(msg) from exc |
| 158 | 158 | ||
| 159 | def check_questions_directory(self): | 159 | def check_questions_directory(self): |
| 160 | '''Check if questions directory is missing or not accessible.''' | 160 | '''Check if questions directory is missing or not accessible.''' |
| @@ -223,16 +223,16 @@ class TestFactory(dict): | @@ -223,16 +223,16 @@ class TestFactory(dict): | ||
| 223 | self.check_grade_scaling() | 223 | self.check_grade_scaling() |
| 224 | 224 | ||
| 225 | # ------------------------------------------------------------------------ | 225 | # ------------------------------------------------------------------------ |
| 226 | - async def generate(self, student): | 226 | + async def generate(self): #, student): |
| 227 | ''' | 227 | ''' |
| 228 | Given a dictionary with a student dict {'name':'john', 'number': 123} | 228 | Given a dictionary with a student dict {'name':'john', 'number': 123} |
| 229 | returns instance of Test() for that particular student | 229 | returns instance of Test() for that particular student |
| 230 | ''' | 230 | ''' |
| 231 | 231 | ||
| 232 | # make list of questions | 232 | # make list of questions |
| 233 | - test = [] | ||
| 234 | - qnum = 1 # track question number | ||
| 235 | - nerr = 0 # count errors generating questions | 233 | + questions = [] |
| 234 | + qnum = 1 # track question number | ||
| 235 | + nerr = 0 # count errors during questions generation | ||
| 236 | 236 | ||
| 237 | for qlist in self['questions']: | 237 | for qlist in self['questions']: |
| 238 | # choose one question variant | 238 | # choose one question variant |
| @@ -255,28 +255,28 @@ class TestFactory(dict): | @@ -255,28 +255,28 @@ class TestFactory(dict): | ||
| 255 | question['number'] = qnum # counter for non informative panels | 255 | question['number'] = qnum # counter for non informative panels |
| 256 | qnum += 1 | 256 | qnum += 1 |
| 257 | 257 | ||
| 258 | - test.append(question) | 258 | + questions.append(question) |
| 259 | 259 | ||
| 260 | # setup scale | 260 | # setup scale |
| 261 | - total_points = sum(q['points'] for q in test) | 261 | + total_points = sum(q['points'] for q in questions) |
| 262 | 262 | ||
| 263 | if total_points > 0: | 263 | if total_points > 0: |
| 264 | # normalize question points to scale | 264 | # normalize question points to scale |
| 265 | if self['scale'] is not None: | 265 | if self['scale'] is not None: |
| 266 | scale_min, scale_max = self['scale'] | 266 | scale_min, scale_max = self['scale'] |
| 267 | - for question in test: | 267 | + for question in questions: |
| 268 | question['points'] *= (scale_max - scale_min) / total_points | 268 | question['points'] *= (scale_max - scale_min) / total_points |
| 269 | else: | 269 | else: |
| 270 | self['scale'] = [0, total_points] | 270 | self['scale'] = [0, total_points] |
| 271 | else: | 271 | else: |
| 272 | logger.warning('Total points is **ZERO**.') | 272 | logger.warning('Total points is **ZERO**.') |
| 273 | if self['scale'] is None: | 273 | if self['scale'] is None: |
| 274 | - self['scale'] = [0, 20] | 274 | + self['scale'] = [0, 20] # default |
| 275 | 275 | ||
| 276 | if nerr > 0: | 276 | if nerr > 0: |
| 277 | logger.error('%s errors found!', nerr) | 277 | logger.error('%s errors found!', nerr) |
| 278 | 278 | ||
| 279 | - # these will be copied to the test instance | 279 | + # copy these from the test configuratoin to each test instance |
| 280 | inherit = {'ref', 'title', 'database', 'answers_dir', | 280 | inherit = {'ref', 'title', 'database', 'answers_dir', |
| 281 | 'questions_dir', 'files', | 281 | 'questions_dir', 'files', |
| 282 | 'duration', 'autosubmit', | 282 | 'duration', 'autosubmit', |
| @@ -284,9 +284,7 @@ class TestFactory(dict): | @@ -284,9 +284,7 @@ class TestFactory(dict): | ||
| 284 | 'show_ref', 'debug', } | 284 | 'show_ref', 'debug', } |
| 285 | # NOT INCLUDED: testfile, allow_all, review | 285 | # NOT INCLUDED: testfile, allow_all, review |
| 286 | 286 | ||
| 287 | - return Test({ | ||
| 288 | - **{'student': student, 'questions': test}, | ||
| 289 | - **{k:self[k] for k in inherit}}) | 287 | + return Test({'questions': questions, **{k:self[k] for k in inherit}}) |
| 290 | 288 | ||
| 291 | # ------------------------------------------------------------------------ | 289 | # ------------------------------------------------------------------------ |
| 292 | def __repr__(self): | 290 | def __repr__(self): |
| @@ -303,6 +301,13 @@ class Test(dict): | @@ -303,6 +301,13 @@ class Test(dict): | ||
| 303 | # ------------------------------------------------------------------------ | 301 | # ------------------------------------------------------------------------ |
| 304 | def __init__(self, d): | 302 | def __init__(self, d): |
| 305 | super().__init__(d) | 303 | super().__init__(d) |
| 304 | + | ||
| 305 | + # ------------------------------------------------------------------------ | ||
| 306 | + def start(self, student): | ||
| 307 | + ''' | ||
| 308 | + Write student id in the test and register start time | ||
| 309 | + ''' | ||
| 310 | + self['student'] = student | ||
| 306 | self['start_time'] = datetime.now() | 311 | self['start_time'] = datetime.now() |
| 307 | self['finish_time'] = None | 312 | self['finish_time'] = None |
| 308 | self['state'] = 'ACTIVE' | 313 | self['state'] = 'ACTIVE' |
| @@ -346,5 +351,12 @@ class Test(dict): | @@ -346,5 +351,12 @@ class Test(dict): | ||
| 346 | self['finish_time'] = datetime.now() | 351 | self['finish_time'] = datetime.now() |
| 347 | self['state'] = 'QUIT' | 352 | self['state'] = 'QUIT' |
| 348 | self['grade'] = 0.0 | 353 | self['grade'] = 0.0 |
| 349 | - logger.info('Student %s: gave up.', self["student"]["number"]) | 354 | + # logger.info('Student %s: gave up.', self["student"]["number"]) |
| 350 | return self['grade'] | 355 | return self['grade'] |
| 356 | + | ||
| 357 | + # ------------------------------------------------------------------------ | ||
| 358 | + def __str__(self): | ||
| 359 | + return ('Test:\n' | ||
| 360 | + f' student: {self.get("student", "--")}\n' | ||
| 361 | + f' start_time: {self.get("start_time", "--")}\n' | ||
| 362 | + f' questions: {", ".join(q["ref"] for q in self["questions"])}\n') |