Commit 8b66e35c6c61c7f7caea3e0019fc9f36070d676e

Authored by Miguel Barão
1 parent 0dff80b7
Exists in master and in 1 other branch dev

changes:

- list of allowed students given in a file (new command line option)
- pregenerate tests for allowed students
- random sample questions from a list specified in the test configuration.
- popper.js removed. its provided by bootstrap.bundle
1 1
2 # BUGS 2 # BUGS
3 3
  4 +- reload do teste recomeça a contagem no inicio do tempo.
4 - em admin, quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20. a tabela teste na DB não tem a escala desse teste. 5 - em admin, quando scale_max não é 20, as cores das barras continuam a reflectir a escala 0,20. a tabela teste na DB não tem a escala desse teste.
5 - em grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao dos testes realizados no passado (tabela test devia guardar a escala). 6 - em grade.html as barras estao normalizadas para os limites scale_min e max do teste actual e nao dos testes realizados no passado (tabela test devia guardar a escala).
6 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>. 7 - codigo `hello world` nao esta a preservar o whitespace. O renderer de markdown gera a tag <code> que não preserva whitespace. Necessario adicionar <pre>.
@@ -8,6 +9,7 @@ @@ -8,6 +9,7 @@
8 - a revisao do teste não mostra as imagens. 9 - a revisao do teste não mostra as imagens.
9 - Test.reset_answers() unused. 10 - Test.reset_answers() unused.
10 - teste nao esta a mostrar imagens de vez em quando.??? 11 - teste nao esta a mostrar imagens de vez em quando.???
  12 +- testar as perguntas todas no início do teste como o aprendizations.
11 13
12 # TODO 14 # TODO
13 15
@@ -17,10 +19,8 @@ @@ -17,10 +19,8 @@
17 ou usar push (websockets?) 19 ou usar push (websockets?)
18 - mudar ref do test para test_id (ref já é usado nas perguntas) 20 - mudar ref do test para test_id (ref já é usado nas perguntas)
19 - servidor ntpd no x220 para configurar a data/hora dos portateis dell 21 - servidor ntpd no x220 para configurar a data/hora dos portateis dell
20 -- autorização dada, mas teste não disponível até que seja dada ordem para começar. 22 +- sala de espera: autorização dada, mas teste não disponível até que seja dada ordem para começar.
21 - alunos com necessidades especiais nao podem ter autosubmit. ter um autosubmit_exceptions: ['123', '456'] 23 - alunos com necessidades especiais nao podem ter autosubmit. ter um autosubmit_exceptions: ['123', '456']
22 -- mostrar unfocus e window area em /admin  
23 -- testar as perguntas todas no início do teste.  
24 - submissao fazer um post ajax? 24 - submissao fazer um post ajax?
25 - adicionar opcao para eliminar um teste em curso. 25 - adicionar opcao para eliminar um teste em curso.
26 - enviar resposta de cada pergunta individualmente. 26 - enviar resposta de cada pergunta individualmente.
@@ -52,13 +52,14 @@ ou usar push (websockets?) @@ -52,13 +52,14 @@ ou usar push (websockets?)
52 - se ocorrer um erro na correcçao avisar aluno para contactar o professor. 52 - se ocorrer um erro na correcçao avisar aluno para contactar o professor.
53 - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova? 53 - abrir o teste numa janela maximizada e que nao permite que o aluno a redimensione/mova?
54 - detectar scroll e enviar posição para servidor (analise de scroll para detectar copianço? ou simplesmente para analisar como os alunos percorrem o teste) 54 - detectar scroll e enviar posição para servidor (analise de scroll para detectar copianço? ou simplesmente para analisar como os alunos percorrem o teste)
55 -- aviso na pagina principal para quem usa browser da treta 55 +- aviso na pagina principal para quem usa browser incompativel ou settings esquisitos... Apos login pode ser enviado e submetido um exemplo de teste para verificar se browser consegue submeter? ha alunos com javascript bloqueado?
56 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput 56 - criar perguntas de outros tipos, e.g. associação, ordenação, varios textinput
57 -- perguntas para professor corrigir mais tarde. 57 +- perguntas para professor corrigir mais tarde. permitir que review possa alterar as notas
58 - fazer uma calculadora javascript e por no menu. surge como modal 58 - fazer uma calculadora javascript e por no menu. surge como modal
59 59
60 # FIXED 60 # FIXED
61 61
  62 +- mostrar unfocus e window area em /admin
