Commit ef9f327f796b63a6873f4170d0b1db691c2fd71e

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

- another version not working...

config/logger-debug.yaml
... ... @@ -34,6 +34,11 @@ loggers:
34 34 level: 'DEBUG'
35 35 propagate: False
36 36  
  37 + 'questionfactory':
  38 + handlers: ['default']
  39 + level: 'DEBUG'
  40 + propagate: False
  41 +
37 42 'tools':
38 43 handlers: ['default']
39 44 level: 'DEBUG'
... ...
demo/demo.yaml 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +title: Example
  2 +database: students.db
  3 +
  4 +# path prefix applied to the topics
  5 +path: ./demo
  6 +
  7 +# Representation of the edges of the dependency graph.
  8 +# Example: A depends on B and C
  9 +# A:
  10 +# name: Topic A
  11 +# deps:
  12 +# - B
  13 +# - C
  14 +topics:
  15 +
  16 + # topic without dependencies
  17 + math:
  18 + name: Matemática
  19 +
  20 + # topic with one dependency
  21 + solar_system:
  22 + name: Solar system
  23 + deps:
  24 + - math
... ...
demo/math/generate-overflow.py 0 → 100755
... ... @@ -0,0 +1,28 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +from random import randint
  4 +import sys
  5 +
  6 +arg = sys.stdin.read() # read arguments
  7 +
  8 +a,b = (int(n) for n in arg.split(','))
  9 +
  10 +q = '''
  11 +type: checkbox
  12 +text: |
  13 + Indique quais das seguintes adições resultam em overflow quando se considera a adição de números com sinal (complemento para 2) em registos de 8 bits.
  14 +
  15 + Os números foram gerados aleatoriamente no intervalo de {0} a {1}.
  16 +options:
  17 +'''.format(a,b)
  18 +
  19 +correct = []
  20 +for i in range(5):
  21 + x = randint(a, b)
  22 + y = randint(a, b)
  23 + q += '- "`{} + {}`"\n'.format(x, y)
  24 + correct.append(1 if x + y > 127 else -1)
  25 +
  26 +q += 'correct: ' + str(correct)
  27 +
  28 +print(q)
... ...
demo/math/questions.yaml 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +-
  2 + ref: prime_numbers
  3 + type: radio
  4 + title: Números primos
  5 + text: Qual dos seguintes números é primo?
  6 + options:
  7 + - 13
  8 + - 12
  9 + - 14
  10 + - 1, a **unidade**
  11 +
  12 +# # ---------------------------------------------------------------------------
  13 +# -
  14 +# ref: distributive_property
  15 +# type: radio
  16 +# title: Propriedade distributiva
  17 +# text: |
  18 +# Qual das seguintes opções usa a propriedade distributiva para calcular $9\times 2 - 2\times 3$?
  19 +# options:
  20 +# - $2\times(9 - 3)$
  21 +# - $18 - 6 = 12$
  22 +# - $2\times 9 - 2\times 3$
  23 +# - $2\times(9-2\times 3)$
  24 +
  25 +# # ---------------------------------------------------------------------------
  26 +# -
  27 +# ref: math-expressions
  28 +# type: checkbox
  29 +# title: Expressões matemáticas
  30 +# text: Quais das seguintes expressões são verdadeiras?
  31 +# options:
  32 +# - $1 > 0$
  33 +# - $\sqrt{3} > \sqrt{2}$
  34 +# - $e^{i\pi} + 1 = 0$
  35 +# - $\frac{\partial f(x,y)}{\partial z} = 1$
  36 +# - $-1 > 1$
  37 +# # how many points for each checkmark (normalized afterwards):
  38 +# correct: [1, 1, 1, -1, -1]
  39 +
  40 +# # ---------------------------------------------------------------------------
  41 +# -
  42 +# ref: overflow
  43 +# type: generator
  44 +# script: generate-overflow.py
  45 +# # opcional
  46 +# arg: "11,120"
  47 +# # the script should print a question dict in yaml format.
