Commit b5140b82404a2e521a92bd51873cbfbdaa623815

Authored by Miguel Barão
1 parent 4d7a7e9a
Exists in master and in 1 other branch dev

changes behavior of checkbox correct and updated tutorial.

demo/questions/questions-tutorial.yaml
@@ -185,11 +185,13 @@ @@ -185,11 +185,13 @@
185 title: Escolha múltipla, várias opções correctas 185 title: Escolha múltipla, várias opções correctas
186 text: | 186 text: |
187 As perguntas de escolha múltipla permitem apresentar um conjunto de opções 187 As perguntas de escolha múltipla permitem apresentar um conjunto de opções
188 - podendo ser seleccionadas várias em simultaneo. 188 + podendo ser seleccionadas várias em simultâneo.
189 Funcionam como múltiplas perguntas independentes de resposta sim/não. 189 Funcionam como múltiplas perguntas independentes de resposta sim/não.
190 190
191 - Cada opção seleccionada (`sim`) recebe a cotação indicada em `correct`.  
192 - Cada opção não seleccionadas (`não`) tem a cotação simétrica. 191 + As respostas que devem ou não ser seleccionadas são indicadas com `1` e `0`
  192 + ou com booleanos `true` e `false`.
  193 + Cada resposta errada desconta um valor que é o simétrico da resposta certa.
  194 + Se acertar uma opção ganha `+1`, se errar obtém `-1`.
193 195
194 ```yaml 196 ```yaml
195 - type: checkbox 197 - type: checkbox
@@ -203,14 +205,15 @@ @@ -203,14 +205,15 @@
203 - Opção 2 205 - Opção 2
204 - Opção 3 (certa) 206 - Opção 3 (certa)
205 - Opção 4 207 - Opção 4
206 - correct: [+1, -1, -1, +1, -1] 208 + correct: [1, 0, 0, 1, 0]
207 ``` 209 ```
208 210
209 - Neste exemplo, seleccionando as opções 0 e 3 obtém-se cotação +1 em cada  
210 - uma, enquanto que seleccionando as opções 1, 2 e 4 obtém-se cotação -1.  
211 - As opções não seleccionadas pelo aluno dão a cotação simétrica à indicada.  
212 - Por exemplo se não seleccionar a opção 0, tem cotação -1, e não  
213 - seleccionando a opção 1 obtém-se +1. 211 + Neste exemplo, seleccionando as opções 0 e 3 obtém-se cotação `+1` em cada
  212 + uma, enquanto que seleccionando erradamente as opções 1, 2 e 4 obtém-se
  213 + cotação `-1`.
  214 + Do mesmo modo, não seleccionando as opções certas 0 e 3 obtém-se a cotação
  215 + `-1` em cada uma, e não seleccionando (correctamente) as 1, 2 e 4 obtém-se
  216 + `+1` em cada.