62 - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?) 63 - CRITICAL se answer for `i<n` a revisão de provas mostra apenas i (interpreta `<` como tag?)
63 - botao de autorizar desliga-se, fazer debounce. 64 - botao de autorizar desliga-se, fazer debounce.
64 - link na pagina com a nota para voltar ao principio. 65 - link na pagina com a nota para voltar ao principio.
perguntations/__init__.py
@@ -32,7 +32,7 @@ proof of submission and for review. @@ -32,7 +32,7 @@ proof of submission and for review.
32 ''' 32 '''
33 33
34 APP_NAME = 'perguntations' 34 APP_NAME = 'perguntations'
35 -APP_VERSION = '2020.11.dev1' 35 +APP_VERSION = '2020.11.dev2'
36 APP_DESCRIPTION = __doc__ 36 APP_DESCRIPTION = __doc__
37 37
38 __author__ = 'Miguel Barão' 38 __author__ = 'Miguel Barão'
perguntations/app.py
@@ -50,6 +50,7 @@ async def hash_password(password): @@ -50,6 +50,7 @@ async def hash_password(password):
50 50
51 51
52 # ============================================================================ 52 # ============================================================================
  53 +# main application
53 # ============================================================================ 54 # ============================================================================
54 class App(): 55 class App():
55 ''' 56 '''
@@ -66,10 +67,10 @@ class App(): @@ -66,10 +67,10 @@ class App():
66 67
67 # ------------------------------------------------------------------------ 68 # ------------------------------------------------------------------------
68 @contextmanager 69 @contextmanager
69 - def db_session(self): 70 + def _db_session(self):
70 ''' 71 '''
71 helper to manage db sessions using the `with` statement, for example: 72 helper to manage db sessions using the `with` statement, for example:
72 - with self.db_session() as s: s.query(...) 73 + with self._db_session() as s: s.query(...)
73 ''' 74 '''
74 session = self.Session() 75 session = self.Session()
75 try: 76 try:
@@ -98,23 +99,29 @@ class App(): @@ -98,23 +99,29 @@ class App():
98 engine = create_engine(database, echo=False) 99 engine = create_engine(database, echo=False)
99 self.Session = sessionmaker(bind=engine) 100 self.Session = sessionmaker(bind=engine)
100 try: 101 try:
101 - with self.db_session() as sess: 102 + with self._db_session() as sess:
102 num = sess.query(Student).filter(Student.id != '0').count() 103 num = sess.query(Student).filter(Student.id != '0').count()
103 except Exception as exc: 104 except Exception as exc:
104 raise AppException(f'Database unusable {dbfile}.') from exc 105 raise AppException(f'Database unusable {dbfile}.') from exc
105 logger.info('Database "%s" has %s students.', dbfile, num) 106 logger.info('Database "%s" has %s students.', dbfile, num)
106 107
107 - # pre-generate tests  
108 - logger.info('Generating tests for %d students...', num)  
109 - self._pregenerate_tests(num)  
110 - logger.info('Tests done.')  
111 -  
112 # command line option --allow-all 108 # command line option --allow-all
113 if conf['allow_all']: 109 if conf['allow_all']:
114 self.allow_all_students() 110 self.allow_all_students()
  111 + elif conf['allow_list'] is not None:
  112 + self.allow_list(conf['allow_list'])
115 else: 113 else:
116 logger.info('Students not yet allowed to login.') 114 logger.info('Students not yet allowed to login.')
117 115
  116 + # pre-generate tests
  117 +
  118 + if self.allowed:
  119 + logger.info('Generating %d tests. May take awhile...',
  120 + len(self.allowed))
  121 + self._pregenerate_tests(len(self.allowed))
  122 + else:
  123 + logger.info('No tests were generated.')
  124 +
