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