Commit 6cdd62d765c73a1433d5bace812d7986004b2848
1 parent
f97d3331
Exists in
master
and in
1 other branch
- separated questions and question factory into two separate files.
Showing
4 changed files
with
12 additions
and
246 deletions
Show diff stats
BUGS.md
... | ... | @@ -4,8 +4,6 @@ |
4 | 4 | - fazer renderer para formulas com mathjax serverside (mathjax-node). |
5 | 5 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg |
6 | 6 | - fazer renderer para linguagem assembly mips? |
7 | -- converter markdown para mistune. | |
8 | -- qual a diferenca entre md_to_html e md_to_html_review, parece desnecessario haver dois. | |
9 | 7 | - servir imagens das perguntas |
10 | 8 | - hints nao funciona |
11 | 9 | - uniformizar question.py com a de aprendizations... |
... | ... | @@ -22,7 +20,7 @@ |
22 | 20 | npm install mathjax-node mathjax-node-cli # pacotes em ~/node_modules |
23 | 21 | node_modules/mathjax-node-cli/bin/tex2svg '\sqrt{x}' |
24 | 22 | usar isto para gerar svg que passa a fazer parte do texto da pergunta (markdown suporta tags svg?) |
25 | - fazer funçao tex() que recebe formula e converte para svg. exemplo: | |
23 | + fazer funçao tex() que recebe formula e converte para svg. exemplo: | |
26 | 24 | fr'''A formula é {tex("\sqrt{x]}")}''' |
27 | 25 | |
28 | 26 | - Gerar pdf's com todos os testes no final (pdfkit). |
... | ... | @@ -41,6 +39,8 @@ |
41 | 39 | |
42 | 40 | # FIXED |
43 | 41 | |
42 | +- qual a diferenca entre md_to_html e md_to_html_review, parece desnecessario haver dois. | |
43 | +- converter markdown para mistune. | |
44 | 44 | - como alterar configuracao para mostrar logs de debug? |
45 | 45 | - espaco no final das tabelas. |
46 | 46 | - total do teste aparece negativo. | ... | ... |
demo/questions/questions-tutorial.yaml
... | ... | @@ -5,10 +5,10 @@ |
5 | 5 | text: | |
6 | 6 | Texto informativo. Não conta para avaliação. |
7 | 7 | |
8 | - A Distribuição gaussiana $\mathcal{N}(x\mid\mu,\sigma^2)$ é definida por | |
8 | + A distribuição gaussiana $\mathcal{N}(x\mid\mu,\sigma^2)$ é definida por | |
9 | 9 | |
10 | 10 | $$ |
11 | - p(x) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{(x-\mu)^2}{2\sigma^2}}. | |
11 | + p(x) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\tfrac{1}{2}\tfrac{(x-\mu)^2}{\sigma^2}}. | |
12 | 12 | $$ |
13 | 13 | |
14 | 14 | # --------------------------------------------------------------------------- |
... | ... | @@ -20,10 +20,10 @@ |
20 | 20 | Texto positivo (sucesso). Não conta para avaliação. |
21 | 21 | |
22 | 22 | ```C |
23 | - int main() { | |
24 | - printf("Hello world!"); | |
25 | - return 0; // comentario | |
26 | - } | |
23 | + int main() { | |
24 | + printf("Hello world!"); | |
25 | + return 0; // comentario | |
26 | + } | |
27 | 27 | ``` |
28 | 28 | |
29 | 29 | Inline `code`. | ... | ... |
questions.py
1 | 1 | |
2 | -# We start with an empty QuestionFactory() that will be populated with | |
3 | -# question generators that we can load from YAML files. | |
4 | -# To generate an instance of a question we use the method generate(ref) where | |
5 | -# the argument is the reference of the question we wish to produce. | |
6 | -# | |
7 | -# Example: | |
8 | -# | |
9 | -# # read everything from question files | |
10 | -# factory = QuestionFactory() | |
11 | -# factory.load_files(['file1.yaml', 'file1.yaml'], '/path/to') | |
12 | -# | |
13 | -# question = factory.generate('some_ref') | |
14 | -# | |
15 | -# # experiment answering one question and correct it | |
16 | -# question['answer'] = 42 # insert answer | |
17 | -# grade = question.correct() # correct answer | |
18 | - | |
19 | -# An instance of an actual question is an object that inherits from Question() | |
20 | -# | |
21 | -# Question - base class inherited by other classes | |
22 | -# QuestionRadio - single choice from a list of options | |
23 | -# QuestionCheckbox - multiple choice, equivalent to multiple true/false | |
24 | -# QuestionText - line of text compared to a list of acceptable answers | |
25 | -# QuestionTextRegex - line of text matched against a regular expression | |
26 | -# QuestionTextArea - corrected by an external program | |
27 | -# QuestionInformation - not a question, just a box with content | |
28 | 2 | |
29 | 3 | # base |
30 | 4 | import random |
31 | 5 | import re |
32 | 6 | from os import path |
33 | 7 | import logging |
34 | -import sys | |
35 | 8 | |
36 | 9 | # packages |
37 | 10 | import yaml |
38 | 11 | |
39 | 12 | # this project |
40 | -from tools import load_yaml, run_script | |
13 | +from tools import run_script | |
14 | + | |
41 | 15 | |
42 | 16 | # regular expressions in yaml files, e.g. correct: !regex '[aA]zul' |
43 | 17 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) |
... | ... | @@ -46,12 +20,6 @@ yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) |
46 | 20 | # setup logger for this module |
47 | 21 | logger = logging.getLogger(__name__) |
48 | 22 | |
49 | -# =========================================================================== | |
50 | -class QuestionFactoryException(Exception): | |
51 | - pass | |
52 | - | |
53 | - | |
54 | - | |
55 | 23 | |
56 | 24 | # =========================================================================== |
57 | 25 | # Questions derived from Question are already instantiated and ready to be |
... | ... | @@ -389,204 +357,3 @@ class QuestionInformation(Question): |
389 | 357 | super().correct() |
390 | 358 | self['grade'] = 1.0 # always "correct" but points should be zero! |
391 | 359 | return self['grade'] |
392 | - | |
393 | - | |
394 | - | |
395 | - | |
396 | -# =========================================================================== | |
397 | -# This class contains a pool of questions generators from which particular | |
398 | -# Question() instances are generated using QuestionsFactory.generate(ref). | |
399 | -# =========================================================================== | |
400 | -class QuestionFactory(dict): | |
401 | - _types = { | |
402 | - 'radio' : QuestionRadio, | |
403 | - 'checkbox' : QuestionCheckbox, | |
404 | - 'text' : QuestionText, | |
405 | - 'text-regex': QuestionTextRegex, | |
406 | - 'numeric-interval': QuestionNumericInterval, | |
407 | - 'textarea' : QuestionTextArea, | |
408 | - # -- informative panels -- | |
409 | - 'information': QuestionInformation, 'info': QuestionInformation, | |
410 | - 'warning' : QuestionInformation, 'warn': QuestionInformation, | |
411 | - 'alert' : QuestionInformation, | |
412 | - 'success' : QuestionInformation, | |
413 | - } | |
414 | - | |
415 | - | |
416 | - # ----------------------------------------------------------------------- | |
417 | - def __init__(self): | |
418 | - super().__init__() | |
419 | - | |
420 | - # ----------------------------------------------------------------------- | |
421 | - # Add single question provided in a dictionary. | |
422 | - # After this, each question will have at least 'ref' and 'type' keys. | |
423 | - # ----------------------------------------------------------------------- | |
424 | - def add_question(self, question): | |
425 | - # if missing defaults to ref='/path/file.yaml:3' | |
426 | - question.setdefault('ref', f'{question["filename"]}:{question["index"]}') | |
427 | - | |
428 | - if question['ref'] in self: | |
429 | - logger.error(f'Duplicate reference "{question["ref"]}" replaces the original.') | |
430 | - | |
431 | - question.setdefault('type', 'information') | |
432 | - | |
433 | - self[question['ref']] = question | |
434 | - logger.debug(f'Added question "{question["ref"]}" to the pool.') | |
435 | - | |
436 | - # ----------------------------------------------------------------------- | |
437 | - # load single YAML questions file | |
438 | - # ----------------------------------------------------------------------- | |
439 | - def load_file(self, pathfile, questions_dir=''): | |
440 | - # questions_dir is a base directory | |
441 | - # pathfile is a path of a file under the questions_dir | |
442 | - # For example, if | |
443 | - # pathfile = 'math/questions.yaml' | |
444 | - # questions_dir = '/home/john/questions' | |
445 | - # then the complete path is | |
446 | - # fullpath = '/home/john/questions/math/questions.yaml' | |
447 | - fullpath = path.normpath(path.join(questions_dir, pathfile)) | |
448 | - (dirname, filename) = path.split(fullpath) | |
449 | - | |
450 | - questions = load_yaml(fullpath, default=[]) | |
451 | - | |
452 | - for i, q in enumerate(questions): | |
453 | - try: | |
454 | - q.update({ | |
455 | - 'filename': filename, | |
456 | - 'path': dirname, | |
457 | - 'index': i # position in the file, 0 based | |
458 | - }) | |
459 | - except AttributeError: | |
460 | - logger.error(f'Question {pathfile}:{i} is not a dictionary. Skipped!') | |
461 | - else: | |
462 | - self.add_question(q) | |
463 | - | |
464 | - logger.info(f'Loaded {len(self)} questions from "{pathfile}".') | |
465 | - | |
466 | - # ----------------------------------------------------------------------- | |
467 | - # load multiple YAML question files | |
468 | - # ----------------------------------------------------------------------- | |
469 | - def load_files(self, files, questions_dir=''): | |
470 | - for filename in files: | |
471 | - self.load_file(filename, questions_dir) | |
472 | - | |
473 | - # ----------------------------------------------------------------------- | |
474 | - # Given a ref returns an instance of a descendent of Question(), | |
475 | - # i.e. a question object (radio, checkbox, ...). | |
476 | - # ----------------------------------------------------------------------- | |
477 | - def generate(self, ref): | |
478 | - | |
479 | - # Shallow copy so that script generated questions will not replace | |
480 | - # the original generators | |
481 | - try: | |
482 | - q = self[ref].copy() | |
483 | - except KeyError: #FIXME exception type? | |
484 | - logger.error(f'Can\'t find question "{ref}".') | |
485 | - raise QuestionFactoryException() | |
486 | - | |
487 | - # If question is of generator type, an external program will be run | |
488 | - # which will print a valid question in yaml format to stdout. This | |
489 | - # output is then converted to a dictionary and `q` becomes that dict. | |
490 | - if q['type'] == 'generator': | |
491 | - logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | |
492 | - q.setdefault('arg', '') # optional arguments will be sent to stdin | |
493 | - script = path.normpath(path.join(q['path'], q['script'])) | |
494 | - out = run_script(script=script, stdin=q['arg']) | |
495 | - try: | |
496 | - q.update(out) | |
497 | - except: | |
498 | - q.update({ | |
499 | - 'type': 'alert', | |
500 | - 'title': 'Erro interno', | |
501 | - 'text': 'Ocorreu um erro a gerar esta pergunta.' | |
502 | - }) | |
503 | - # The generator was replaced by a question but not yet instantiated | |
504 | - | |
505 | - # Finally we create an instance of Question() | |
506 | - try: | |
507 | - qinstance = self._types[q['type']](q) # instance with correct class | |
508 | - except KeyError as e: | |
509 | - logger.error(f'Unknown type "{q["type"]}" in "{q["filename"]}:{q["ref"]}".') | |
510 | - raise e | |
511 | - except: | |
512 | - logger.error(f'Failed to create question "{q["ref"]}" from file "{q["filename"]}".') | |
513 | - raise | |
514 | - else: | |
515 | - logger.debug(f'Generated question "{ref}".') | |
516 | - return qinstance | |
517 | - | |
518 | - | |
519 | - | |
520 | - | |
521 | - | |
522 | - | |
523 | - | |
524 | - | |
525 | - | |
526 | - | |
527 | - | |
528 | - | |
529 | - | |
530 | - | |
531 | - | |
532 | - | |
533 | - | |
534 | - | |
535 | - | |
536 | - | |
537 | - | |
538 | - | |
539 | - | |
540 | - | |
541 | - | |
542 | -# =========================================================================== | |
543 | -# Question Factory | |
544 | -# =========================================================================== | |
545 | -# class QFactory(object): | |
546 | -# # Depending on the type of question, a different question class will be | |
547 | -# # instantiated. All these classes derive from the base class `Question`. | |
548 | -# _types = { | |
549 | -# 'radio' : QuestionRadio, | |
550 | -# 'checkbox' : QuestionCheckbox, | |
551 | -# 'text' : QuestionText, | |
552 | -# 'text_regex': QuestionTextRegex, 'text-regex': QuestionTextRegex, | |
553 | -# 'text_numeric': QuestionTextNumeric, 'text-numeric': QuestionTextNumeric, | |
554 | -# 'textarea' : QuestionTextArea, | |
555 | -# # -- informative panels -- | |
556 | -# 'information': QuestionInformation, 'info': QuestionInformation, | |
557 | -# 'warning' : QuestionInformation, 'warn': QuestionInformation, | |
558 | -# 'alert' : QuestionInformation, | |
559 | -# 'success' : QuestionInformation, | |
560 | -# } | |
561 | - | |
562 | -# def __init__(self, question_dict): | |
563 | -# self.question = question_dict | |
564 | - | |
565 | -# # ----------------------------------------------------------------------- | |
566 | -# # Given a ref returns an instance of a descendent of Question(), | |
567 | -# # i.e. a question object (radio, checkbox, ...). | |
568 | -# # ----------------------------------------------------------------------- | |
569 | -# def generate(self): | |
570 | -# logger.debug(f'Generating "{self.question["ref"]}"') | |
571 | -# # Shallow copy so that script generated questions will not replace | |
572 | -# # the original generators | |
573 | -# q = self.question.copy() | |
574 | - | |
575 | -# # If question is of generator type, an external program will be run | |
576 | -# # which will print a valid question in yaml format to stdout. This | |
577 | -# # output is then yaml parsed into a dictionary `q`. | |
578 | -# if q['type'] == 'generator': | |
579 | -# logger.debug(f' \_ Running script "{q["script"]}"...') | |
580 | -# q.setdefault('arg', '') # optional arguments will be sent to stdin | |
581 | -# script = path.join(q['path'], q['script']) | |
582 | -# out = run_script(script=script, stdin=q['arg']) | |
583 | -# q.update(out) | |
584 | - | |
585 | -# # Finally we create an instance of Question() | |
586 | -# try: | |
587 | -# qinstance = self._types[q['type']](q) # instance with correct class | |
588 | -# except KeyError as e: | |
589 | -# logger.error(f'Failed to generate question "{q["ref"]}"') | |
590 | -# raise e | |
591 | -# else: | |
592 | -# return qinstance | ... | ... |