118 # ------------------------------------------------------------------------ 125 # ------------------------------------------------------------------------
119 async def login(self, uid, try_pw): 126 async def login(self, uid, try_pw):
120 '''login authentication''' 127 '''login authentication'''
@@ -123,7 +130,7 @@ class App(): @@ -123,7 +130,7 @@ class App():
123 return False 130 return False
124 131
125 # get name+password from db 132 # get name+password from db
126 - with self.db_session() as sess: 133 + with self._db_session() as sess:
127 name, password = sess.query(Student.name, Student.password)\ 134 name, password = sess.query(Student.name, Student.password)\
128 .filter_by(id=uid)\ 135 .filter_by(id=uid)\
129 .one() 136 .one()
@@ -183,31 +190,34 @@ class App(): @@ -183,31 +190,34 @@ class App():
183 190
184 # ------------------------------------------------------------------------ 191 # ------------------------------------------------------------------------
185 def _pregenerate_tests(self, num): 192 def _pregenerate_tests(self, num):
186 - for _ in range(num):  
187 - event_loop = asyncio.get_event_loop()  
188 - test = event_loop.run_until_complete(self.testfactory.generate())  
189 - self.pregenerated_tests.append(test) 193 + event_loop = asyncio.get_event_loop()
  194 + # for _ in range(num):
  195 + # test = event_loop.run_until_complete(self.testfactory.generate())
  196 + # self.pregenerated_tests.append(test)
  197 +
  198 + self.pregenerated_tests += [
  199 + event_loop.run_until_complete(self.testfactory.generate())
  200 + for _ in range(num)]
190 201
191 # ------------------------------------------------------------------------ 202 # ------------------------------------------------------------------------
192 async def generate_test(self, uid): 203 async def generate_test(self, uid):
193 - '''generate a test for a given student'''  
194 - if uid in self.online:  
195 - try:  
196 - test = self.pregenerated_tests.pop()  
197 - except IndexError:  
198 - logger.info('"%s" generating new test.', uid)  
199 - test = await self.testfactory.generate() # student_id) FIXME  
200 - else:  
201 - logger.info('"%s" using pregenerated test.', uid) 204 + '''generate a test for a given student. the student must be online'''
  205 +
  206 + student_id = self.online[uid]['student'] # {'name': ?, 'number': ?}
202 207
203 - student_id = self.online[uid]['student'] # {number, name}  
204 - test.start(student_id)  
205 - self.online[uid]['test'] = test 208 + try:
  209 + test = self.pregenerated_tests.pop()
  210 + except IndexError:
  211 + logger.info('"%s" generating new test...', uid)
  212 + test = await self.testfactory.generate()
206 logger.info('"%s" test is ready.', uid) 213 logger.info('"%s" test is ready.', uid)
207 - return self.online[uid]['test'] 214 + else:
  215 + logger.info('"%s" using a pregenerated test.', uid)
208 216
209 - # this implies an error in the program, code should be unreachable!  
210 - logger.critical('"%s" is offline, can\'t generate test', uid) 217 + test.start(student_id) # student signs the test
  218 + self.online[uid]['test'] = test # register test for this student
  219 +
  220 + return self.online[uid]['test']