... ...
demo/solar_system/correct-first_3_planets.py 0 → 100755
... ... @@ -0,0 +1,27 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +import re
  4 +import sys
  5 +
  6 +s = sys.stdin.read()
  7 +
  8 +# set of words converted to lowercase
  9 +answer = set(re.findall(r'[\w]+', s.lower()))
  10 +answer.difference_update({'e', 'a', 'planeta', 'planetas'}) # ignore these
  11 +
  12 +# correct set of colors
  13 +planets = set(['mercúrio', 'vénus', 'terra'])
  14 +
  15 +correct = set.intersection(answer, planets) # os que acertei
  16 +wrong = set.difference(answer, planets) # os que errei
  17 +# allnames = set.union(answer, planets)
  18 +
  19 +grade = (len(correct) - len(wrong)) / len(planets)
  20 +
  21 +out = f'grade: {grade}'
  22 +
  23 +if grade < 1.0:
  24 + out += '\ncomments: A resposta correcta é `Mercúrio, Vénus e Terra`.'
  25 +
  26 +print(out)
  27 +exit(0)
0 28 \ No newline at end of file
... ...
demo/solar_system/correct-timeout.py 0 → 100755
... ... @@ -0,0 +1,11 @@
  1 +#!/usr/bin/env python3
  2 +
  3 +import sys
  4 +import time
  5 +
  6 +s = sys.stdin.read()
  7 +
  8 +# generate timeout
  9 +time.sleep(100)
  10 +
  11 +print(0.5)
... ...
demo/solar_system/public/planets.png 0 → 100644

419 KB

demo/solar_system/questions.yaml 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +
  2 +# ---------------------------------------------------------------------------
  3 +-
  4 + ref: solar-system
  5 + type: radio
  6 + title: Sistema solar
  7 + text: Qual é o maior planeta do Sistema Solar?
  8 + options:
  9 + - Mercúrio
  10 + - Marte
  11 + - Júpiter
  12 + - Têm todos o mesmo tamanho
  13 + # opcional
  14 + correct: 2
  15 + shuffle: False
  16 + discount: True
  17 +
  18 +# ---------------------------------------------------------------------------
  19 +-
  20 + ref: home-planet
  21 + type: text
  22 + title: Sistema solar
  23 + text: O nosso planeta chama-se planeta...
  24 + correct: ['Terra', 'terra']
  25 + # opcional
  26 + answer: Não é Marte...
  27 +
  28 +# ---------------------------------------------------------------------------
  29 +-
  30 + ref: saturn
  31 + type: text-regex
  32 + title: Sistema solar
  33 + text: O planeta do nosso sistema solar conhecido por ter aneis chama-se planeta...
  34 + correct: !regex '[Ss]aturno'
  35 +
  36 +# ---------------------------------------------------------------------------
  37 +-
  38 + ref: first_3_planets
  39 + type: textarea
  40 + title: Sistema solar
  41 + text: Escreva o nome dos três planetas mais próximos do Sol. (Exemplo `A, B e C`)
  42 + correct: correct-first_3_planets.py
  43 + # opcional
  44 + lines: 3
  45 + timeout: 5