214 217
215 *(Neste tipo de perguntas não há forma de responder a apenas algumas delas, 218 *(Neste tipo de perguntas não há forma de responder a apenas algumas delas,
216 são sempre todas corrigidas. Se um aluno só sabe a resposta a algumas das 219 são sempre todas corrigidas. Se um aluno só sabe a resposta a algumas das
@@ -230,7 +233,7 @@ @@ -230,7 +233,7 @@
230 ``` 233 ```
231 234
232 Assume-se que a primeira alternativa de cada opção tem a cotação indicada 235 Assume-se que a primeira alternativa de cada opção tem a cotação indicada
233 - em `correct`, enquanto a segunda alternativa tem a cotação simétrica. 236 + em `correct`, enquanto a segunda alternativa tem a cotação contrária.
234 237
235 Tal como nas perguntas do tipo `radio`, podem ser usadas as configurações 238 Tal como nas perguntas do tipo `radio`, podem ser usadas as configurações
236 `shuffle` e `discount` com valor `false` para as desactivar. 239 `shuffle` e `discount` com valor `false` para as desactivar.
@@ -241,7 +244,7 @@ @@ -241,7 +244,7 @@
241 - ['Opção 1 (não)', 'Opção 1 (sim)'] 244 - ['Opção 1 (não)', 'Opção 1 (sim)']
242 - Opção 2 (não) 245 - Opção 2 (não)
243 - Opção 3 (sim) 246 - Opção 3 (sim)
244 - correct: [1, -1, -1, 1] 247 + correct: [1, 0, 0, 1]
245 shuffle: false 248 shuffle: false
246 249
247 # ---------------------------------------------------------------------------- 250 # ----------------------------------------------------------------------------
@@ -266,8 +269,7 @@ @@ -266,8 +269,7 @@
266 Neste caso, as respostas aceites são `azul`, `Azul` ou `AZUL`. 269 Neste caso, as respostas aceites são `azul`, `Azul` ou `AZUL`.
267 270
268 Em alguns casos pode ser conveniente transformar a resposta antes de a 271 Em alguns casos pode ser conveniente transformar a resposta antes de a
269 - comparar, por exemplo para remover espaços ou converter para maiúsculas ou  
270 - maiúsculas. 272 + comparar, por exemplo para remover espaços ou converter para maiúsculas.
271 A opção `transform` permite dar uma sequência de transformações a aplicar à 273 A opção `transform` permite dar uma sequência de transformações a aplicar à
272 resposta do aluno, por exemplo: 274 resposta do aluno, por exemplo:
273 275
@@ -284,7 +286,7 @@ @@ -284,7 +286,7 @@
284 * `normalize_space` remove espaços do início e fim (trim), e substitui 286 * `normalize_space` remove espaços do início e fim (trim), e substitui
285 múltiplos espaços por um único espaço (no meio). 287 múltiplos espaços por um único espaço (no meio).
286 * `lower` e `upper` convertem respectivamente para minúsculas e maiúsculas. 288 * `lower` e `upper` convertem respectivamente para minúsculas e maiúsculas.
287 - transform: ['remove_spaces', 'lower'] 289 + transform: ['trim', 'lower']
288 correct: ['azul'] 290 correct: ['azul']
289 291
290 # --------------------------------------------------------------------------- 292 # ---------------------------------------------------------------------------
@@ -349,6 +351,9 @@ @@ -349,6 +351,9 @@
349 Neste exemplo o intervalo de respostas correctas é o intervalo fechado 351 Neste exemplo o intervalo de respostas correctas é o intervalo fechado
350 [3.14, 3.15]. 352 [3.14, 3.15].
351 353
  354 + Se em vez de dar um intervalo, apenas for indicado um valor numérico $n$,
  355 + este é automaticamente convertido para para um intervalo $[n,n]$.
  356 +
352 **Atenção:** as respostas têm de usar o ponto como separador decimal. 357 **Atenção:** as respostas têm de usar o ponto como separador decimal.
353 Em geral são aceites números inteiros, como `123`, 358 Em geral são aceites números inteiros, como `123`,
354 ou em vírgula flutuante, como em `0.23`, `1e-3`. 359 ou em vírgula flutuante, como em `0.23`, `1e-3`.
@@ -362,8 +367,8 @@ @@ -362,8 +367,8 @@
362 ref: tut-textarea 367 ref: tut-textarea
363 title: Resposta em múltiplas linhas de texto 368 title: Resposta em múltiplas linhas de texto
364 text: | 369 text: |
365 - Este tipo de perguntas permitem respostas em múltiplas linhas de texto, que  
366 - podem ser úteis por exemplo para introduzir código. 370 + Este tipo de perguntas permitem respostas em múltiplas linhas de texto e
  371 + são as mais flexíveis.
367 372
368 A resposta é enviada para um programa externo para ser avaliada. 373 A resposta é enviada para um programa externo para ser avaliada.
369 O programa externo é um programa escrito numa linguagem qualquer, desde que 374 O programa externo é um programa escrito numa linguagem qualquer, desde que
@@ -401,8 +406,8 @@ @@ -401,8 +406,8 @@
401 0.75 406 0.75
402 ``` 407 ```
403 408
404 - ou opcionalmente escrever em formato yaml, eventualmente com um comentário  
405 - que será arquivado com o teste. 409 + ou opcionalmente escrever em formato json ou yaml, eventualmente com um
  410 + comentário que será arquivado com o teste.
406 Exemplo: 411 Exemplo:
407 412
408 ```yaml 413 ```yaml
perguntations/questions.py
@@ -42,7 +42,6 @@ class Question(dict): @@ -42,7 +42,6 @@ class Question(dict):
42 'comments': '', 42 'comments': '',
43 'solution': '', 43 'solution': '',
44 'files': {}, 44 'files': {},
45 - # 'max_tries': 3,  
46 })) 45 }))
47 46
48 def correct(self) -> None: 47 def correct(self) -> None:
@@ -72,7 +71,6 @@ class QuestionRadio(Question): @@ -72,7 +71,6 @@ class QuestionRadio(Question):
72 ''' 71 '''
73 72
74 # ------------------------------------------------------------------------ 73 # ------------------------------------------------------------------------
75 - # FIXME marking all options right breaks  
76 def __init__(self, q: QDict) -> None: 74 def __init__(self, q: QDict) -> None:
77 super().__init__(q) 75 super().__init__(q)
78 76
@@ -86,18 +84,46 @@ class QuestionRadio(Question): @@ -86,18 +84,46 @@ class QuestionRadio(Question):
86 'max_tries': (n + 3) // 4 # 1 try for each 4 options 84 'max_tries': (n + 3) // 4 # 1 try for each 4 options
87 })) 85 }))
88 86
89 - # convert int to list, e.g. correct: 2 --> correct: [0,0,1,0,0]  
90 - # correctness levels from 0.0 to 1.0 (no discount here!) 87 + # check correct bounds and convert int to list,
  88 + # e.g. correct: 2 --> correct: [0,0,1,0,0]