211 221
212 # ------------------------------------------------------------------------ 222 # ------------------------------------------------------------------------
213 async def correct_test(self, uid, ans): 223 async def correct_test(self, uid, ans):
@@ -236,7 +246,7 @@ class App(): @@ -236,7 +246,7 @@ class App():
236 logger.info('"%s" saved JSON.', uid) 246 logger.info('"%s" saved JSON.', uid)
237 247
238 # --- insert test and questions into database 248 # --- insert test and questions into database
239 - with self.db_session() as sess: 249 + with self._db_session() as sess:
240 sess.add(Test( 250 sess.add(Test(
241 ref=test['ref'], 251 ref=test['ref'],
242 title=test['title'], 252 title=test['title'],
@@ -273,7 +283,7 @@ class App(): @@ -273,7 +283,7 @@ class App():
273 test.save_json(fpath) 283 test.save_json(fpath)
274 284
275 # insert test into database 285 # insert test into database
276 - with self.db_session() as sess: 286 + with self._db_session() as sess:
277 sess.add(Test(ref=test['ref'], 287 sess.add(Test(ref=test['ref'],
278 title=test['title'], 288 title=test['title'],
279 grade=test['grade'], 289 grade=test['grade'],
@@ -309,7 +319,7 @@ class App(): @@ -309,7 +319,7 @@ class App():
309 '''generates a CSV with the grades of the test''' 319 '''generates a CSV with the grades of the test'''
310 test_id = self.testfactory['ref'] 320 test_id = self.testfactory['ref']
311 321
312 - with self.db_session() as sess: 322 + with self._db_session() as sess:
313 grades = sess.query(Question.student_id, Question.starttime, 323 grades = sess.query(Question.student_id, Question.starttime,
314 Question.ref, Question.grade)\ 324 Question.ref, Question.grade)\
315 .filter(Question.test_id == test_id)\ 325 .filter(Question.test_id == test_id)\
@@ -338,7 +348,7 @@ class App(): @@ -338,7 +348,7 @@ class App():
338 348
339 def get_test_csv(self): 349 def get_test_csv(self):
340 '''generates a CSV with the grades of the test''' 350 '''generates a CSV with the grades of the test'''
341 - with self.db_session() as sess: 351 + with self._db_session() as sess:
342 grades = sess.query(Test.student_id, Test.grade, 352 grades = sess.query(Test.student_id, Test.grade,
343 Test.starttime, Test.finishtime)\ 353 Test.starttime, Test.finishtime)\
344 .filter(Test.ref == self.testfactory['ref'])\ 354 .filter(Test.ref == self.testfactory['ref'])\
@@ -360,21 +370,21 @@ class App(): @@ -360,21 +370,21 @@ class App():
360 370
361 def get_student_grades_from_all_tests(self, uid): 371 def get_student_grades_from_all_tests(self, uid):
362 '''get grades of student from all tests''' 372 '''get grades of student from all tests'''
363 - with self.db_session() as sess: 373 + with self._db_session() as sess:
364 return sess.query(Test.title, Test.grade, Test.finishtime)\ 374 return sess.query(Test.title, Test.grade, Test.finishtime)\
365 .filter_by(student_id=uid)\ 375 .filter_by(student_id=uid)\
366 .order_by(Test.finishtime) 376 .order_by(Test.finishtime)
367 377
368 def get_json_filename_of_test(self, test_id): 378 def get_json_filename_of_test(self, test_id):
369 '''get JSON filename from database given the test_id''' 379 '''get JSON filename from database given the test_id'''
370 - with self.db_session() as sess: 380 + with self._db_session() as sess:
371 return sess.query(Test.filename)\ 381 return sess.query(Test.filename)\
372 .filter_by(id=test_id)\ 382 .filter_by(id=test_id)\
373 .scalar() 383 .scalar()
374 384
375 def get_student_grades_from_test(self, uid, testid): 385 def get_student_grades_from_test(self, uid, testid):
376 '''get grades of student for a given testid''' 386 '''get grades of student for a given testid'''
377 - with self.db_session() as sess: 387 + with self._db_session() as sess:
378 return sess.query(Test.grade, Test.finishtime, Test.id)\ 388 return sess.query(Test.grade, Test.finishtime, Test.id)\
379 .filter_by(student_id=uid)\ 389 .filter_by(student_id=uid)\
380 .filter_by(ref=testid)\ 390 .filter_by(ref=testid)\
@@ -399,7 +409,7 @@ class App(): @@ -399,7 +409,7 @@ class App():
399 # --- private methods ---------------------------------------------------- 409 # --- private methods ----------------------------------------------------
400 def _get_all_students(self): 410 def _get_all_students(self):
401 '''get all students from database''' 411 '''get all students from database'''
402 - with self.db_session() as sess: 412 + with self._db_session() as sess:
403 return sess.query(Student.id, Student.name, Student.password)\ 413 return sess.query(Student.id, Student.name, Student.password)\
404 .filter(Student.id != '0')\ 414 .filter(Student.id != '0')\
405 .order_by(Student.id) 415 .order_by(Student.id)
@@ -433,13 +443,33 @@ class App(): @@ -433,13 +443,33 @@ class App():
433 '''allow all students to login''' 443 '''allow all students to login'''
434 all_students = self._get_all_students() 444 all_students = self._get_all_students()
435 self.allowed.update(s[0] for s in all_students) 445 self.allowed.update(s[0] for s in all_students)
436 - logger.info('Allowed all students.') 446 + logger.info('Allowed all %d students.', len(self.allowed))
437 447
438 def deny_all_students(self): 448 def deny_all_students(self):
439 '''deny all students to login''' 449 '''deny all students to login'''
440 logger.info('Denying all students...') 450 logger.info('Denying all students...')
441 self.allowed.clear() 451 self.allowed.clear()
442 452
  453 + def allow_list(self, filename):
  454 + '''allow students listed in file (one number per line)'''
  455 + try:
  456 + with open(filename, 'r') as file:
  457 + allowed_in_file = {s.strip() for s in file} - {''}
  458 + except Exception as exc:
  459 + error_msg = f'Cannot read file {filename}'
  460 + logger.critical(error_msg)
  461 + raise AppException(error_msg) from exc
  462 +
  463 + enrolled = set(s[0] for s in self._get_all_students()) # in database
  464 + self.allowed.update(allowed_in_file & enrolled)
  465 + logger.info('Allowed %d students provided in "%s"', len(self.allowed),
  466 + filename)
  467 +
  468 + not_enrolled = allowed_in_file - enrolled
  469 + if not_enrolled:
  470 + logger.warning(' but found students not in the database: %s',
  471 + ', '.join(not_enrolled))
  472 +
443 def _focus_student(self, uid): 473 def _focus_student(self, uid):
444 '''set student in focus state''' 474 '''set student in focus state'''
445 self.unfocus.discard(uid) 475 self.unfocus.discard(uid)
@@ -462,7 +492,7 @@ class App(): @@ -462,7 +492,7 @@ class App():
462 '''change password on the database''' 492 '''change password on the database'''
463 if password: 493 if password:
464 password = await hash_password(password) 494 password = await hash_password(password)
465 - with self.db_session() as sess: 495 + with self._db_session() as sess:
466 student = sess.query(Student).filter_by(id=uid).one() 496 student = sess.query(Student).filter_by(id=uid).one()
467 student.password = password 497 student.password = password
468 logger.info('"%s" password updated.', uid) 498 logger.info('"%s" password updated.', uid)
@@ -470,7 +500,7 @@ class App(): @@ -470,7 +500,7 @@ class App():
470 def insert_new_student(self, uid, name): 500 def insert_new_student(self, uid, name):
471 '''insert new student into the database''' 501 '''insert new student into the database'''
472 try: 502 try:
473 - with self.db_session() as sess: 503 + with self._db_session() as sess:
474 sess.add(Student(id=uid, name=name, password='')) 504 sess.add(Student(id=uid, name=name, password=''))
475 except exc.SQLAlchemyError: 505 except exc.SQLAlchemyError:
476 logger.error('Insert failed: student %s already exists?', uid) 506 logger.error('Insert failed: student %s already exists?', uid)
perguntations/main.py
@@ -32,10 +32,14 @@ def parse_cmdline_arguments(): @@ -32,10 +32,14 @@ def parse_cmdline_arguments():
32 'included with this software before running the server.') 32 'included with this software before running the server.')
33 parser.add_argument('testfile', 33 parser.add_argument('testfile',
34 type=str, 34 type=str,
  35 + # nargs='+', # TODO
35 help='tests in YAML format') 36 help='tests in YAML format')
36 parser.add_argument('--allow-all', 37 parser.add_argument('--allow-all',
37 action='store_true', 38 action='store_true',
38 help='Allow all students to login immediately') 39 help='Allow all students to login immediately')
  40 + parser.add_argument('--allow-list',
  41 + type=str,
  42 + help='File with list of students to allow immediately')
39 parser.add_argument('--debug', 43 parser.add_argument('--debug',
40 action='store_true', 44 action='store_true',
41 help='Enable debug messages') 45 help='Enable debug messages')
@@ -46,9 +50,12 @@ def parse_cmdline_arguments(): @@ -46,9 +50,12 @@ def parse_cmdline_arguments():
46 action='store_true', 50 action='store_true',
47 help='Review mode: doesn\'t generate test') 51 help='Review mode: doesn\'t generate test')
48 parser.add_argument('--port', 52 parser.add_argument('--port',
49 - type=int, default=8443, 53 + type=int,
  54 + default=8443,
50 help='port for the HTTPS server (default: 8443)') 55 help='port for the HTTPS server (default: 8443)')
51 - parser.add_argument('--version', action='version', version=APP_VERSION, 56 + parser.add_argument('--version',
  57 + action='version',
  58 + version=APP_VERSION,
52 help='Show version information and exit') 59 help='Show version information and exit')
53 return parser.parse_args() 60 return parser.parse_args()
54 61
@@ -119,6 +126,7 @@ def main(): @@ -119,6 +126,7 @@ def main():
119 'testfile': args.testfile, 126 'testfile': args.testfile,
120 'debug': args.debug, 127 'debug': args.debug,
121 'allow_all': args.allow_all, 128 'allow_all': args.allow_all,
  129 + 'allow_list': args.allow_list,
122 'show_ref': args.show_ref, 130 'show_ref': args.show_ref,
123 'review': args.review, 131 'review': args.review,
124 } 132 }
perguntations/serve.py
@@ -518,6 +518,7 @@ def run_webserver(app, ssl_opt, port, debug): @@ -518,6 +518,7 @@ def run_webserver(app, ssl_opt, port, debug):
518 ''' 518 '''
519 519
520 # --- create web application 520 # --- create web application
  521 + logging.info('-----------------------------------------------------------')
521 logging.info('Starting WebApplication (tornado)') 522 logging.info('Starting WebApplication (tornado)')
522 try: 523 try:
523 webapp = WebApplication(app, debug=debug) 524 webapp = WebApplication(app, debug=debug)
perguntations/static/css/test.css
@@ -8,6 +8,11 @@ body { @@ -8,6 +8,11 @@ body {
8 background: #bbb; 8 background: #bbb;
9 } 9 }
10 10
  11 +/*code {
  12 + white-space: pre;
  13 +}
  14 +*/
  15 +
11 /* Hack to avoid name clash between pygments and mathjax */ 16 /* Hack to avoid name clash between pygments and mathjax */
12 .MathJax .mo, 17 .MathJax .mo,
13 .MathJax .mi { 18 .MathJax .mi {
perguntations/static/popper.js
@@ -1 +0,0 @@ @@ -1 +0,0 @@
1 -../../node_modules/popper.js/dist/  
2 \ No newline at end of file 0 \ No newline at end of file
perguntations/test.py
@@ -48,7 +48,7 @@ class TestFactory(dict): @@ -48,7 +48,7 @@ class TestFactory(dict):
48 'duration': 0, # 0=infinite 48 'duration': 0, # 0=infinite
49 'autosubmit': False, 49 'autosubmit': False,
50 'debug': False, 50 'debug': False,
51 - 'show_ref': False 51 + 'show_ref': False,
52 }) 52 })
53 self.update(conf) 53 self.update(conf)
54 54
@@ -122,7 +122,6 @@ class TestFactory(dict): @@ -122,7 +122,6 @@ class TestFactory(dict):
122 if qmissing: 122 if qmissing:
123 raise TestFactoryException(f'Could not find questions {qmissing}.') 123 raise TestFactoryException(f'Could not find questions {qmissing}.')
124 124
125 -  
126 # ------------------------------------------------------------------------ 125 # ------------------------------------------------------------------------
127 def check_test_ref(self): 126 def check_test_ref(self):
128 '''Test must have a `ref`''' 127 '''Test must have a `ref`'''
@@ -223,7 +222,7 @@ class TestFactory(dict): @@ -223,7 +222,7 @@ class TestFactory(dict):
223 self.check_grade_scaling() 222 self.check_grade_scaling()
224 223
225 # ------------------------------------------------------------------------ 224 # ------------------------------------------------------------------------
226 - async def generate(self): #, student): 225 + async def generate(self):
227 ''' 226 '''
228 Given a dictionary with a student dict {'name':'john', 'number': 123} 227 Given a dictionary with a student dict {'name':'john', 'number': 123}
229 returns instance of Test() for that particular student 228 returns instance of Test() for that particular student
@@ -235,27 +234,29 @@ class TestFactory(dict): @@ -235,27 +234,29 @@ class TestFactory(dict):
235 nerr = 0 # count errors during questions generation 234 nerr = 0 # count errors during questions generation
236 235
237 for qlist in self['questions']: 236 for qlist in self['questions']:
238 - # choose one question variant  
239 - qref = random.choice(qlist['ref'])  
240 -  
241 - # generate instance of question  
242 - try:  
243 - question = await self.question_factory[qref].gen_async()  
244 - except QuestionException:  
245 - logger.error('Can\'t generate question "%s". Skipping.', qref)  
246 - nerr += 1  
247 - continue  
248 -  
249 - # some defaults  
250 - if question['type'] in ('information', 'success', 'warning',  
251 - 'alert'):  
252 - question['points'] = qlist.get('points', 0.0)  
253 - else:  
254 - question['points'] = qlist.get('points', 1.0)  
255 - question['number'] = qnum # counter for non informative panels  
256 - qnum += 1  
257 -  
258 - questions.append(question) 237 + # choose list of question variants
  238 + choose = qlist.get('choose', 1)
  239 + qrefs = random.sample(qlist['ref'], k=choose)
  240 +
  241 + for qref in qrefs:
  242 + # generate instance of question
  243 + try:
  244 + question = await self.question_factory[qref].gen_async()
  245 + except QuestionException:
  246 + logger.error('Can\'t generate question "%s". Skipping.', qref)
  247 + nerr += 1
  248 + continue
  249 +
  250 + # some defaults
  251 + if question['type'] in ('information', 'success', 'warning',
  252 + 'alert'):
  253 + question['points'] = qlist.get('points', 0.0)
  254 + else:
  255 + question['points'] = qlist.get('points', 1.0)
  256 + question['number'] = qnum # counter for non informative panels
  257 + qnum += 1
  258 +
  259 + questions.append(question)