... ...
factory.py 0 → 100644
... ... @@ -0,0 +1,93 @@
  1 +# QFactory is a class that can generate question instances, e.g. by shuffling
  2 +# options, running a script to generate the question, etc.
  3 +#
  4 +# To generate an instance of a question we use the method generate() where
  5 +# the argument is the reference of the question we wish to produce.
  6 +#
  7 +# Example:
  8 +#
  9 +# # read question from file
  10 +# qdict = tools.load_yaml(filename)
  11 +# qfactory = QFactory(qdict)
  12 +# question = qfactory.generate()
  13 +#
  14 +# # experiment answering one question and correct it
  15 +# question.updateAnswer('42') # insert answer
  16 +# grade = question.correct() # correct answer
  17 +
  18 +# An instance of an actual question is an object that inherits from Question()
  19 +#
  20 +# Question - base class inherited by other classes
  21 +# QuestionInformation - not a question, just a box with content
  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 +# QuestionNumericInterval - line of text parsed as a float
  28 +
  29 +# base
  30 +from os import path
  31 +import logging
  32 +
  33 +# project
  34 +from tools import run_script
  35 +from questions import QuestionInformation, QuestionRadio, QuestionCheckbox, QuestionText, QuestionTextRegex, QuestionTextArea, QuestionNumericInterval
  36 +
  37 +# setup logger for this module
  38 +logger = logging.getLogger(__name__)
  39 +
  40 +
  41 +
  42 +# ===========================================================================
  43 +# Question Factory
  44 +# ===========================================================================
  45 +class QFactory(object):
  46 + # Depending on the type of question, a different question class will be
  47 + # instantiated. All these classes derive from the base class `Question`.
  48 + _types = {
  49 + 'radio' : QuestionRadio,
  50 + 'checkbox' : QuestionCheckbox,
  51 + 'text' : QuestionText,
  52 + 'text-regex': QuestionTextRegex,
  53 + 'numeric-interval': QuestionNumericInterval,
  54 + 'textarea' : QuestionTextArea,
  55 + # -- informative panels --
  56 + 'information': QuestionInformation, 'info': QuestionInformation,
  57 + 'warning' : QuestionInformation, 'warn': QuestionInformation,
  58 + 'alert' : QuestionInformation,
  59 + 'success' : QuestionInformation,
  60 + }
  61 +
  62 + def __init__(self, question_dict):
  63 + self.question = question_dict
  64 +
  65 + # -----------------------------------------------------------------------
  66 + # Given a ref returns an instance of a descendent of Question(),
  67 + # i.e. a question object (radio, checkbox, ...).
  68 + # -----------------------------------------------------------------------
  69 + def generate(self):
  70 + logger.debug(f'Generating "{self.question["ref"]}"')
  71 + # Shallow copy so that script generated questions will not replace
  72 + # the original generators
  73 + q = self.question.copy()
  74 +
  75 + # If question is of generator type, an external program will be run
  76 + # which will print a valid question in yaml format to stdout. This
  77 + # output is then yaml parsed into a dictionary `q`.
  78 + if q['type'] == 'generator':
  79 + logger.debug(f' \_ Running script "{q["script"]}"...')
  80 + q.setdefault('arg', '') # optional arguments will be sent to stdin
  81 + script = path.join(q['path'], q['script'])
  82 + out = run_script(script=script, stdin=q['arg'])
  83 + q.update(out)
  84 +
  85 + # Finally we create an instance of Question()
  86 + try:
  87 + qinstance = self._types[q['type']](q) # instance with correct class
  88 + except KeyError as e:
  89 + logger.error(f'Failed to generate question "{q["ref"]}"')
  90 + raise e
  91 + else:
  92 + return qinstance
  93 +
... ...
knowledge.py
... ... @@ -86,11 +86,11 @@ class StudentKnowledge(object):
86 86 # current_question: the current question to be presented
87 87 # ------------------------------------------------------------------------
88 88 def init_topic(self, topic=''):
89   - # self.unlock_topics() # FIXME needed?
  89 + logger.debug(f'StudentKnowledge.init_topic({topic})')
90 90  
91 91 if not topic:
92 92 topic = self.recommended_topic()
93   -
  93 + print(f'recommended {topic}')
94 94  
95 95 self.current_topic = topic
96 96 logger.info(f'Topic set to "{topic}"')
... ... @@ -134,9 +134,11 @@ class StudentKnowledge(object):
134 134  
135 135 # if answer is wrong, keep same question and add a similar one at the end
136 136 else:
  137 + print('failed')
137 138 factory = self.deps.node[self.current_topic]['factory']
138 139 self.questions.append(factory[q['ref']].generate())
139 140  
  141 +
140 142 # returns answered and corrected question
141 143 return q
142 144  
... ... @@ -154,10 +156,9 @@ class StudentKnowledge(object):
154 156 return self.current_topic
155 157  
156 158 # ------------------------------------------------------------------------
157   - # Return list of tuples (topic, level).
158   - # Levels are in the interval [0, 1] or None if the topic is locked.
  159 + # Return list of {ref: 'xpto', name: 'long name', leve: 0.5}
  160 + # Levels are in the interval [0, 1] if unlocked or None if locked.