91 if isinstance(self['correct'], int): 89 if isinstance(self['correct'], int):
  90 + if not (0 <= self['correct'] < n):
  91 + msg = (f'Correct option not in range 0..{n-1} in '
  92 + f'"{self["ref"]}"')
  93 + raise QuestionException(msg)
  94 +
92 self['correct'] = [1.0 if x == self['correct'] else 0.0 95 self['correct'] = [1.0 if x == self['correct'] else 0.0
93 for x in range(n)] 96 for x in range(n)]
94 97
95 - if len(self['correct']) != n:  
96 - msg = ('Number of options and correct differ in '  
97 - f'"{self["ref"]}", file "{self["filename"]}".')  
98 - logger.error(msg)  
99 - raise QuestionException(msg)  
100 - 98 + elif isinstance(self['correct'], list):
  99 + # must match number of options
  100 + if len(self['correct']) != n:
  101 + msg = (f'Incompatible sizes: {n} options vs '
  102 + f'{len(self["correct"])} correct in "{self["ref"]}"')
  103 + raise QuestionException(msg)
  104 + # make sure is a list of floats
  105 + try:
  106 + self['correct'] = [float(x) for x in self['correct']]
  107 + except (ValueError, TypeError):
  108 + msg = (f'Correct list must contain numbers [0.0, 1.0] or '
  109 + f'booleans in "{self["ref"]}"')
  110 + raise QuestionException(msg)
  111 +
  112 + # check grade boundaries
  113 + if self['discount'] and not all(0.0 <= x <= 1.0
  114 + for x in self['correct']):
  115 + msg = (f'Correct values must be in the interval [0.0, 1.0] in '
  116 + f'"{self["ref"]}"')
  117 + raise QuestionException(msg)
  118 +
  119 + # at least one correct option
  120 + if all(x < 1.0 for x in self['correct']):
  121 + msg = (f'At least one correct option is required in '
  122 + f'"{self["ref"]}"')
  123 + raise QuestionException(msg)
  124 +
  125 + # If shuffle==false, all options are shown as defined
  126 + # otherwise, select 1 correct and choose a few wrong ones
