Commit 524327c3fcf1b0e3004b4eed3531ce0987cd7972

Authored by Miguel Barão
1 parent 2e43c7a2
Exists in master and in 1 other branch dev

pregenerates tests for all students

use raise from pattern to chain exception stack trace
add function to print test (incomplete)
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')
... ...