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,8 +4,6 @@ | ||
| 4 | - fazer renderer para formulas com mathjax serverside (mathjax-node). | 4 | - fazer renderer para formulas com mathjax serverside (mathjax-node). |
| 5 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg | 5 | - fazer renderer para imagens, com links /file?ref=xpto;name=zzz.jpg |
| 6 | - fazer renderer para linguagem assembly mips? | 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 | - servir imagens das perguntas | 7 | - servir imagens das perguntas |
| 10 | - hints nao funciona | 8 | - hints nao funciona |
| 11 | - uniformizar question.py com a de aprendizations... | 9 | - uniformizar question.py com a de aprendizations... |
| @@ -22,7 +20,7 @@ | @@ -22,7 +20,7 @@ | ||
| 22 | npm install mathjax-node mathjax-node-cli # pacotes em ~/node_modules | 20 | npm install mathjax-node mathjax-node-cli # pacotes em ~/node_modules |
| 23 | node_modules/mathjax-node-cli/bin/tex2svg '\sqrt{x}' | 21 | node_modules/mathjax-node-cli/bin/tex2svg '\sqrt{x}' |
| 24 | usar isto para gerar svg que passa a fazer parte do texto da pergunta (markdown suporta tags svg?) | 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 | fr'''A formula é {tex("\sqrt{x]}")}''' | 24 | fr'''A formula é {tex("\sqrt{x]}")}''' |
| 27 | 25 | ||
| 28 | - Gerar pdf's com todos os testes no final (pdfkit). | 26 | - Gerar pdf's com todos os testes no final (pdfkit). |
| @@ -41,6 +39,8 @@ | @@ -41,6 +39,8 @@ | ||
| 41 | 39 | ||
| 42 | # FIXED | 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 | - como alterar configuracao para mostrar logs de debug? | 44 | - como alterar configuracao para mostrar logs de debug? |
| 45 | - espaco no final das tabelas. | 45 | - espaco no final das tabelas. |
| 46 | - total do teste aparece negativo. | 46 | - total do teste aparece negativo. |
demo/questions/questions-tutorial.yaml
| @@ -5,10 +5,10 @@ | @@ -5,10 +5,10 @@ | ||
| 5 | text: | | 5 | text: | |
| 6 | Texto informativo. Não conta para avaliação. | 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,10 +20,10 @@ | ||
| 20 | Texto positivo (sucesso). Não conta para avaliação. | 20 | Texto positivo (sucesso). Não conta para avaliação. |
| 21 | 21 | ||
| 22 | ```C | 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 | Inline `code`. | 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 | # base | 3 | # base |
| 30 | import random | 4 | import random |
| 31 | import re | 5 | import re |
| 32 | from os import path | 6 | from os import path |
| 33 | import logging | 7 | import logging |
| 34 | -import sys | ||
| 35 | 8 | ||
| 36 | # packages | 9 | # packages |
| 37 | import yaml | 10 | import yaml |
| 38 | 11 | ||
| 39 | # this project | 12 | # this project |
| 40 | -from tools import load_yaml, run_script | 13 | +from tools import run_script |
| 14 | + | ||
| 41 | 15 | ||
| 42 | # regular expressions in yaml files, e.g. correct: !regex '[aA]zul' | 16 | # regular expressions in yaml files, e.g. correct: !regex '[aA]zul' |
| 43 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) | 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,12 +20,6 @@ yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) | ||
| 46 | # setup logger for this module | 20 | # setup logger for this module |
| 47 | logger = logging.getLogger(__name__) | 21 | logger = logging.getLogger(__name__) |
| 48 | 22 | ||
| 49 | -# =========================================================================== | ||
| 50 | -class QuestionFactoryException(Exception): | ||
| 51 | - pass | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - | ||
| 55 | 23 | ||
| 56 | # =========================================================================== | 24 | # =========================================================================== |
| 57 | # Questions derived from Question are already instantiated and ready to be | 25 | # Questions derived from Question are already instantiated and ready to be |
| @@ -389,204 +357,3 @@ class QuestionInformation(Question): | @@ -389,204 +357,3 @@ class QuestionInformation(Question): | ||
| 389 | super().correct() | 357 | super().correct() |
| 390 | self['grade'] = 1.0 # always "correct" but points should be zero! | 358 | self['grade'] = 1.0 # always "correct" but points should be zero! |
| 391 | return self['grade'] | 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 |
test.py
| @@ -8,8 +8,7 @@ import json | @@ -8,8 +8,7 @@ import json | ||
| 8 | import logging | 8 | import logging |
| 9 | 9 | ||
| 10 | # project | 10 | # project |
| 11 | -import questions | ||
| 12 | -# from tools import load_yaml | 11 | +import questionfactory as questions |
| 13 | 12 | ||
| 14 | # Logger configuration | 13 | # Logger configuration |
| 15 | logger = logging.getLogger(__name__) | 14 | logger = logging.getLogger(__name__) |