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.

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
... ...
test.py
... ... @@ -8,8 +8,7 @@ import json
8 8 import logging
9 9  
10 10 # project
11   -import questions
12   -# from tools import load_yaml
  11 +import questionfactory as questions
13 12  
14 13 # Logger configuration
15 14 logger = logging.getLogger(__name__)
... ...