259 260
260 # setup scale 261 # setup scale
261 total_points = sum(q['points'] for q in questions) 262 total_points = sum(q['points'] for q in questions)
@@ -320,13 +321,20 @@ class Test(dict): @@ -320,13 +321,20 @@ class Test(dict):
320 question['answer'] = None 321 question['answer'] = None
321 322
322 # ------------------------------------------------------------------------ 323 # ------------------------------------------------------------------------
323 - def update_answers(self, ans): 324 + def update_answer(self, ref, ans):
  325 + '''updates one answer in the test'''
  326 + self['questions'][ref].set_answer(ans)
  327 +
  328 + # ------------------------------------------------------------------------
  329 + def update_answers(self, answers_dict):
324 ''' 330 '''
325 Given a dictionary ans={'ref': 'some answer'} updates the answers of 331 Given a dictionary ans={'ref': 'some answer'} updates the answers of
326 - the test. Only affects the questions referred in the dictionary. 332 + multiple questions in the test.
  333 + Only affects the questions referred in the dictionary.
327 ''' 334 '''
328 - for ref, answer in ans.items():  
329 - self['questions'][ref]['answer'] = answer 335 + for ref, ans in answers_dict.items():
  336 + self['questions'][ref].set_answer(ans)
  337 + # self['questions'][ref]['answer'] = ans
330 338
331 # ------------------------------------------------------------------------ 339 # ------------------------------------------------------------------------
332 async def correct(self): 340 async def correct(self):
@@ -351,7 +359,7 @@ class Test(dict): @@ -351,7 +359,7 @@ class Test(dict):
351 self['finish_time'] = datetime.now() 359 self['finish_time'] = datetime.now()
352 self['state'] = 'QUIT' 360 self['state'] = 'QUIT'
353 self['grade'] = 0.0 361 self['grade'] = 0.0
354 - # logger.info('Student %s: gave up.', self["student"]["number"]) 362 + logger.info('Student %s: gave up.', self["student"]["number"])
355 return self['grade'] 363 return self['grade']
356 364
357 # ------------------------------------------------------------------------ 365 # ------------------------------------------------------------------------