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 | 88 | self.allowed = set() # '0' is hardcoded to allowed elsewhere |
| 89 | 89 | self.unfocus = set() # set of students that have no browser focus |
| 90 | 90 | self.area = dict() # {uid: percent_area} |
| 91 | + self.pregenerated_tests = [] # list of tests to give to students | |
| 91 | 92 | |
| 92 | 93 | self._make_test_factory(conf) |
| 93 | 94 | |
| ... | ... | @@ -99,11 +100,15 @@ class App(): |
| 99 | 100 | try: |
| 100 | 101 | with self.db_session() as sess: |
| 101 | 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 | 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 | 112 | # command line option --allow-all |
| 108 | 113 | if conf['allow_all']: |
| 109 | 114 | self.allow_all_students() |
| ... | ... | @@ -159,10 +164,12 @@ class App(): |
| 159 | 164 | try: |
| 160 | 165 | testconf = load_yaml(conf['testfile']) |
| 161 | 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 | 174 | # start test factory |
| 168 | 175 | logger.info('Making test factory...') |
| ... | ... | @@ -170,23 +177,38 @@ class App(): |
| 170 | 177 | self.testfactory = TestFactory(testconf) |
| 171 | 178 | except TestFactoryException as exc: |
| 172 | 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 | 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 | 193 | async def generate_test(self, uid): |
| 179 | 194 | '''generate a test for a given student''' |
| 180 | 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 | 204 | student_id = self.online[uid]['student'] # {number, name} |
| 183 | - test = await self.testfactory.generate(student_id) | |
| 205 | + test.start(student_id) | |
| 184 | 206 | self.online[uid]['test'] = test |
| 185 | 207 | logger.info('"%s" test is ready.', uid) |
| 186 | 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 | 214 | async def correct_test(self, uid, ans): |
| ... | ... | @@ -410,7 +432,8 @@ class App(): |
| 410 | 432 | |
| 411 | 433 | def allow_all_students(self): |
| 412 | 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 | 437 | logger.info('Allowed all students.') |
| 415 | 438 | |
| 416 | 439 | def deny_all_students(self): | ... | ... |
perguntations/serve.py
| ... | ... | @@ -383,6 +383,7 @@ class TestHandler(BaseHandler): |
| 383 | 383 | ''' |
| 384 | 384 | |
| 385 | 385 | _templates = { |
| 386 | + # -- question templates -- | |
| 386 | 387 | 'radio': 'question-radio.html', |
| 387 | 388 | 'checkbox': 'question-checkbox.html', |
| 388 | 389 | 'text': 'question-text.html', |
| ... | ... | @@ -406,6 +407,7 @@ class TestHandler(BaseHandler): |
| 406 | 407 | test = self.testapp.get_student_test(uid) # reloading returns same test |
| 407 | 408 | if test is None: |
| 408 | 409 | test = await self.testapp.generate_test(uid) |
| 410 | + | |
| 409 | 411 | self.render('test.html', t=test, md=md_to_html, templ=self._templates) |
| 410 | 412 | |
| 411 | 413 | # --- POST | ... | ... |
perguntations/test.py
| ... | ... | @@ -152,9 +152,9 @@ class TestFactory(dict): |
| 152 | 152 | try: |
| 153 | 153 | with open(testfile, 'w') as file: |
| 154 | 154 | file.write('You can safely remove this file.') |
| 155 | - except OSError: | |
| 155 | + except OSError as exc: | |
| 156 | 156 | msg = f'Cannot write answers to directory "{self["answers_dir"]}"' |
| 157 | - raise TestFactoryException(msg) | |
| 157 | + raise TestFactoryException(msg) from exc | |
| 158 | 158 | |
| 159 | 159 | def check_questions_directory(self): |
| 160 | 160 | '''Check if questions directory is missing or not accessible.''' |
| ... | ... | @@ -223,16 +223,16 @@ class TestFactory(dict): |
| 223 | 223 | self.check_grade_scaling() |
| 224 | 224 | |
| 225 | 225 | # ------------------------------------------------------------------------ |
| 226 | - async def generate(self, student): | |
| 226 | + async def generate(self): #, student): | |
| 227 | 227 | ''' |
| 228 | 228 | Given a dictionary with a student dict {'name':'john', 'number': 123} |
| 229 | 229 | returns instance of Test() for that particular student |
| 230 | 230 | ''' |
| 231 | 231 | |
| 232 | 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 | 237 | for qlist in self['questions']: |
| 238 | 238 | # choose one question variant |
| ... | ... | @@ -255,28 +255,28 @@ class TestFactory(dict): |
| 255 | 255 | question['number'] = qnum # counter for non informative panels |
| 256 | 256 | qnum += 1 |
| 257 | 257 | |
| 258 | - test.append(question) | |
| 258 | + questions.append(question) | |
| 259 | 259 | |
| 260 | 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 | 263 | if total_points > 0: |
| 264 | 264 | # normalize question points to scale |
| 265 | 265 | if self['scale'] is not None: |
| 266 | 266 | scale_min, scale_max = self['scale'] |
| 267 | - for question in test: | |
| 267 | + for question in questions: | |
| 268 | 268 | question['points'] *= (scale_max - scale_min) / total_points |
| 269 | 269 | else: |
| 270 | 270 | self['scale'] = [0, total_points] |
| 271 | 271 | else: |
| 272 | 272 | logger.warning('Total points is **ZERO**.') |
| 273 | 273 | if self['scale'] is None: |
| 274 | - self['scale'] = [0, 20] | |
| 274 | + self['scale'] = [0, 20] # default | |
| 275 | 275 | |
| 276 | 276 | if nerr > 0: |
| 277 | 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 | 280 | inherit = {'ref', 'title', 'database', 'answers_dir', |
| 281 | 281 | 'questions_dir', 'files', |
| 282 | 282 | 'duration', 'autosubmit', |
| ... | ... | @@ -284,9 +284,7 @@ class TestFactory(dict): |
| 284 | 284 | 'show_ref', 'debug', } |
| 285 | 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 | 290 | def __repr__(self): |
| ... | ... | @@ -303,6 +301,13 @@ class Test(dict): |
| 303 | 301 | # ------------------------------------------------------------------------ |
| 304 | 302 | def __init__(self, d): |
| 305 | 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 | 311 | self['start_time'] = datetime.now() |
| 307 | 312 | self['finish_time'] = None |
| 308 | 313 | self['state'] = 'ACTIVE' |
| ... | ... | @@ -346,5 +351,12 @@ class Test(dict): |
| 346 | 351 | self['finish_time'] = datetime.now() |
| 347 | 352 | self['state'] = 'QUIT' |
| 348 | 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 | 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') | ... | ... |