101 if self['shuffle']: 127 if self['shuffle']:
102 # lists with indices of right and wrong options 128 # lists with indices of right and wrong options
103 right = [i for i in range(n) if self['correct'][i] >= 1] 129 right = [i for i in range(n) if self['correct'][i] >= 1]
@@ -123,7 +149,7 @@ class QuestionRadio(Question): @@ -123,7 +149,7 @@ class QuestionRadio(Question):
123 # final shuffle of the options 149 # final shuffle of the options
124 perm = random.sample(range(self['choose']), k=self['choose']) 150 perm = random.sample(range(self['choose']), k=self['choose'])
125 self['options'] = [str(options[i]) for i in perm] 151 self['options'] = [str(options[i]) for i in perm]
126 - self['correct'] = [float(correct[i]) for i in perm] 152 + self['correct'] = [correct[i] for i in perm]
127 153
128 # ------------------------------------------------------------------------ 154 # ------------------------------------------------------------------------
129 # can assign negative grades for wrong answers 155 # can assign negative grades for wrong answers
@@ -131,10 +157,13 @@ class QuestionRadio(Question): @@ -131,10 +157,13 @@ class QuestionRadio(Question):
131 super().correct() 157 super().correct()
132 158
133 if self['answer'] is not None: 159 if self['answer'] is not None:
134 - x = self['correct'][int(self['answer'])]  
135 - if self['discount']:  
136 - n = len(self['options']) # number of options  
137 - x_aver = sum(self['correct']) / n 160 + x = self['correct'][int(self['answer'])] # get grade of the answer
  161 + n = len(self['options'])
  162 + x_aver = sum(self['correct']) / n # expected value of grade
  163 +
  164 + # note: there are no numerical errors when summing 1.0s so the
  165 + # x_aver can be exactly 1.0 if all options are right
  166 + if self['discount'] and x_aver != 1.0:
138 x = (x - x_aver) / (1.0 - x_aver) 167 x = (x - x_aver) / (1.0 - x_aver)
139 self['grade'] = x 168 self['grade'] = x
140 169
@@ -168,12 +197,44 @@ class QuestionCheckbox(Question): @@ -168,12 +197,44 @@ class QuestionCheckbox(Question):
168 'max_tries': max(1, min(n - 1, 3)) 197 'max_tries': max(1, min(n - 1, 3))
169 })) 198 }))
170 199
  200 + # must be a list of numbers
  201 + if not isinstance(self['correct'], list):
  202 + msg = 'Correct must be a list of numbers or booleans'
  203 + raise QuestionException(msg)
  204 +
  205 + # must match number of options
171 if len(self['correct']) != n: 206 if len(self['correct']) != n:
172 - msg = (f'Options and correct size mismatch in '  
173 - f'"{self["ref"]}", file "{self["filename"]}".')  
174 - logger.error(msg) 207 + msg = (f'Incompatible sizes: {n} options vs '
  208 + f'{len(self["correct"])} correct in "{self["ref"]}"')
  209 + raise QuestionException(msg)
  210 +
  211 + # make sure is a list of floats
  212 + try:
  213 + self['correct'] = [float(x) for x in self['correct']]
  214 + except (ValueError, TypeError):
  215 + msg = (f'Correct list must contain numbers or '
  216 + f'booleans in "{self["ref"]}"')
