Commit 6cdd62d765c73a1433d5bace812d7986004b2848

Authored by Miguel Barão
1 parent f97d3331
Exists in master and in 1 other branch dev

- separated questions and question factory into two separate files.

@@ -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`.
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  
@@ -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__)