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__) |