159 161 # Topics unlocked but not yet done have level 0.0.
160   - # Example: [('topic_A', 0.9), ('topic_B', None), ...]
161 162 # ------------------------------------------------------------------------
162 163 def get_knowledge_state(self):
163 164 return [{
... ... @@ -166,15 +167,6 @@ class StudentKnowledge(object):
166 167 'level': self.state[ref]['level'] if ref in self.state else None
167 168 } for ref in self.topic_sequence ]
168 169  
169   - # ts = []
170   - # for ref in self.topic_sequence:
171   - # ts.append({
172   - # 'ref': ref,
173   - # 'name': self.deps.nodes[ref]['name'],
174   - # 'level': self.state[ref]['level'] if ref in self.state else None
175   - # })
176   - # return ts
177   -
178 170 # ------------------------------------------------------------------------
179 171 def get_topic_progress(self):
180 172 return len(self.finished_questions) / (1 + len(self.finished_questions) + len(self.questions))
... ...
learnapp.py
... ... @@ -15,7 +15,7 @@ import yaml
15 15 # this project
16 16 from models import Student, Answer, Topic, StudentTopic
17 17 from knowledge import StudentKnowledge
18   -from questionfactory import QFactory
  18 +from factory import QFactory
19 19 from tools import load_yaml
20 20  
21 21 # setup logger for this module
... ...
questionfactory.py
... ... @@ -1,93 +0,0 @@
1   -# QFactory is a class that can generate question instances, e.g. by shuffling
2   -# options, running a script to generate the question, etc.
3   -#
4   -# To generate an instance of a question we use the method generate() where
5   -# the argument is the reference of the question we wish to produce.
6   -#
7   -# Example:
8   -#
9   -# # read question from file
10   -# qdict = tools.load_yaml(filename)
11   -# qfactory = QFactory(qdict)
12   -# question = qfactory.generate()
13   -#
14   -# # experiment answering one question and correct it
15   -# question.updateAnswer('42') # insert answer
16   -# grade = question.correct() # correct answer
17   -
18   -# An instance of an actual question is an object that inherits from Question()
19   -#
20   -# Question - base class inherited by other classes
21   -# QuestionInformation - not a question, just a box with content
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   -# QuestionNumericInterval - line of text parsed as a float
28   -
29   -# base
30   -from os import path
31   -import logging
32   -
33   -# project
34   -from tools import run_script
35   -from questions import QuestionInformation, QuestionRadio, QuestionCheckbox, QuestionText, QuestionTextRegex, QuestionTextArea, QuestionNumericInterval
36   -
37   -# setup logger for this module
38   -logger = logging.getLogger(__name__)
39   -
40   -
41   -
42   -# ===========================================================================
43   -# Question Factory
44   -# ===========================================================================
45   -class QFactory(object):
46   - # Depending on the type of question, a different question class will be
47   - # instantiated. All these classes derive from the base class `Question`.
48   - _types = {
49   - 'radio' : QuestionRadio,
50   - 'checkbox' : QuestionCheckbox,
51   - 'text' : QuestionText,
52   - 'text-regex': QuestionTextRegex,
53   - 'numeric-interval': QuestionNumericInterval,
54   - 'textarea' : QuestionTextArea,
55   - # -- informative panels --
56   - 'information': QuestionInformation, 'info': QuestionInformation,
57   - 'warning' : QuestionInformation, 'warn': QuestionInformation,
58   - 'alert' : QuestionInformation,
59   - 'success' : QuestionInformation,
60   - }
61   -
62   - def __init__(self, question_dict):
63   - self.question = question_dict
64   -
65   - # -----------------------------------------------------------------------
66   - # Given a ref returns an instance of a descendent of Question(),
67   - # i.e. a question object (radio, checkbox, ...).
68   - # -----------------------------------------------------------------------
69   - def generate(self):
70   - logger.debug(f'Generating "{self.question["ref"]}"')
71   - # Shallow copy so that script generated questions will not replace
72   - # the original generators
73   - q = self.question.copy()
74   -
75   - # If question is of generator type, an external program will be run
76   - # which will print a valid question in yaml format to stdout. This
77   - # output is then yaml parsed into a dictionary `q`.
78   - if q['type'] == 'generator':
79   - logger.debug(f' \_ Running script "{q["script"]}"...')
80   - q.setdefault('arg', '') # optional arguments will be sent to stdin
81   - script = path.join(q['path'], q['script'])
82   - out = run_script(script=script, stdin=q['arg'])
83   - q.update(out)
84   -
85   - # Finally we create an instance of Question()
86   - try:
87   - qinstance = self._types[q['type']](q) # instance with correct class
88   - except KeyError as e:
89   - logger.error(f'Failed to generate question "{q["ref"]}"')
90   - raise e
91   - else:
92   - return qinstance
93   -
serve.py
... ... @@ -135,7 +135,6 @@ class TopicHandler(BaseHandler):
135 135 self.render('topic.html',
136 136 uid=uid,
137 137 name=self.learn.get_student_name(uid),
138   - # title=self.learn.get_title()
139 138 )
140 139  
141 140  
... ... @@ -175,7 +174,7 @@ class QuestionHandler(BaseHandler):
175 174 }
176 175  
177 176 def new_question(self, user):
178   -
  177 + logger.debug(f'new_question({user})')
