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 | ... | ... |