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') | ... | ... |