175 raise QuestionException(msg) 217 raise QuestionException(msg)
176 218
  219 + # check grade boundaries
  220 + if self['discount'] and not all(0.0 <= x <= 1.0
  221 + for x in self['correct']):
  222 +
  223 + msg0 = ('+-------------- BEHAVIOR CHANGE NOTICE --------------+')
  224 + msg1 = ('| Correct values must be in the interval [0.0, 1.0]. |')
  225 + msg2 = ('| I will convert to the new behavior, but you should |')
  226 + msg3 = ('| fix it in the question. |')
  227 + msg4 = ('+----------------------------------------------------+')
  228 + logger.warning(msg0)
  229 + logger.warning(msg1)
  230 + logger.warning(msg2)
  231 + logger.warning(msg3)
  232 + logger.warning(msg4)
  233 + logger.warning(f'-> please fix "{self["ref"]}"')
  234 +
  235 + # normalize to [0,1]
  236 + self['correct'] = [(x+1)/2 for x in self['correct']]
  237 +
177 # if an option is a list of (right, wrong), pick one 238 # if an option is a list of (right, wrong), pick one
178 options = [] 239 options = []
179 correct = [] 240 correct = []
@@ -182,9 +243,10 @@ class QuestionCheckbox(Question): @@ -182,9 +243,10 @@ class QuestionCheckbox(Question):
182 r = random.randint(0, 1) 243 r = random.randint(0, 1)
183 o = o[r] 244 o = o[r]
184 if r == 1: 245 if r == 1:
185 - c = -c 246 + # c = -c
  247 + c = 1.0 - c
186 options.append(str(o)) 248 options.append(str(o))
187 - correct.append(float(c)) 249 + correct.append(c)
188 250
189 # generate random permutation, e.g. [2,1,4,0,3] 251 # generate random permutation, e.g. [2,1,4,0,3]
190 # and apply to `options` and `correct` 252 # and apply to `options` and `correct`
@@ -202,21 +264,20 @@ class QuestionCheckbox(Question): @@ -202,21 +264,20 @@ class QuestionCheckbox(Question):
202 super().correct() 264 super().correct()
203 265
204 if self['answer'] is not None: 266 if self['answer'] is not None:
205 - sum_abs = sum(abs(p) for p in self['correct'])  
206 - if sum_abs < 1e-6: # case correct [0,...,0] avoid div-by-zero  
207 - self['grade'] = 1.0  
208 - 267 + x = 0.0
  268 + if self['discount']:
  269 + sum_abs = sum(abs(2*p-1) for p in self['correct'])
  270 + for i, p in enumerate(self['correct']):
  271 + x += 2*p-1 if str(i) in self['answer'] else 1-2*p
209 else: 272 else:
210 - x = 0.0  
211 -  
212 - if self['discount']:  
213 - for i, p in enumerate(self['correct']):  
214 - x += p if str(i) in self['answer'] else -p  
215 - else:  
216 - for i, p in enumerate(self['correct']):  
217 - x += p if str(i) in self['answer'] else 0.0 273 + sum_abs = sum(abs(p) for p in self['correct'])
  274 + for i, p in enumerate(self['correct']):
  275 + x += p if str(i) in self['answer'] else 0.0
218 276
  277 + try:
219 self['grade'] = x / sum_abs 278 self['grade'] = x / sum_abs
  279 + except ZeroDivisionError:
  280 + self['grade'] = 1.0 # limit p->0
220 281
221 282
222 # ============================================================================ 283 # ============================================================================
@@ -240,29 +301,35 @@ class QuestionText(Question): @@ -240,29 +301,35 @@ class QuestionText(Question):
240 301
241 # make sure its always a list of possible correct answers 302 # make sure its always a list of possible correct answers
242 if not isinstance(self['correct'], list): 303 if not isinstance(self['correct'], list):
243 - self['correct'] = [self['correct']] 304 + self['correct'] = [str(self['correct'])]
  305 + else:
  306 + # make sure all elements of the list are strings
  307 + self['correct'] = [str(a) for a in self['correct']]
