Commit 8f643ad6a247b911b08270ee1a9cfd9cd02c2a52
1 parent
e0d5ddfe
Exists in
master
and in
1 other branch
fix error in radio questions where all options right would break
added more checks in radio questions
Showing
7 changed files
with
141 additions
and
87 deletions
Show diff stats
BUGS.md
| 1 | 1 | ||
| 2 | # BUGS | 2 | # BUGS |
| 3 | 3 | ||
| 4 | +- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | ||
| 4 | - nao esta a seguir o max_tries definido no ficheiro de dependencias. | 5 | - nao esta a seguir o max_tries definido no ficheiro de dependencias. |
| 5 | -- registar last_seen e remover os antigos de cada vez que houver um login. | ||
| 6 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) | 6 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) |
| 7 | - double click submits twice. | 7 | - double click submits twice. |
| 8 | -- marking all options right in a radio question breaks! | ||
| 9 | - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks | 8 | - duplo clicks no botao "responder" dessincroniza as questões, ver debounce em https://stackoverflow.com/questions/20281546/how-to-prevent-calling-of-en-event-handler-twice-on-fast-clicks |
| 10 | -- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo) | ||
| 11 | -- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) | ||
| 12 | - devia mostrar timeout para o aluno saber a razao. | 9 | - devia mostrar timeout para o aluno saber a razao. |
| 13 | - permitir configuracao para escolher entre static files locais ou remotos | 10 | - permitir configuracao para escolher entre static files locais ou remotos |
| 14 | - templates question-*.html tem input hidden question_ref que não é usado. remover? | 11 | - templates question-*.html tem input hidden question_ref que não é usado. remover? |
| 15 | -- click numa opcao checkbox fora da checkbox+label não está a funcionar. | ||
| 16 | - shift-enter não está a funcionar | 12 | - shift-enter não está a funcionar |
| 17 | - default prefix should be obtained from each course (yaml conf)? | 13 | - default prefix should be obtained from each course (yaml conf)? |
| 18 | -- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. | ||
| 19 | 14 | ||
| 20 | # TODO | 15 | # TODO |
| 21 | 16 | ||
| 17 | +- checkbox devia ter correct no intervalo [0,1] tal como radio. em caso de desconto a correccção faz 2*x-1. isto permite a mesma semantica nos dois tipos de perguntas. | ||
| 18 | +- registar last_seen e remover os antigos de cada vez que houver um login. | ||
| 22 | - indicar qtos topicos faltam (>=50%) para terminar o curso. | 19 | - indicar qtos topicos faltam (>=50%) para terminar o curso. |
| 23 | -- use run_script_async to run run_script using asyncio.run? | ||
| 24 | -- ao fim de 3 tentativas de login, envial email para aluno com link para definir nova password (com timeout de 5 minutos). | 20 | +- ao fim de 3 tentativas com password errada, envia email com nova password. |
| 25 | - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. | 21 | - mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias. |
| 26 | - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. | 22 | - mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. |
| 27 | - botão não sei... | 23 | - botão não sei... |
| @@ -33,14 +29,16 @@ | @@ -33,14 +29,16 @@ | ||
| 33 | - tabela com perguntas / quantidade de respostas certas/erradas. | 29 | - tabela com perguntas / quantidade de respostas certas/erradas. |
| 34 | - tabela com topicos / quantidade de estrelas. | 30 | - tabela com topicos / quantidade de estrelas. |
| 35 | - pymips: activar/desactivar instruções | 31 | - pymips: activar/desactivar instruções |
| 36 | -- implementar servidor http com redirect para https. | ||
| 37 | -- ao fim de 3 tentativas com password errada, envia email com nova password. | ||
| 38 | - titulos das perguntas não suportam markdown. | 32 | - titulos das perguntas não suportam markdown. |
| 39 | - pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. | 33 | - pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. |
| 40 | - normalizar com perguntations. | 34 | - normalizar com perguntations. |
| 41 | 35 | ||
| 42 | # FIXED | 36 | # FIXED |
| 43 | 37 | ||
| 38 | +- marking all options right in a radio question breaks! | ||
| 39 | +- implementar servidor http com redirect para https. | ||
| 40 | +- tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. | ||
| 41 | +- click numa opcao checkbox fora da checkbox+label não está a funcionar. | ||
| 44 | - mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam centradas em toda a coluna mas apenas na largura do parágrafo. | 42 | - mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam centradas em toda a coluna mas apenas na largura do parágrafo. |
| 45 | - QFactory.generate() devia fazer run da gen_async, ou remover. | 43 | - QFactory.generate() devia fazer run da gen_async, ou remover. |
| 46 | - classificacoes so devia mostrar os que ja fizeram alguma coisa | 44 | - classificacoes so devia mostrar os que ja fizeram alguma coisa |
aprendizations/learnapp.py
| @@ -16,7 +16,7 @@ import sqlalchemy as sa | @@ -16,7 +16,7 @@ import sqlalchemy as sa | ||
| 16 | 16 | ||
| 17 | # this project | 17 | # this project |
| 18 | from .models import Student, Answer, Topic, StudentTopic | 18 | from .models import Student, Answer, Topic, StudentTopic |
| 19 | -from .questions import Question, QFactory, QDict | 19 | +from .questions import Question, QFactory, QDict, QuestionException |
| 20 | from .student import StudentState | 20 | from .student import StudentState |
| 21 | from .tools import load_yaml | 21 | from .tools import load_yaml |
| 22 | 22 | ||
| @@ -100,7 +100,6 @@ class LearnApp(object): | @@ -100,7 +100,6 @@ class LearnApp(object): | ||
| 100 | # if graph has topics that are not in the database, add them | 100 | # if graph has topics that are not in the database, add them |
| 101 | self.add_missing_topics(self.deps.nodes()) | 101 | self.add_missing_topics(self.deps.nodes()) |
| 102 | 102 | ||
| 103 | - | ||
| 104 | if check: | 103 | if check: |
| 105 | self.sanity_check_questions() | 104 | self.sanity_check_questions() |
| 106 | 105 | ||
| @@ -113,8 +112,8 @@ class LearnApp(object): | @@ -113,8 +112,8 @@ class LearnApp(object): | ||
| 113 | logger.debug(f'checking {qref}...') | 112 | logger.debug(f'checking {qref}...') |
| 114 | try: | 113 | try: |
| 115 | q = self.factory[qref].generate() | 114 | q = self.factory[qref].generate() |
| 116 | - except Exception: | ||
| 117 | - logger.error(f'Failed to generate "{qref}".') | 115 | + except QuestionException as e: |
| 116 | + logger.error(e) | ||
| 118 | errors += 1 | 117 | errors += 1 |
| 119 | continue # to next question | 118 | continue # to next question |
| 120 | 119 | ||
| @@ -137,7 +136,7 @@ class LearnApp(object): | @@ -137,7 +136,7 @@ class LearnApp(object): | ||
| 137 | continue # to next test | 136 | continue # to next test |
| 138 | 137 | ||
| 139 | if errors > 0: | 138 | if errors > 0: |
| 140 | - logger.error(f'{errors:>6} errors found.') | 139 | + logger.error(f'{errors:>6} error(s) found.') |
| 141 | raise LearnException('Sanity checks') | 140 | raise LearnException('Sanity checks') |
| 142 | else: | 141 | else: |
| 143 | logger.info(' 0 errors found.') | 142 | logger.info(' 0 errors found.') |
| @@ -345,7 +344,7 @@ class LearnApp(object): | @@ -345,7 +344,7 @@ class LearnApp(object): | ||
| 345 | logger.info(f'{m:6} topics') | 344 | logger.info(f'{m:6} topics') |
| 346 | logger.info(f'{q:6} answers') | 345 | logger.info(f'{q:6} answers') |
| 347 | 346 | ||
| 348 | - # ============================================================================ | 347 | + # ======================================================================== |
| 349 | # Populates a digraph. | 348 | # Populates a digraph. |
| 350 | # | 349 | # |
| 351 | # Nodes are the topic references e.g. 'my/topic' | 350 | # Nodes are the topic references e.g. 'my/topic' |
| @@ -400,7 +399,7 @@ class LearnApp(object): | @@ -400,7 +399,7 @@ class LearnApp(object): | ||
| 400 | def make_factory(self) -> Dict[str, QFactory]: | 399 | def make_factory(self) -> Dict[str, QFactory]: |
| 401 | 400 | ||
| 402 | logger.info('Building questions factory:') | 401 | logger.info('Building questions factory:') |
| 403 | - factory: Dict[str, QFactory] = {} | 402 | + factory = {} |
| 404 | g = self.deps | 403 | g = self.deps |
| 405 | for tref in g.nodes(): | 404 | for tref in g.nodes(): |
| 406 | t = g.nodes[tref] | 405 | t = g.nodes[tref] |
| @@ -418,7 +417,7 @@ class LearnApp(object): | @@ -418,7 +417,7 @@ class LearnApp(object): | ||
| 418 | # within the file | 417 | # within the file |
| 419 | for i, q in enumerate(questions): | 418 | for i, q in enumerate(questions): |
| 420 | qref = q.get('ref', str(i)) # ref or number | 419 | qref = q.get('ref', str(i)) # ref or number |
| 421 | - q['ref'] = tref + ':' + qref | 420 | + q['ref'] = f'{tref}:{qref}' |
| 422 | q['path'] = topicpath | 421 | q['path'] = topicpath |
| 423 | q.setdefault('append_wrong', t['append_wrong']) | 422 | q.setdefault('append_wrong', t['append_wrong']) |
| 424 | 423 |
aprendizations/questions.py
| @@ -72,7 +72,6 @@ class QuestionRadio(Question): | @@ -72,7 +72,6 @@ class QuestionRadio(Question): | ||
| 72 | ''' | 72 | ''' |
| 73 | 73 | ||
| 74 | # ------------------------------------------------------------------------ | 74 | # ------------------------------------------------------------------------ |
| 75 | - # FIXME marking all options right breaks | ||
| 76 | def __init__(self, q: QDict) -> None: | 75 | def __init__(self, q: QDict) -> None: |
| 77 | super().__init__(q) | 76 | super().__init__(q) |
| 78 | 77 | ||
| @@ -86,18 +85,46 @@ class QuestionRadio(Question): | @@ -86,18 +85,46 @@ class QuestionRadio(Question): | ||
| 86 | 'max_tries': (n + 3) // 4 # 1 try for each 4 options | 85 | 'max_tries': (n + 3) // 4 # 1 try for each 4 options |
| 87 | })) | 86 | })) |
| 88 | 87 | ||
| 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!) | 88 | + # check correct bounds and convert int to list, |
| 89 | + # e.g. correct: 2 --> correct: [0,0,1,0,0] | ||
| 91 | if isinstance(self['correct'], int): | 90 | if isinstance(self['correct'], int): |
| 91 | + if not (0 <= self['correct'] < n): | ||
| 92 | + msg = (f'Correct option not in range 0..{n-1} in ' | ||
| 93 | + f'"{self["ref"]}"') | ||
| 94 | + raise QuestionException(msg) | ||
| 95 | + | ||
| 92 | self['correct'] = [1.0 if x == self['correct'] else 0.0 | 96 | self['correct'] = [1.0 if x == self['correct'] else 0.0 |
| 93 | for x in range(n)] | 97 | for x in range(n)] |
| 94 | 98 | ||
| 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 | - | 99 | + elif isinstance(self['correct'], list): |
| 100 | + # must match number of options | ||
| 101 | + if len(self['correct']) != n: | ||
| 102 | + msg = (f'Incompatible sizes: {n} options vs ' | ||
| 103 | + f'{len(self["correct"])} correct in "{self["ref"]}"') | ||
| 104 | + raise QuestionException(msg) | ||
| 105 | + # make sure is a list of floats | ||
| 106 | + try: | ||
| 107 | + self['correct'] = [float(x) for x in self['correct']] | ||
| 108 | + except (ValueError, TypeError): | ||
| 109 | + msg = (f'Correct list must contain numbers [0.0, 1.0] or ' | ||
| 110 | + f'booleans in "{self["ref"]}"') | ||
| 111 | + raise QuestionException(msg) | ||
| 112 | + | ||
| 113 | + # check grade boundaries | ||
| 114 | + if self['discount'] and not all(0.0 <= x <= 1.0 | ||
| 115 | + for x in self['correct']): | ||
| 116 | + msg = (f'If discount=true, correct values must be in the ' | ||
| 117 | + f'interval [0.0, 1.0] in "{self["ref"]}"') | ||
| 118 | + raise QuestionException(msg) | ||
| 119 | + | ||
| 120 | + # at least one correct option | ||
| 121 | + if all(x < 1.0 for x in self['correct']): | ||
| 122 | + msg = (f'At least one correct option is required in ' | ||
| 123 | + f'"{self["ref"]}"') | ||
| 124 | + raise QuestionException(msg) | ||
| 125 | + | ||
| 126 | + # If shuffle==false, all options are shown as defined | ||
| 127 | + # otherwise, select 1 correct and choose a few wrong ones | ||
| 101 | if self['shuffle']: | 128 | if self['shuffle']: |
| 102 | # lists with indices of right and wrong options | 129 | # lists with indices of right and wrong options |
| 103 | right = [i for i in range(n) if self['correct'][i] >= 1] | 130 | right = [i for i in range(n) if self['correct'][i] >= 1] |
| @@ -123,7 +150,7 @@ class QuestionRadio(Question): | @@ -123,7 +150,7 @@ class QuestionRadio(Question): | ||
| 123 | # final shuffle of the options | 150 | # final shuffle of the options |
| 124 | perm = random.sample(range(self['choose']), k=self['choose']) | 151 | perm = random.sample(range(self['choose']), k=self['choose']) |
| 125 | self['options'] = [str(options[i]) for i in perm] | 152 | self['options'] = [str(options[i]) for i in perm] |
| 126 | - self['correct'] = [float(correct[i]) for i in perm] | 153 | + self['correct'] = [correct[i] for i in perm] |
| 127 | 154 | ||
| 128 | # ------------------------------------------------------------------------ | 155 | # ------------------------------------------------------------------------ |
| 129 | # can assign negative grades for wrong answers | 156 | # can assign negative grades for wrong answers |
| @@ -131,10 +158,13 @@ class QuestionRadio(Question): | @@ -131,10 +158,13 @@ class QuestionRadio(Question): | ||
| 131 | super().correct() | 158 | super().correct() |
| 132 | 159 | ||
| 133 | if self['answer'] is not None: | 160 | 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 | 161 | + x = self['correct'][int(self['answer'])] # get grade of the answer |
| 162 | + n = len(self['options']) | ||
| 163 | + x_aver = sum(self['correct']) / n # expected value of grade | ||
| 164 | + | ||
| 165 | + # note: there are no numerical errors when summing 1.0s so the | ||
| 166 | + # x_aver can be exactly 1.0 if all options are right | ||
| 167 | + if self['discount'] and x_aver != 1.0: | ||
| 138 | x = (x - x_aver) / (1.0 - x_aver) | 168 | x = (x - x_aver) / (1.0 - x_aver) |
| 139 | self['grade'] = x | 169 | self['grade'] = x |
| 140 | 170 | ||
| @@ -168,12 +198,32 @@ class QuestionCheckbox(Question): | @@ -168,12 +198,32 @@ class QuestionCheckbox(Question): | ||
| 168 | 'max_tries': max(1, min(n - 1, 3)) | 198 | 'max_tries': max(1, min(n - 1, 3)) |
| 169 | })) | 199 | })) |
| 170 | 200 | ||
| 201 | + # must be a list of numbers | ||
| 202 | + if not isinstance(self['correct'], list): | ||
| 203 | + msg = 'Correct must be a list of numbers or booleans' | ||
| 204 | + raise QuestionException(msg) | ||
| 205 | + | ||
| 206 | + # must match number of options | ||
| 171 | if len(self['correct']) != n: | 207 | 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) | 208 | + msg = (f'Incompatible sizes: {n} options vs ' |
| 209 | + f'{len(self["correct"])} correct in "{self["ref"]}"') | ||
| 175 | raise QuestionException(msg) | 210 | raise QuestionException(msg) |
| 176 | 211 | ||
| 212 | + # make sure is a list of floats | ||
| 213 | + try: | ||
| 214 | + self['correct'] = [float(x) for x in self['correct']] | ||
| 215 | + except (ValueError, TypeError): | ||
| 216 | + msg = (f'Correct list must contain numbers or ' | ||
| 217 | + f'booleans in "{self["ref"]}"') | ||
| 218 | + raise QuestionException(msg) | ||
| 219 | + | ||
| 220 | + # check grade boundaries (FUTURE) | ||
| 221 | + # if self['discount'] and not all(0.0 <= x <= 1.0 | ||
| 222 | + # for x in self['correct']): | ||
| 223 | + # msg = (f'If discount=true, correct values must be in the ' | ||
| 224 | + # f'interval [0.0, 1.0] in "{self["ref"]}"') | ||
| 225 | + # raise QuestionException(msg) | ||
| 226 | + | ||
| 177 | # if an option is a list of (right, wrong), pick one | 227 | # if an option is a list of (right, wrong), pick one |
| 178 | options = [] | 228 | options = [] |
| 179 | correct = [] | 229 | correct = [] |
| @@ -526,17 +576,21 @@ class QFactory(object): | @@ -526,17 +576,21 @@ class QFactory(object): | ||
| 526 | stdin=q['stdin']) | 576 | stdin=q['stdin']) |
| 527 | q.update(out) | 577 | q.update(out) |
| 528 | 578 | ||
| 529 | - # Finally we create an instance of Question() | 579 | + # Get class for this question type |
| 530 | try: | 580 | try: |
| 531 | - qinstance = self._types[q['type']](QDict(q)) # of matching class | ||
| 532 | - except QuestionException as e: | ||
| 533 | - logger.error(e) | ||
| 534 | - raise e | 581 | + qclass = self._types[q['type']] |
| 535 | except KeyError: | 582 | except KeyError: |
| 536 | logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') | 583 | logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') |
| 537 | raise | 584 | raise |
| 538 | - else: | ||
| 539 | - return qinstance | 585 | + |
| 586 | + # Finally create an instance of Question() | ||
| 587 | + try: | ||
| 588 | + qinstance = qclass(QDict(q)) | ||
| 589 | + except QuestionException as e: | ||
| 590 | + # logger.error(e) | ||
| 591 | + raise e | ||
| 592 | + | ||
| 593 | + return qinstance | ||
| 540 | 594 | ||
| 541 | # ------------------------------------------------------------------------ | 595 | # ------------------------------------------------------------------------ |
| 542 | def generate(self) -> Question: | 596 | def generate(self) -> Question: |
aprendizations/serve.py
| @@ -254,7 +254,6 @@ class TopicHandler(BaseHandler): | @@ -254,7 +254,6 @@ class TopicHandler(BaseHandler): | ||
| 254 | class FileHandler(BaseHandler): | 254 | class FileHandler(BaseHandler): |
| 255 | @tornado.web.authenticated | 255 | @tornado.web.authenticated |
| 256 | async def get(self, filename): | 256 | async def get(self, filename): |
| 257 | - # logger.debug(f'[FileHandler] {filename}') | ||
| 258 | uid = self.current_user | 257 | uid = self.current_user |
| 259 | public_dir = self.learn.get_current_public_dir(uid) | 258 | public_dir = self.learn.get_current_public_dir(uid) |
| 260 | filepath = path.expanduser(path.join(public_dir, filename)) | 259 | filepath = path.expanduser(path.join(public_dir, filename)) |
| @@ -331,7 +330,7 @@ class QuestionHandler(BaseHandler): | @@ -331,7 +330,7 @@ class QuestionHandler(BaseHandler): | ||
| 331 | user = self.current_user | 330 | user = self.current_user |
| 332 | answer = self.get_body_arguments('answer') # list | 331 | answer = self.get_body_arguments('answer') # list |
| 333 | qid = self.get_body_arguments('qid')[0] | 332 | qid = self.get_body_arguments('qid')[0] |
| 334 | - logger.debug(f'[QuestionHandler] user={user}, answer={answer}') | 333 | + logger.debug(f'[QuestionHandler] answer={answer}') |
| 335 | 334 | ||
| 336 | # --- check if browser opened different questions simultaneously | 335 | # --- check if browser opened different questions simultaneously |
| 337 | if qid != self.learn.get_current_question_id(user): | 336 | if qid != self.learn.get_current_question_id(user): |
config/logger-debug.yaml
| @@ -2,50 +2,52 @@ | @@ -2,50 +2,52 @@ | ||
| 2 | version: 1 | 2 | version: 1 |
| 3 | 3 | ||
| 4 | formatters: | 4 | formatters: |
| 5 | - void: | ||
| 6 | - format: '' | ||
| 7 | - standard: | ||
| 8 | - format: '%(asctime)s | %(thread)-15d | %(levelname)-8s | %(module)-10s | %(funcName)-20s | %(message)s' | ||
| 9 | - # datefmt: '%H:%M:%S' | 5 | + void: |
| 6 | + format: '' | ||
| 7 | + standard: | ||
| 8 | + format: '%(asctime)s | %(levelname)-8s | %(module)-10s | %(funcName)-22s | | ||
| 9 | + %(message)s' | ||
| 10 | + # | %(thread)-15d | ||
| 11 | + # datefmt: '%H:%M:%S' | ||
| 10 | 12 | ||
| 11 | handlers: | 13 | handlers: |
| 12 | - default: | ||
| 13 | - level: 'DEBUG' | ||
| 14 | - class: 'logging.StreamHandler' | ||
| 15 | - formatter: 'standard' | ||
| 16 | - stream: 'ext://sys.stdout' | 14 | + default: |
| 15 | + level: 'DEBUG' | ||
| 16 | + class: 'logging.StreamHandler' | ||
| 17 | + formatter: 'standard' | ||
| 18 | + stream: 'ext://sys.stdout' | ||
| 17 | 19 | ||
| 18 | loggers: | 20 | loggers: |
| 19 | - '': | ||
| 20 | - handlers: ['default'] | ||
| 21 | - level: 'DEBUG' | ||
| 22 | - | ||
| 23 | - 'aprendizations.factory': | ||
| 24 | - handlers: ['default'] | ||
| 25 | - level: 'DEBUG' | ||
| 26 | - propagate: false | ||
| 27 | - | ||
| 28 | - 'aprendizations.student': | ||
| 29 | - handlers: ['default'] | ||
| 30 | - level: 'DEBUG' | ||
| 31 | - propagate: false | ||
| 32 | - | ||
| 33 | - 'aprendizations.learnapp': | ||
| 34 | - handlers: ['default'] | ||
| 35 | - level: 'DEBUG' | ||
| 36 | - propagate: false | ||
| 37 | - | ||
| 38 | - 'aprendizations.questions': | ||
| 39 | - handlers: ['default'] | ||
| 40 | - level: 'DEBUG' | ||
| 41 | - propagate: false | ||
| 42 | - | ||
| 43 | - 'aprendizations.tools': | ||
| 44 | - handlers: ['default'] | ||
| 45 | - level: 'DEBUG' | ||
| 46 | - propagate: false | ||
| 47 | - | ||
| 48 | - 'aprendizations.serve': | ||
| 49 | - handlers: ['default'] | ||
| 50 | - level: 'DEBUG' | ||
| 51 | - propagate: false | 21 | + '': |
| 22 | + handlers: ['default'] | ||
| 23 | + level: 'DEBUG' | ||
| 24 | + | ||
| 25 | + 'aprendizations.factory': | ||
| 26 | + handlers: ['default'] | ||
| 27 | + level: 'DEBUG' | ||
| 28 | + propagate: false | ||
| 29 | + | ||
| 30 | + 'aprendizations.student': | ||
| 31 | + handlers: ['default'] | ||
| 32 | + level: 'DEBUG' | ||
| 33 | + propagate: false | ||
| 34 | + | ||
| 35 | + 'aprendizations.learnapp': | ||
| 36 | + handlers: ['default'] | ||
| 37 | + level: 'DEBUG' | ||
| 38 | + propagate: false | ||
| 39 | + | ||
| 40 | + 'aprendizations.questions': | ||
| 41 | + handlers: ['default'] | ||
| 42 | + level: 'DEBUG' | ||
| 43 | + propagate: false | ||
| 44 | + | ||
| 45 | + 'aprendizations.tools': | ||
| 46 | + handlers: ['default'] | ||
| 47 | + level: 'DEBUG' | ||
| 48 | + propagate: false | ||
| 49 | + | ||
| 50 | + 'aprendizations.serve': | ||
| 51 | + handlers: ['default'] | ||
| 52 | + level: 'DEBUG' | ||
| 53 | + propagate: false |
demo/astronomy/solar-system/questions.yaml
| @@ -28,6 +28,7 @@ | @@ -28,6 +28,7 @@ | ||
| 28 | - Têm todos o mesmo tamanho | 28 | - Têm todos o mesmo tamanho |
| 29 | # opcional | 29 | # opcional |
| 30 | correct: 2 | 30 | correct: 2 |
| 31 | + # discount: true | ||
| 31 | shuffle: false | 32 | shuffle: false |
| 32 | solution: | | 33 | solution: | |
| 33 | O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | 34 | O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas |
demo/math/addition/questions.yaml
| @@ -5,6 +5,7 @@ | @@ -5,6 +5,7 @@ | ||
| 5 | script: addition-two-digits.py | 5 | script: addition-two-digits.py |
| 6 | args: [10, 20] | 6 | args: [10, 20] |
| 7 | 7 | ||
| 8 | +# --------------------------------------------------------------------------- | ||
| 8 | - type: checkbox | 9 | - type: checkbox |
| 9 | ref: addition-properties | 10 | ref: addition-properties |
| 10 | title: Propriedades da adição | 11 | title: Propriedades da adição |