179 178 # state = self.learn.get_student_state(user) # [{ref, name, level},...]
180 179 # current_topic = self.learn.get_student_topic(user) # str
181 180  
... ... @@ -192,6 +191,7 @@ class QuestionHandler(BaseHandler):
192 191 }
193 192  
194 193 def wrong_answer(self, user):
  194 + logger.debug(f'wrong_answer({user})')
195 195 progress = self.learn.get_student_progress(user) # in the current topic
196 196 return {
197 197 'method': 'shake',
... ... @@ -201,6 +201,8 @@ class QuestionHandler(BaseHandler):
201 201 }
202 202  
203 203 def finished_topic(self, user):
  204 + logger.debug(f'finished_topic({user})')
  205 +
204 206 # state = self.learn.get_student_state(user) # all topics
205 207 # current_topic = self.learn.get_student_topic(uid)
206 208  
... ... @@ -214,7 +216,7 @@ class QuestionHandler(BaseHandler):
214 216  
215 217 @tornado.web.authenticated
216 218 def get(self):
217   - logging.debug('QuestionHandler.get')
  219 + logging.debug('QuestionHandler.get()')
218 220 user = self.current_user
219 221  
220 222 question = self.learn.get_student_question(user)
... ... @@ -232,9 +234,10 @@ class QuestionHandler(BaseHandler):
232 234 # handles answer posted
233 235 @tornado.web.authenticated
234 236 def post(self):
235   - logging.debug('QuestionHandler.post')
  237 + logging.debug('QuestionHandler.post()')
236 238 user = self.current_user
237 239  
  240 + print(self.get_body_arguments())
238 241 # if self.learn.get_student_question(user) is None:
239 242  
240 243 answer = self.get_body_argument('answer')
... ...
templates/question-text.html
... ... @@ -3,7 +3,7 @@
3 3 {% block answer %}
4 4 <fieldset data-role="controlgroup">
5 5 {% if question['answer'] %}
6   - <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'][0] }}" autofocus>
  6 + <input type="text" class="form-control" id="answer" name="answer" value="{{ question['answer'] }}" autofocus>
7 7 {% else %}
8 8 <input type="text" class="form-control" id="answer" name="answer" value="" autofocus>
9 9 {% end %}
... ...
templates/topic.html
... ... @@ -82,13 +82,15 @@
82 82  
83 83 <div id="notifications"></div>
84 84  
85   - <form action="/question" method="post" id="question_form" autocomplete="off">
86   - {% module xsrf_form_html() %}
  85 + <div id="content">
  86 + <form action="/question" method="post" id="question_form" autocomplete="off">
  87 + {% module xsrf_form_html() %}
87 88  
88   - <div id="question_div"></div>
  89 + <div id="question_div"></div>
89 90  
90   - </form>
91   - <button class="btn btn-primary" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button>
  91 + </form>
  92 + <button class="btn btn-primary" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button>
  93 + </div>
92 94 </div>
93 95  
94 96 <!-- ===================================================================== -->
... ... @@ -153,8 +155,8 @@
153 155  
154 156 case "finished_topic":
155 157 $('#topic_progress').css('width', '100%').attr('aria-valuenow', 100);
156   - $("#container").html('<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto my-5 d-block" width="25%">');
157   - $("#container").animateCSS('tada');
  158 + $("#content").html('<img src="/static/trophy.png" alt="trophy" class="img-fluid mx-auto my-5 d-block" width="25%">');
  159 + $("#content").animateCSS('tada');
158 160 setTimeout(function(){ window.location.replace('/'); }, 2000);
159 161 break;
160 162 }
... ...