244 308
245 - # make sure all elements of the list are strings  
246 - self['correct'] = [str(a) for a in self['correct']] 309 + for f in self['transform']:
  310 + if f not in ('remove_space', 'trim', 'normalize_space', 'lower',
  311 + 'upper'):
  312 + msg = (f'Unknown transform "{f}" in "{self["ref"]}"')
  313 + raise QuestionException(msg)
247 314
248 - # make sure that the answers are invariant with respect to the filters 315 + # check if answers are invariant with respect to the transforms
249 if any(c != self.transform(c) for c in self['correct']): 316 if any(c != self.transform(c) for c in self['correct']):
250 logger.warning(f'in "{self["ref"]}", correct answers are not ' 317 logger.warning(f'in "{self["ref"]}", correct answers are not '
251 - 'invariant wrt transformations') 318 + 'invariant wrt transformations => never correct')
252 319
253 # ------------------------------------------------------------------------ 320 # ------------------------------------------------------------------------
254 # apply optional filters to the answer 321 # apply optional filters to the answer
255 def transform(self, ans): 322 def transform(self, ans):
256 for f in self['transform']: 323 for f in self['transform']:
257 - if f == 'remove_space': 324 + if f == 'remove_space': # removes all spaces
258 ans = ans.replace(' ', '') 325 ans = ans.replace(' ', '')
259 - elif f == 'trim': 326 + elif f == 'trim': # removes spaces around
260 ans = ans.strip() 327 ans = ans.strip()
261 - elif f == 'normalize_space': 328 + elif f == 'normalize_space': # replaces multiple spaces by one
262 ans = re.sub(r'\s+', ' ', ans.strip()) 329 ans = re.sub(r'\s+', ' ', ans.strip())
263 - elif f == 'lower': 330 + elif f == 'lower': # convert to lowercase
264 ans = ans.lower() 331 ans = ans.lower()
265 - elif f == 'upper': 332 + elif f == 'upper': # convert to uppercase
266 ans = ans.upper() 333 ans = ans.upper()
267 else: 334 else:
268 logger.warning(f'in "{self["ref"]}", unknown transform "{f}"') 335 logger.warning(f'in "{self["ref"]}", unknown transform "{f}"')
@@ -302,8 +369,12 @@ class QuestionTextRegex(Question): @@ -302,8 +369,12 @@ class QuestionTextRegex(Question):
302 if not isinstance(self['correct'], list): 369 if not isinstance(self['correct'], list):
303 self['correct'] = [self['correct']] 370 self['correct'] = [self['correct']]
304 371
305 - # make sure all elements of the list are strings  
306 - self['correct'] = [str(a) for a in self['correct']] 372 + # converts patterns to compiled versions
  373 + try:
  374 + self['correct'] = [re.compile(a) for a in self['correct']]
  375 + except Exception:
  376 + msg = f'Failed to compile regex in "{self["ref"]}"'
  377 + raise QuestionException(msg)
307 378
308 # ------------------------------------------------------------------------ 379 # ------------------------------------------------------------------------
309 def correct(self) -> None: 380 def correct(self) -> None:
@@ -312,12 +383,12 @@ class QuestionTextRegex(Question): @@ -312,12 +383,12 @@ class QuestionTextRegex(Question):
312 self['grade'] = 0.0 383 self['grade'] = 0.0
313 for r in self['correct']: 384 for r in self['correct']:
314 try: 385 try:
315 - if re.match(r, self['answer']): 386 + if r.match(self['answer']):
316 self['grade'] = 1.0 387 self['grade'] = 1.0
317 return 388 return
318 except TypeError: 389 except TypeError:
319 - logger.error(f'While matching regex {self["correct"]} with'  
320 - f' answer "{self["answer"]}".') 390 + logger.error(f'While matching regex {r.pattern} with '
  391 + f'answer "{self["answer"]}".')
