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