Commit ef9f327f796b63a6873f4170d0b1db691c2fd71e
1 parent
d1410958
Exists in
master
and in
1 other branch
- another version not working...
Showing
15 changed files
with
304 additions
and
120 deletions
Show diff stats
config/logger-debug.yaml
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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) | ... | ... |
... | ... | @@ -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. | ... | ... |
... | ... | @@ -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 | ... | ... |
419 KB
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | } | ... | ... |