321 392
322 393
323 # ============================================================================ 394 # ============================================================================
@@ -339,6 +410,30 @@ class QuestionNumericInterval(Question): @@ -339,6 +410,30 @@ class QuestionNumericInterval(Question):
339 'correct': [1.0, -1.0], # will always return false 410 'correct': [1.0, -1.0], # will always return false
340 })) 411 }))
341 412
  413 + # if only one number n is given, make an interval [n,n]
  414 + if isinstance(self['correct'], (int, float)):
  415 + self['correct'] = [float(self['correct']), float(self['correct'])]
  416 +
  417 + # make sure its a list of two numbers
  418 + elif isinstance(self['correct'], list):
  419 + if len(self['correct']) != 2:
  420 + msg = (f'Numeric interval must be a list with two numbers, in '
  421 + f'{self["ref"]}')
  422 + raise QuestionException(msg)
  423 +
  424 + try:
  425 + self['correct'] = [float(n) for n in self['correct']]
  426 + except Exception:
  427 + msg = (f'Numeric interval must be a list with two numbers, in '
  428 + f'{self["ref"]}')
  429 + raise QuestionException(msg)
  430 +
  431 + # invalid
  432 + else:
  433 + msg = (f'Numeric interval must be a list with two numbers, in '
  434 + f'{self["ref"]}')
  435 + raise QuestionException(msg)
  436 +
342 # ------------------------------------------------------------------------ 437 # ------------------------------------------------------------------------
343 def correct(self) -> None: 438 def correct(self) -> None:
344 super().correct() 439 super().correct()
@@ -520,23 +615,27 @@ class QFactory(object): @@ -520,23 +615,27 @@ class QFactory(object):
520 if q['type'] == 'generator': 615 if q['type'] == 'generator':
521 logger.debug(f' \\_ Running "{q["script"]}".') 616 logger.debug(f' \\_ Running "{q["script"]}".')
522 q.setdefault('args', []) 617 q.setdefault('args', [])
523 - q.setdefault('stdin', '') # FIXME is it really necessary? 618 + q.setdefault('stdin', '')
524 script = path.join(q['path'], q['script']) 619 script = path.join(q['path'], q['script'])
525 out = await run_script_async(script=script, args=q['args'], 620 out = await run_script_async(script=script, args=q['args'],
526 stdin=q['stdin']) 621 stdin=q['stdin'])
527 q.update(out) 622 q.update(out)
528 623
529 - # Finally we create an instance of Question() 624 + # Get class for this question type
530 try: 625 try:
531 - qinstance = self._types[q['type']](QDict(q)) # of matching class  
532 - except QuestionException as e:  
533 - logger.error(e)  
534 - raise e 626 + qclass = self._types[q['type']]
535 except KeyError: 627 except KeyError:
536 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') 628 logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
537 raise 629 raise
538 - else:  
539 - return qinstance 630 +
  631 + # Finally create an instance of Question()
  632 + try:
  633 + qinstance = qclass(QDict(q))
  634 + except QuestionException as e:
  635 + # logger.error(e)
  636 + raise e
  637 +
  638 + return qinstance
540 639
541 # ------------------------------------------------------------------------ 640 # ------------------------------------------------------------------------
542 def generate(self) -> Question: 641 def generate(self) -> Question:
perguntations/test.py
@@ -141,7 +141,7 @@ class TestFactory(dict): @@ -141,7 +141,7 @@ class TestFactory(dict):
141 logger.warning('Undefined title!') 141 logger.warning('Undefined title!')
142 142
143 if self['scale_points']: 143 if self['scale_points']:
144 - logger.info(f'Grades are scaled to the interval [{self["scale_min"]}, {self["scale_max"]}]') 144 + logger.info(f'Grades will be scaled to [{self["scale_min"]}, {self["scale_max"]}]')
145 else: 145 else:
146 logger.info('Grades are just the sum of points defined for the questions, not being scaled.') 146 logger.info('Grades are just the sum of points defined for the questions, not being scaled.')
147 147