Commit f4809e6f0f647e10fb039749d838ffe47f913a3f
1 parent
9a20acc8
Exists in
master
and in
1 other branch
- code cleaning questions.py
- fix image size in solution box - update demo to match the new courses organization
Showing
31 changed files
with
414 additions
and
341 deletions
Show diff stats
BUGS.md
1 | 1 | ||
2 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | +- se num topico, a ultima pergunta tem imagens, o servidor nao fornece as imagengs porque o current_topic passa a None antes de carregar no botao continuar. O caminho é prefix+None e dá erro. | ||
5 | +- registar last_seen e remover os antigos de cada vez que houver um login. | ||
4 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) | 6 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) |
5 | - permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. | 7 | - permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic. |
6 | - double click submits twice. | 8 | - double click submits twice. |
aprendizations/learnapp.py
@@ -462,7 +462,7 @@ class LearnApp(object): | @@ -462,7 +462,7 @@ class LearnApp(object): | ||
462 | 462 | ||
463 | # ------------------------------------------------------------------------ | 463 | # ------------------------------------------------------------------------ |
464 | def get_current_public_dir(self, uid: str) -> str: | 464 | def get_current_public_dir(self, uid: str) -> str: |
465 | - topic: str = self.online[uid]['state'].get_current_topic() | 465 | + topic: str = self.online[uid]['state'].get_current_topic() # FIXME returns None if its the last question in the topic |
466 | prefix: str = self.deps.graph['prefix'] | 466 | prefix: str = self.deps.graph['prefix'] |
467 | return path.join(prefix, topic, 'public') | 467 | return path.join(prefix, topic, 'public') |
468 | 468 |
aprendizations/main.py
@@ -140,21 +140,22 @@ def main(): | @@ -140,21 +140,22 @@ def main(): | ||
140 | path.join(certs_dir, 'privkey.pem')) | 140 | path.join(certs_dir, 'privkey.pem')) |
141 | except FileNotFoundError: | 141 | except FileNotFoundError: |
142 | logging.critical(f'SSL certificates missing in {certs_dir}') | 142 | logging.critical(f'SSL certificates missing in {certs_dir}') |
143 | - print('--------------------------------------------------------------') | ||
144 | - print('Certificates should be issued by a certificate authority (CA),') | ||
145 | - print('such as https://letsencrypt.org. ') | ||
146 | - print('For testing purposes a selfsigned certificate can be generated') | ||
147 | - print('locally by running: ') | ||
148 | - print(' ') | ||
149 | - print(' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ') | ||
150 | - print(' -out cert.pem -days 365 -nodes ') | ||
151 | - print(' ') | ||
152 | - print('Copy the cert.pem and privkey.pem files to: ') | ||
153 | - print(' ') | ||
154 | - print(f' {certs_dir:<62}') | ||
155 | - print(' ') | ||
156 | - print('(See README.md for more information) ') | ||
157 | - print('--------------------------------------------------------------') | 143 | + print('--------------------------------------------------------------', |
144 | + 'Certificates should be issued by a certificate authority (CA),', | ||
145 | + 'such as https://letsencrypt.org. ', | ||
146 | + 'For testing purposes a selfsigned certificate can be generated', | ||
147 | + 'locally by running: ', | ||
148 | + ' ', | ||
149 | + ' openssl req -x509 -newkey rsa:4096 -keyout privkey.pem \\ ', | ||
150 | + ' -out cert.pem -days 365 -nodes ', | ||
151 | + ' ', | ||
152 | + 'Copy the cert.pem and privkey.pem files to: ', | ||
153 | + ' ', | ||
154 | + f' {certs_dir:<62}', | ||
155 | + ' ', | ||
156 | + 'See README.md for more information ', | ||
157 | + '--------------------------------------------------------------', | ||
158 | + sep='\n') | ||
158 | sys.exit(1) | 159 | sys.exit(1) |
159 | else: | 160 | else: |
160 | logging.info('SSL certificates loaded') | 161 | logging.info('SSL certificates loaded') |
@@ -167,20 +168,21 @@ def main(): | @@ -167,20 +168,21 @@ def main(): | ||
167 | check=arg.check) | 168 | check=arg.check) |
168 | except DatabaseUnusableError: | 169 | except DatabaseUnusableError: |
169 | logging.critical('Failed to start application.') | 170 | logging.critical('Failed to start application.') |
170 | - print('--------------------------------------------------------------') | ||
171 | - print('Could not find a usable database. Use one of the follwing ') | ||
172 | - print('commands to initialize: ') | ||
173 | - print(' ') | ||
174 | - print(' initdb-aprendizations --admin # add admin ') | ||
175 | - print(' initdb-aprendizations -a 86 "Max Smart" # add student ') | ||
176 | - print(' initdb-aprendizations students.csv # add many students') | ||
177 | - print('--------------------------------------------------------------') | 171 | + print('--------------------------------------------------------------', |
172 | + 'Could not find a usable database. Use one of the follwing ', | ||
173 | + 'commands to initialize: ', | ||
174 | + ' ', | ||
175 | + ' initdb-aprendizations --admin # add admin ', | ||
176 | + ' initdb-aprendizations -a 86 "Max Smart" # add student ', | ||
177 | + ' initdb-aprendizations students.csv # add many students', | ||
178 | + '--------------------------------------------------------------', | ||
179 | + sep='\n') | ||
178 | sys.exit(1) | 180 | sys.exit(1) |
179 | except Exception: | 181 | except Exception: |
180 | - logging.critical('Failed to start application.') | 182 | + logging.critical('Failed to start backend.') |
181 | sys.exit(1) | 183 | sys.exit(1) |
182 | else: | 184 | else: |
183 | - logging.info('Backend application started') | 185 | + logging.info('Backend started') |
184 | 186 | ||
185 | # --- run webserver forever | 187 | # --- run webserver forever |
186 | run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug) | 188 | run_webserver(app=learnapp, ssl=ssl_ctx, port=arg.port, debug=arg.debug) |
aprendizations/questions.py
1 | 1 | ||
2 | # python standard library | 2 | # python standard library |
3 | +import asyncio | ||
3 | import random | 4 | import random |
4 | import re | 5 | import re |
5 | from os import path | 6 | from os import path |
@@ -21,10 +22,10 @@ class QuestionException(Exception): | @@ -21,10 +22,10 @@ class QuestionException(Exception): | ||
21 | pass | 22 | pass |
22 | 23 | ||
23 | 24 | ||
24 | -# =========================================================================== | 25 | +# ============================================================================ |
25 | # Questions derived from Question are already instantiated and ready to be | 26 | # Questions derived from Question are already instantiated and ready to be |
26 | # presented to students. | 27 | # presented to students. |
27 | -# =========================================================================== | 28 | +# ============================================================================ |
28 | class Question(dict): | 29 | class Question(dict): |
29 | ''' | 30 | ''' |
30 | Classes derived from this base class are meant to instantiate questions | 31 | Classes derived from this base class are meant to instantiate questions |
@@ -41,7 +42,7 @@ class Question(dict): | @@ -41,7 +42,7 @@ class Question(dict): | ||
41 | 'comments': '', | 42 | 'comments': '', |
42 | 'solution': '', | 43 | 'solution': '', |
43 | 'files': {}, | 44 | 'files': {}, |
44 | - 'max_tries': 3, | 45 | + # 'max_tries': 3, |
45 | })) | 46 | })) |
46 | 47 | ||
47 | def correct(self) -> None: | 48 | def correct(self) -> None: |
@@ -57,7 +58,7 @@ class Question(dict): | @@ -57,7 +58,7 @@ class Question(dict): | ||
57 | self.setdefault(k, v) | 58 | self.setdefault(k, v) |
58 | 59 | ||
59 | 60 | ||
60 | -# ========================================================================== | 61 | +# ============================================================================ |
61 | class QuestionRadio(Question): | 62 | class QuestionRadio(Question): |
62 | '''An instance of QuestionRadio will always have the keys: | 63 | '''An instance of QuestionRadio will always have the keys: |
63 | type (str) | 64 | type (str) |
@@ -92,7 +93,7 @@ class QuestionRadio(Question): | @@ -92,7 +93,7 @@ class QuestionRadio(Question): | ||
92 | for x in range(n)] | 93 | for x in range(n)] |
93 | 94 | ||
94 | if len(self['correct']) != n: | 95 | if len(self['correct']) != n: |
95 | - msg = (f'Options and correct mismatch in ' | 96 | + msg = ('Number of options and correct differ in ' |
96 | f'"{self["ref"]}", file "{self["filename"]}".') | 97 | f'"{self["ref"]}", file "{self["filename"]}".') |
97 | logger.error(msg) | 98 | logger.error(msg) |
98 | raise QuestionException(msg) | 99 | raise QuestionException(msg) |
@@ -138,7 +139,7 @@ class QuestionRadio(Question): | @@ -138,7 +139,7 @@ class QuestionRadio(Question): | ||
138 | self['grade'] = x | 139 | self['grade'] = x |
139 | 140 | ||
140 | 141 | ||
141 | -# =========================================================================== | 142 | +# ============================================================================ |
142 | class QuestionCheckbox(Question): | 143 | class QuestionCheckbox(Question): |
143 | '''An instance of QuestionCheckbox will always have the keys: | 144 | '''An instance of QuestionCheckbox will always have the keys: |
144 | type (str) | 145 | type (str) |
@@ -218,7 +219,7 @@ class QuestionCheckbox(Question): | @@ -218,7 +219,7 @@ class QuestionCheckbox(Question): | ||
218 | self['grade'] = x / sum_abs | 219 | self['grade'] = x / sum_abs |
219 | 220 | ||
220 | 221 | ||
221 | -# =========================================================================== | 222 | +# ============================================================================ |
222 | class QuestionText(Question): | 223 | class QuestionText(Question): |
223 | '''An instance of QuestionText will always have the keys: | 224 | '''An instance of QuestionText will always have the keys: |
224 | type (str) | 225 | type (str) |
@@ -251,7 +252,7 @@ class QuestionText(Question): | @@ -251,7 +252,7 @@ class QuestionText(Question): | ||
251 | self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 | 252 | self['grade'] = 1.0 if self['answer'] in self['correct'] else 0.0 |
252 | 253 | ||
253 | 254 | ||
254 | -# =========================================================================== | 255 | +# ============================================================================ |
255 | class QuestionTextRegex(Question): | 256 | class QuestionTextRegex(Question): |
256 | '''An instance of QuestionTextRegex will always have the keys: | 257 | '''An instance of QuestionTextRegex will always have the keys: |
257 | type (str) | 258 | type (str) |
@@ -281,7 +282,7 @@ class QuestionTextRegex(Question): | @@ -281,7 +282,7 @@ class QuestionTextRegex(Question): | ||
281 | self['grade'] = 1.0 if ok else 0.0 | 282 | self['grade'] = 1.0 if ok else 0.0 |
282 | 283 | ||
283 | 284 | ||
284 | -# =========================================================================== | 285 | +# ============================================================================ |
285 | class QuestionNumericInterval(Question): | 286 | class QuestionNumericInterval(Question): |
286 | '''An instance of QuestionTextNumeric will always have the keys: | 287 | '''An instance of QuestionTextNumeric will always have the keys: |
287 | type (str) | 288 | type (str) |
@@ -316,7 +317,7 @@ class QuestionNumericInterval(Question): | @@ -316,7 +317,7 @@ class QuestionNumericInterval(Question): | ||
316 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 | 317 | self['grade'] = 1.0 if lower <= answer <= upper else 0.0 |
317 | 318 | ||
318 | 319 | ||
319 | -# =========================================================================== | 320 | +# ============================================================================ |
320 | class QuestionTextArea(Question): | 321 | class QuestionTextArea(Question): |
321 | '''An instance of QuestionTextArea will always have the keys: | 322 | '''An instance of QuestionTextArea will always have the keys: |
322 | type (str) | 323 | type (str) |
@@ -397,7 +398,7 @@ class QuestionTextArea(Question): | @@ -397,7 +398,7 @@ class QuestionTextArea(Question): | ||
397 | logger.error(f'Invalid grade in "{self["correct"]}".') | 398 | logger.error(f'Invalid grade in "{self["correct"]}".') |
398 | 399 | ||
399 | 400 | ||
400 | -# =========================================================================== | 401 | +# ============================================================================ |
401 | class QuestionInformation(Question): | 402 | class QuestionInformation(Question): |
402 | # ------------------------------------------------------------------------ | 403 | # ------------------------------------------------------------------------ |
403 | def __init__(self, q: QDict) -> None: | 404 | def __init__(self, q: QDict) -> None: |
@@ -412,30 +413,38 @@ class QuestionInformation(Question): | @@ -412,30 +413,38 @@ class QuestionInformation(Question): | ||
412 | self['grade'] = 1.0 # always "correct" but points should be zero! | 413 | self['grade'] = 1.0 # always "correct" but points should be zero! |
413 | 414 | ||
414 | 415 | ||
415 | -# =========================================================================== | 416 | +# ============================================================================ |
417 | +# | ||
416 | # QFactory is a class that can generate question instances, e.g. by shuffling | 418 | # QFactory is a class that can generate question instances, e.g. by shuffling |
417 | # options, running a script to generate the question, etc. | 419 | # options, running a script to generate the question, etc. |
418 | # | 420 | # |
419 | -# To generate an instance of a question we use the method generate() where | ||
420 | -# the argument is the reference of the question we wish to produce. | ||
421 | -# The generate() method returns a question instance of the correct class. | 421 | +# To generate an instance of a question we use the method generate(). |
422 | +# It returns a question instance of the correct class. | ||
423 | +# There is also an asynchronous version called gen_async(). This version is | ||
424 | +# synchronous for all question types (radio, checkbox, etc) except for generator | ||
425 | +# types which run asynchronously. | ||
422 | # | 426 | # |
423 | # Example: | 427 | # Example: |
424 | # | 428 | # |
425 | -# # generate a question instance from a dictionary | ||
426 | -# qdict = { | 429 | +# # make a factory for a question |
430 | +# qfactory = QFactory({ | ||
427 | # 'type': 'radio', | 431 | # 'type': 'radio', |
428 | # 'text': 'Choose one', | 432 | # 'text': 'Choose one', |
429 | # 'options': ['a', 'b'] | 433 | # 'options': ['a', 'b'] |
430 | -# } | ||
431 | -# qfactory = QFactory(qdict) | 434 | +# }) |
435 | +# | ||
436 | +# # generate synchronously | ||
432 | # question = qfactory.generate() | 437 | # question = qfactory.generate() |
433 | # | 438 | # |
439 | +# # generate asynchronously | ||
440 | +# question = await qfactory.gen_async() | ||
441 | +# | ||
434 | # # answer one question and correct it | 442 | # # answer one question and correct it |
435 | # question['answer'] = 42 # set answer | 443 | # question['answer'] = 42 # set answer |
436 | # question.correct() # correct answer | 444 | # question.correct() # correct answer |
437 | # grade = question['grade'] # get grade | 445 | # grade = question['grade'] # get grade |
438 | -# =========================================================================== | 446 | +# |
447 | +# ============================================================================ | ||
439 | class QFactory(object): | 448 | class QFactory(object): |
440 | # Depending on the type of question, a different question class will be | 449 | # Depending on the type of question, a different question class will be |
441 | # instantiated. All these classes derive from the base class `Question`. | 450 | # instantiated. All these classes derive from the base class `Question`. |
@@ -456,44 +465,10 @@ class QFactory(object): | @@ -456,44 +465,10 @@ class QFactory(object): | ||
456 | def __init__(self, qdict: QDict = QDict({})) -> None: | 465 | def __init__(self, qdict: QDict = QDict({})) -> None: |
457 | self.question = qdict | 466 | self.question = qdict |
458 | 467 | ||
459 | - # ----------------------------------------------------------------------- | ||
460 | - # Given a ref returns an instance of a descendent of Question(), | ||
461 | - # i.e. a question object (radio, checkbox, ...). | ||
462 | - # ----------------------------------------------------------------------- | ||
463 | - def generate(self) -> Question: | ||
464 | - # FIXME this is almost the same as the one below | ||
465 | - # return asyncio.run(self.gen_async()) | ||
466 | - | ||
467 | - logger.debug(f'generating {self.question["ref"]}...') | ||
468 | - # Shallow copy so that script generated questions will not replace | ||
469 | - # the original generators | ||
470 | - q = self.question.copy() | ||
471 | - q['qid'] = str(uuid.uuid4()) # unique for each generated question | ||
472 | - | ||
473 | - # If question is of generator type, an external program will be run | ||
474 | - # which will print a valid question in yaml format to stdout. This | ||
475 | - # output is then yaml parsed into a dictionary `q`. | ||
476 | - if q['type'] == 'generator': | ||
477 | - logger.debug(f' \\_ Running "{q["script"]}".') | ||
478 | - q.setdefault('args', []) | ||
479 | - q.setdefault('stdin', '') # FIXME is it really necessary? | ||
480 | - script = path.join(q['path'], q['script']) | ||
481 | - out = run_script(script=script, args=q['args'], stdin=q['stdin']) | ||
482 | - q.update(out) | ||
483 | - | ||
484 | - # Finally we create an instance of Question() | ||
485 | - try: | ||
486 | - qinstance = self._types[q['type']](QDict(q)) # of matching class | ||
487 | - except QuestionException as e: | ||
488 | - logger.error(e) | ||
489 | - raise e | ||
490 | - except KeyError: | ||
491 | - logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"') | ||
492 | - raise | ||
493 | - else: | ||
494 | - return qinstance | ||
495 | - | ||
496 | - # ----------------------------------------------------------------------- | 468 | + # ------------------------------------------------------------------------ |
469 | + # generates a question instance of QuestionRadio, QuestionCheckbox, ..., | ||
470 | + # which is a descendent of base class Question. | ||
471 | + # ------------------------------------------------------------------------ | ||
497 | async def gen_async(self) -> Question: | 472 | async def gen_async(self) -> Question: |
498 | logger.debug(f'generating {self.question["ref"]}...') | 473 | logger.debug(f'generating {self.question["ref"]}...') |
499 | # Shallow copy so that script generated questions will not replace | 474 | # Shallow copy so that script generated questions will not replace |
@@ -524,3 +499,7 @@ class QFactory(object): | @@ -524,3 +499,7 @@ class QFactory(object): | ||
524 | raise | 499 | raise |
525 | else: | 500 | else: |
526 | return qinstance | 501 | return qinstance |
502 | + | ||
503 | + # ------------------------------------------------------------------------ | ||
504 | + def generate(self) -> Question: | ||
505 | + return asyncio.get_event_loop().run_until_complete(self.gen_async()) |
aprendizations/serve.py
@@ -433,6 +433,7 @@ def run_webserver(app, | @@ -433,6 +433,7 @@ def run_webserver(app, | ||
433 | webapp = WebApplication(app, debug=debug) | 433 | webapp = WebApplication(app, debug=debug) |
434 | except Exception: | 434 | except Exception: |
435 | logger.critical('Failed to start web application.') | 435 | logger.critical('Failed to start web application.') |
436 | + raise | ||
436 | sys.exit(1) | 437 | sys.exit(1) |
437 | else: | 438 | else: |
438 | logger.info('Web application started (tornado.web.Application)') | 439 | logger.info('Web application started (tornado.web.Application)') |
aprendizations/tools.py
@@ -110,14 +110,14 @@ class HighlightRenderer(mistune.Renderer): | @@ -110,14 +110,14 @@ class HighlightRenderer(mistune.Renderer): | ||
110 | return highlight(code, lexer, formatter) | 110 | return highlight(code, lexer, formatter) |
111 | 111 | ||
112 | def table(self, header, body): | 112 | def table(self, header, body): |
113 | - return '<table class="table table-sm"><thead class="thead-light">' \ | ||
114 | - + header + '</thead><tbody>' + body + '</tbody></table>' | 113 | + return ('<table class="table table-sm"><thead class="thead-light">' |
114 | + f'{header}</thead><tbody>{body}</tbody></table>') | ||
115 | 115 | ||
116 | def image(self, src, title, alt): | 116 | def image(self, src, title, alt): |
117 | alt = mistune.escape(alt, quote=True) | 117 | alt = mistune.escape(alt, quote=True) |
118 | title = mistune.escape(title or '', quote=True) | 118 | title = mistune.escape(title or '', quote=True) |
119 | - return f'<img src="/file/{src}" ' \ | ||
120 | - f'alt="{alt}" title="{title}">' | 119 | + return (f'<img src="/file/{src}" alt="{alt}" title="{title}"' |
120 | + 'class="img-fluid">') | ||
121 | # class="img-fluid mx-auto d-block" | 121 | # class="img-fluid mx-auto d-block" |
122 | 122 | ||
123 | # Pass math through unaltered - mathjax does the rendering in the browser | 123 | # Pass math through unaltered - mathjax does the rendering in the browser |
@@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
1 | +--- | ||
2 | +# ---------------------------------------------------------------------------- | ||
3 | +# optional values applied to each topic, if undefined there | ||
4 | +# ---------------------------------------------------------------------------- | ||
5 | + | ||
6 | +# defaults: | ||
7 | +# file: questions.yaml | ||
8 | +# shuffle_questions: true | ||
9 | +# choose: 6 | ||
10 | +# max_tries: 2 | ||
11 | +# forgetting_factor: 0.97 | ||
12 | +# min_level: 0.01 | ||
13 | +# append_wrong: true | ||
14 | + | ||
15 | +# ---------------------------------------------------------------------------- | ||
16 | +# topics and their dependencies | ||
17 | +# ---------------------------------------------------------------------------- | ||
18 | + | ||
19 | +topics: | ||
20 | + astronomy/solar-system: | ||
21 | + name: Sistema solar | ||
22 | + | ||
23 | + # astronomy/milky-way: | ||
24 | + # name: Via Láctea | ||
25 | + # deps: | ||
26 | + # - solar-system |
@@ -0,0 +1,28 @@ | @@ -0,0 +1,28 @@ | ||
1 | +#!/usr/bin/env python3 | ||
2 | + | ||
3 | +import re | ||
4 | +import sys | ||
5 | +import time | ||
6 | + | ||
7 | +s = sys.stdin.read() | ||
8 | + | ||
9 | +ans = set(re.findall(r'[\w]+', s.lower())) # convert answer to lowercase | ||
10 | +ans.difference_update({'e', 'a', 'o', 'planeta', 'planetas'}) # ignore words | ||
11 | + | ||
12 | +# correct set of planets | ||
13 | +planets = {'mercúrio', 'vénus', 'terra'} | ||
14 | + | ||
15 | +correct = set.intersection(ans, planets) # the ones I got right | ||
16 | +wrong = set.difference(ans, planets) # the ones I got wrong | ||
17 | + | ||
18 | +grade = (len(correct) - len(wrong)) / len(planets) | ||
19 | + | ||
20 | +comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | ||
21 | + | ||
22 | +out = f'''--- | ||
23 | +grade: {grade} | ||
24 | +comments: {comments}''' | ||
25 | + | ||
26 | +time.sleep(2) # simulate computation time (may generate timeout) | ||
27 | + | ||
28 | +print(out) |
1.02 MB
2.96 MB
419 KB
117 KB
@@ -0,0 +1,66 @@ | @@ -0,0 +1,66 @@ | ||
1 | +--- | ||
2 | +# ---------------------------------------------------------------------------- | ||
3 | +- type: text | ||
4 | + ref: planet-earth | ||
5 | + title: Sistema solar | ||
6 | + text: O nosso planeta chama-se planeta... | ||
7 | + correct: ['Terra', 'terra'] | ||
8 | + # opcional | ||
9 | + answer: dos macacos? | ||
10 | + solution: | | ||
11 | + O nosso planeta é o planeta **Terra**. | ||
12 | + | ||
13 | +  | ||
14 | + | ||
15 | +# ---------------------------------------------------------------------------- | ||
16 | +- type: radio | ||
17 | + ref: largest-planet | ||
18 | + title: Sistema solar | ||
19 | + text: | | ||
20 | +  | ||
21 | + | ||
22 | + Qual é o maior planeta do Sistema Solar? | ||
23 | + options: | ||
24 | + - Mercúrio | ||
25 | + - Marte | ||
26 | + - Júpiter | ||
27 | + - Saturno | ||
28 | + - Têm todos o mesmo tamanho | ||
29 | + # opcional | ||
30 | + correct: 2 | ||
31 | + shuffle: false | ||
32 | + solution: | | ||
33 | + O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | ||
34 | + ainda assim 2.5 vezes maior que a massa de todos os outros planetas juntos. | ||
35 | + É um gigante gasoso maioritariamente composto por hidrogénio. | ||
36 | + | ||
37 | +  | ||
38 | + | ||
39 | +# ---------------------------------------------------------------------------- | ||
40 | +- type: text-regex | ||
41 | + ref: saturn | ||
42 | + title: Sistema solar | ||
43 | + text: O planeta do sistema solar conhecido pelos seus aneis é o planeta... | ||
44 | + correct: '[Ss]aturno' | ||
45 | + solution: | | ||
46 | + O planeta Saturno é famoso pelos seus anéis. | ||
47 | + É o segundo maior planeta do Sistema Solar. | ||
48 | + Tal como Júpiter, é um gigante gasoso. | ||
49 | + Os seus anéis são formados por partículas de gelo. | ||
50 | + | ||
51 | +  | ||
52 | + | ||
53 | +# ---------------------------------------------------------------------------- | ||
54 | +- type: textarea | ||
55 | + ref: first_3_planets | ||
56 | + title: Sistema solar | ||
57 | + text: | | ||
58 | + Qual o nome dos três planetas mais próximos do Sol? | ||
59 | + Exemplo `Ceres, Krypton e Vulcano` | ||
60 | + correct: correct-first_3_planets.py | ||
61 | + # correct: correct-timeout.py | ||
62 | + # opcional | ||
63 | + answer: Ceres, Krypton e Vulcano | ||
64 | + timeout: 3 | ||
65 | + solution: | | ||
66 | + Os 3 planetas mais perto do Sol são Mercúrio, Vénus e Terra. |
@@ -0,0 +1,28 @@ | @@ -0,0 +1,28 @@ | ||
1 | +--- | ||
2 | +# ---------------------------------------------------------------------------- | ||
3 | +# import topics and their dependencies | ||
4 | +# ---------------------------------------------------------------------------- | ||
5 | +topics_from: | ||
6 | + - math.yaml | ||
7 | + - astronomy.yaml | ||
8 | + | ||
9 | +# ---------------------------------------------------------------------------- | ||
10 | +# course names, their goals and other details | ||
11 | +# ---------------------------------------------------------------------------- | ||
12 | +courses: | ||
13 | + math: | ||
14 | + title: Matemática | ||
15 | + description: | | ||
16 | + Adição, multiplicação e números primos. | ||
17 | + goals: | ||
18 | + - math/addition | ||
19 | + - math/multiplication | ||
20 | + - math/prime-numbers | ||
21 | + | ||
22 | + astronomy: | ||
23 | + title: Astronomia | ||
24 | + description: | | ||
25 | + Sistema Solar e Via Láctea. | ||
26 | + goals: | ||
27 | + - astronomy/solar-system | ||
28 | + # - astronomy/milky-way |
demo/demo.yaml
@@ -1,29 +0,0 @@ | @@ -1,29 +0,0 @@ | ||
1 | ---- | ||
2 | - | ||
3 | -title: Example | ||
4 | -database: students.db | ||
5 | - | ||
6 | -# values applied to each topic, if undefined there | ||
7 | -file: questions.yaml | ||
8 | -shuffle_questions: true | ||
9 | -choose: 6 | ||
10 | -max_tries: 2 | ||
11 | -forgetting_factor: 0.97 | ||
12 | -min_level: 0.01 | ||
13 | -append_wrong: true | ||
14 | - | ||
15 | -# ---------------------------------------------------------------------------- | ||
16 | -topics: | ||
17 | - # topic without dependencies | ||
18 | - math: | ||
19 | - name: Matemática | ||
20 | - file: questions.yaml | ||
21 | - choose: 6 | ||
22 | - shuffle: true | ||
23 | - | ||
24 | - # topic with one dependency | ||
25 | - solar_system: | ||
26 | - name: Sistema solar | ||
27 | - deps: | ||
28 | - - math | ||
29 | - forgetting_factor: 0.1 |
@@ -0,0 +1,31 @@ | @@ -0,0 +1,31 @@ | ||
1 | +--- | ||
2 | +# ---------------------------------------------------------------------------- | ||
3 | +# optional values applied to each topic, if undefined there | ||
4 | +# ---------------------------------------------------------------------------- | ||
5 | + | ||
6 | +# defaults: | ||
7 | +# file: questions.yaml | ||
8 | +# shuffle_questions: true | ||
9 | +# choose: 6 | ||
10 | +# max_tries: 2 | ||
11 | +# forgetting_factor: 0.97 | ||
12 | +# min_level: 0.01 | ||
13 | +# append_wrong: true | ||
14 | + | ||
15 | +# ---------------------------------------------------------------------------- | ||
16 | +# topics and their dependencies | ||
17 | +# ---------------------------------------------------------------------------- | ||
18 | + | ||
19 | +topics: | ||
20 | + math/addition: | ||
21 | + name: Adição | ||
22 | + | ||
23 | + math/multiplication: | ||
24 | + name: Multiplicação | ||
25 | + deps: | ||
26 | + - math/addition | ||
27 | + | ||
28 | + math/prime-numbers: | ||
29 | + name: Números primos | ||
30 | + deps: | ||
31 | + - math/multiplication |
@@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
1 | +#!/usr/bin/env python3 | ||
2 | + | ||
3 | +import random | ||
4 | +import sys | ||
5 | + | ||
6 | +a = int(sys.argv[1]) | ||
7 | +b = int(sys.argv[2]) | ||
8 | + | ||
9 | +x = random.randint(a, b) | ||
10 | +y = random.randint(a, b) | ||
11 | +r = x + y | ||
12 | + | ||
13 | +print(f'''--- | ||
14 | +type: text | ||
15 | +title: Adição de números com 2 algarismos | ||
16 | +text: | | ||
17 | + Qual o resultado da soma ${x}+{y}$? | ||
18 | +correct: ['{r}'] | ||
19 | +solution: | | ||
20 | + O resultado é {r}.''') |
@@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
1 | +--- | ||
2 | +# --------------------------------------------------------------------------- | ||
3 | +- type: generator | ||
4 | + ref: addition-two-digits | ||
5 | + script: addition-two-digits.py | ||
6 | + args: [10, 20] | ||
7 | + | ||
8 | +- type: checkbox | ||
9 | + ref: addition-properties | ||
10 | + title: Propriedades da adição | ||
11 | + text: Indique quais as propriedades que a adição satisfaz. | ||
12 | + options: | ||
13 | + # right | ||
14 | + - Existência de elemento neutro, $x+0=x$. | ||
15 | + - Existência de inverso aditivo (simétrico), $x+(-x)=0$. | ||
16 | + - Propriedade associativa, $(x+y)+z = x+(y+z)$. | ||
17 | + - Propriedade comutativa, $x+y=y+x$. | ||
18 | + # wrong | ||
19 | + - Existência de elemento absorvente, $x+1=1$. | ||
20 | + correct: [1, 1, 1, 1, -1] | ||
21 | + solution: | | ||
22 | + A adição não tem elemento absorvente. |
demo/math/gen-multiples-of-3.py
@@ -1,29 +0,0 @@ | @@ -1,29 +0,0 @@ | ||
1 | -#!/usr/bin/env python3 | ||
2 | - | ||
3 | -import random | ||
4 | -import sys | ||
5 | - | ||
6 | -a, b = map(int, sys.argv[1:]) # get command line arguments | ||
7 | - | ||
8 | -numbers = list(range(a, b)) | ||
9 | -random.shuffle(numbers) | ||
10 | -numbers = numbers[:8] | ||
11 | -correct = [1 if n % 3 == 0 else -1 for n in numbers] | ||
12 | - | ||
13 | -multiples = [str(n) for n in numbers if n % 3 == 0] | ||
14 | -if multiples: | ||
15 | - solution = f'Os números múltiplos de 3 são: {", ".join(multiples)}.' | ||
16 | -else: | ||
17 | - solution = f'Nenhum número mostrado é múltiplo de 3.' | ||
18 | - | ||
19 | - | ||
20 | -q = f'''--- | ||
21 | -type: checkbox | ||
22 | -title: Múltiplos de 3 | ||
23 | -text: Indique quais dos seguintes números são múltiplos de 3. | ||
24 | -options: {numbers} | ||
25 | -correct: {correct} | ||
26 | -solution: | | ||
27 | - {solution}''' | ||
28 | - | ||
29 | -print(q) |
@@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
1 | +#!/usr/bin/env python3 | ||
2 | + | ||
3 | +import random | ||
4 | +import sys | ||
5 | + | ||
6 | +# can be repeated | ||
7 | +# x = random.randint(2, 9) | ||
8 | +# y = random.randint(2, 9) | ||
9 | + | ||
10 | +# with x != y | ||
11 | +x, y = random.sample(range(2,10), k=2) | ||
12 | +r = x * y | ||
13 | + | ||
14 | +yy = '+'.join([str(y)]*x) | ||
15 | +xx = '+'.join([str(x)]*y) | ||
16 | + | ||
17 | +print(f'''--- | ||
18 | +type: text | ||
19 | +title: Multiplicação (tabuada) | ||
20 | +text: | | ||
21 | + Qual o resultado da multiplicação ${x}\\times {y}$? | ||
22 | +correct: ['{r}'] | ||
23 | +solution: | | ||
24 | + A multiplicação é a repetição da soma. Podemos fazer de duas maneiras: | ||
25 | + $$ {x}\\times {y} = {yy} = {r} $$ | ||
26 | + ou | ||
27 | + $$ {y}\\times {x} = {xx} = {r}. $$''') |
@@ -0,0 +1,24 @@ | @@ -0,0 +1,24 @@ | ||
1 | +--- | ||
2 | +# --------------------------------------------------------------------------- | ||
3 | +- type: generator | ||
4 | + ref: multiplication-table | ||
5 | + script: multiplication-table.py | ||
6 | + | ||
7 | +- type: checkbox | ||
8 | + ref: multiplication-properties | ||
9 | + title: Propriedades da multiplicação | ||
10 | + text: Indique quais as propriedades que a multiplicação satisfaz. | ||
11 | + options: | ||
12 | + # right | ||
13 | + - Existência de elemento neutro, $1x=x$. | ||
14 | + - Propriedade associativa, $(xy)z = x(yz)$. | ||
15 | + - Propriedade comutativa, $xy=yx$. | ||
16 | + - Existência de elemento absorvente, $0x=0$. | ||
17 | + # wrong | ||
18 | + - Existência de inverso, todos os números $x$ tem um inverso $1/x$ tal que | ||
19 | + $x(1/x)=1$. | ||
20 | + correct: [1, 1, 1, 1, -1] | ||
21 | + solution: | | ||
22 | + Na multiplicação nem todos os números têm inverso. Só têm inverso os números | ||
23 | + diferentes de zero. | ||
24 | + As outras propriedades são satisfeitas para todos os números. |
@@ -0,0 +1,59 @@ | @@ -0,0 +1,59 @@ | ||
1 | +--- | ||
2 | +# --------------------------------------------------------------------------- | ||
3 | +- type: radio | ||
4 | + ref: prime-number-definition | ||
5 | + title: Números primos | ||
6 | + text: | | ||
7 | + Qual a definição de número primo? | ||
8 | + options: | ||
9 | + - Um número $n>1$ é primo se só é divisível por si próprio e pela unidade. | ||
10 | + - Um número $n\ge 2$ é primo se só é divisível por si próprio e pela unidade. | ||
11 | + - Um número $n>1$ é primo se os seus divisores são apenas $1$ e $n$. | ||
12 | + - Um número $n\ge 2$ é primo se os seus divisores são apenas $1$ e $n$. | ||
13 | + # wrong | ||
14 | + - Um número $n$ é primo se é divisível por si próprio e pela unidade. | ||
15 | + - Um número $n>1$ é primo se é divisível por si próprio e pela unidade. | ||
16 | + - Um número $n\ge 2$ é primo se não tem divisores. | ||
17 | + choose: 3 | ||
18 | + correct: [1, 1, 1, 1, 0, 0, 0] | ||
19 | + solution: | ||
20 | + Um número $n$ é primo se $n\ge 2$ e se os únicos divisores forem $1$ e $n$. | ||
21 | + | ||
22 | +# --------------------------------------------------------------------------- | ||
23 | +- type: checkbox | ||
24 | + ref: even-odd-prime | ||
25 | + title: Números pares, ímpares e primos | ||
26 | + text: Indique as afirmações verdadeiras. | ||
27 | + options: | ||
28 | + - ['3 é primo', '4 é primo'] | ||
29 | + - ['2 é par', '3 é par'] | ||
30 | + - ['1 é ímpar', '2 é ímpar'] | ||
31 | + correct: [1, 1, 1] | ||
32 | + solution: | | ||
33 | + A tabela seguinte resume as propriedades dos números 1 a 4: | ||
34 | + | ||
35 | + Número | ímpar | par | primo | ||
36 | + :-----:|:-----:|:---:|:------: | ||
37 | + 1 | S | N | N | ||
38 | + 2 | N | S | S | ||
39 | + 3 | S | N | S | ||
40 | + 4 | N | S | N | ||
41 | + | ||
42 | +# --------------------------------------------------------------------------- | ||
43 | +- type: radio | ||
44 | + ref: prime-numbers | ||
45 | + title: Números primos | ||
46 | + text: Qual dos seguintes números é um primo múltiplo de 3? | ||
47 | + options: | ||
48 | + - 3 | ||
49 | + # wrong | ||
50 | + - 9 | ||
51 | + - 13 | ||
52 | + - 21 | ||
53 | + - 15 | ||
54 | + - Não há primos múltiplos de 3. | ||
55 | + max_tries: 1 | ||
56 | + solution: | | ||
57 | + O único número primo múltiplo de 3 é o próprio 3. | ||
58 | + Todos os outros múltiplos (6, 9, 12, 15, ...) têm 3 como divisor e portanto | ||
59 | + não são primos. |
demo/math/questions.yaml
@@ -1,87 +0,0 @@ | @@ -1,87 +0,0 @@ | ||
1 | ---- | ||
2 | -# --------------------------------------------------------------------------- | ||
3 | -- type: radio | ||
4 | - ref: distributive_property | ||
5 | - title: Propriedade distributiva | ||
6 | - text: | | ||
7 | - Qual das seguintes opções usa a propriedade distributiva para calcular | ||
8 | - $9\times 2 - 2\times 3$? | ||
9 | - options: | ||
10 | - # correct | ||
11 | - - $2\times(9 - 3)$ | ||
12 | - - $(9-3)\times 2$ | ||
13 | - # wrong | ||
14 | - - $18 - 6 = 12$ | ||
15 | - - $2\times 9 - 2\times 3$ | ||
16 | - - $2\times(9-2\times 3)$ | ||
17 | - correct: [1, 1, 0, 0, 0] | ||
18 | - choose: 3 | ||
19 | - max_tries: 1 | ||
20 | - solution: | | ||
21 | - Colocando o 2 em evidência, obtém-se a resposta correcta $2\times(9 - 3)$ | ||
22 | - ou $(9-3)\times 2$. | ||
23 | - | ||
24 | -# --------------------------------------------------------------------------- | ||
25 | -- type: checkbox | ||
26 | - ref: numbers | ||
27 | - title: Números pares e primos | ||
28 | - text: Indique as afirmações verdadeiras. | ||
29 | - options: | ||
30 | - - ['3 é primo', '4 é primo'] | ||
31 | - - ['2 é par', '3 é par'] | ||
32 | - - ['1 é ímpar', '2 é ímpar'] | ||
33 | - correct: [1, 1, 1] | ||
34 | - solution: | | ||
35 | - A tabela seguinte resume as propriedades dos números 1 a 4: | ||
36 | - | ||
37 | - Número | ímpar | par | primo | ||
38 | - :-----:|:-----:|:---:|:------: | ||
39 | - 1 | S | N | N | ||
40 | - 2 | N | S | S | ||
41 | - 3 | S | N | S | ||
42 | - 4 | N | S | N | ||
43 | - | ||
44 | -# --------------------------------------------------------------------------- | ||
45 | -- type: radio | ||
46 | - ref: prime_numbers | ||
47 | - title: Números primos | ||
48 | - text: Qual dos seguintes números é primo? | ||
49 | - options: | ||
50 | - - 13 | ||
51 | - - 12 | ||
52 | - - 14 | ||
53 | - - 1, a **unidade** | ||
54 | - max_tries: 1 | ||
55 | - solution: | | ||
56 | - O único número primo é o 13. | ||
57 | - | ||
58 | - O número 1, embora apenas seja divisível por ele próprio e pela unidade | ||
59 | - (que neste caso coincide com ele próprio), não é um número primo. Apenas | ||
60 | - são considerados números primos a partir do 2. | ||
61 | - | ||
62 | -# --------------------------------------------------------------------------- | ||
63 | -- type: checkbox | ||
64 | - ref: math-expressions | ||
65 | - title: Expressões matemáticas | ||
66 | - text: Quais das seguintes expressões são verdadeiras? | ||
67 | - options: | ||
68 | - - $1 > 0$ | ||
69 | - - $\sqrt{3} > \sqrt{2}$ | ||
70 | - - $e^{i\pi} + 1 = 0$ | ||
71 | - - $\frac{\partial f(x,y)}{\partial z} = 1$ | ||
72 | - - $-1 > 1$ | ||
73 | - # how many points for each checkmark (normalized afterwards): | ||
74 | - correct: [1, 1, 1, -1, -1] | ||
75 | - solution: | | ||
76 | - Das 5 opções existem 2 falsas. A derivada parcial é zero uma vez que a | ||
77 | - função não varia com $z$. A desigualdade $-1 > 1$ é também falsa, os | ||
78 | - números negativos são menores que os positivos. | ||
79 | - | ||
80 | -# --------------------------------------------------------------------------- | ||
81 | -# the program should print a question in yaml format. The args will be | ||
82 | -# sent as command line options when the program is run. | ||
83 | -- type: generator | ||
84 | - ref: overflow | ||
85 | - script: gen-multiples-of-3.py | ||
86 | - args: [11, 120] | ||
87 | - choose: 3 |
demo/solar_system/correct-first_3_planets.py
@@ -1,28 +0,0 @@ | @@ -1,28 +0,0 @@ | ||
1 | -#!/usr/bin/env python3 | ||
2 | - | ||
3 | -import re | ||
4 | -import sys | ||
5 | -import time | ||
6 | - | ||
7 | -s = sys.stdin.read() | ||
8 | - | ||
9 | -ans = set(re.findall(r'[\w]+', s.lower())) # convert answer to lowercase | ||
10 | -ans.difference_update({'e', 'a', 'o', 'planeta', 'planetas'}) # ignore words | ||
11 | - | ||
12 | -# correct set of planets | ||
13 | -planets = {'mercúrio', 'vénus', 'terra'} | ||
14 | - | ||
15 | -correct = set.intersection(ans, planets) # the ones I got right | ||
16 | -wrong = set.difference(ans, planets) # the ones I got wrong | ||
17 | - | ||
18 | -grade = (len(correct) - len(wrong)) / len(planets) | ||
19 | - | ||
20 | -comments = 'Certo' if grade == 1.0 else 'as iniciais dos planetas são M, V e T' | ||
21 | - | ||
22 | -out = f'''--- | ||
23 | -grade: {grade} | ||
24 | -comments: {comments}''' | ||
25 | - | ||
26 | -time.sleep(2) # simulate computation time (may generate timeout) | ||
27 | - | ||
28 | -print(out) |
demo/solar_system/correct-timeout.py
demo/solar_system/public/earth.jpg
1.02 MB
demo/solar_system/public/jupiter.gif
2.96 MB
demo/solar_system/public/planets.png
419 KB
demo/solar_system/public/saturn.jpg
117 KB
demo/solar_system/questions.yaml
@@ -1,69 +0,0 @@ | @@ -1,69 +0,0 @@ | ||
1 | ---- | ||
2 | - | ||
3 | -# --------------------------------------------------------------------------- | ||
4 | -- type: text | ||
5 | - ref: home-planet | ||
6 | - title: Sistema solar | ||
7 | - text: O nosso planeta chama-se planeta... | ||
8 | - correct: ['Terra', 'terra'] | ||
9 | - # opcional | ||
10 | - answer: Não é Marte... | ||
11 | - solution: | | ||
12 | - Embora algumas pessoas andem muitas vezes na Lua, o nosso planeta é o | ||
13 | - planeta **Terra**! | ||
14 | - | ||
15 | -  | ||
16 | - | ||
17 | -# --------------------------------------------------------------------------- | ||
18 | -- type: radio | ||
19 | - ref: solar-system | ||
20 | - title: Sistema solar | ||
21 | - text: | | ||
22 | -  | ||
23 | - | ||
24 | - Qual é o maior planeta do Sistema Solar? | ||
25 | - | ||
26 | - options: | ||
27 | - - Mercúrio | ||
28 | - - Marte | ||
29 | - - Júpiter | ||
30 | - - Têm todos o mesmo tamanho | ||
31 | - # opcional | ||
32 | - correct: 2 | ||
33 | - shuffle: false | ||
34 | - discount: true | ||
35 | - solution: | | ||
36 | - O maior planeta é Júpiter. Tem uma massa 1000 vezes inferior ao Sol, mas | ||
37 | - ainda assim 2.5 vezes maior que a massa de todos os outros planetas juntos. | ||
38 | - É um gigante gasoso maioritariamente composto por hidrogénio. | ||
39 | - | ||
40 | -  | ||
41 | - | ||
42 | -# --------------------------------------------------------------------------- | ||
43 | -- type: text-regex | ||
44 | - ref: saturn | ||
45 | - title: Sistema solar | ||
46 | - text: O planeta do sistema solar conhecido por ter aneis é o planeta... | ||
47 | - correct: '[Ss]aturno' | ||
48 | - solution: | | ||
49 | - O planeta Saturno é famoso pelos seus anéis. | ||
50 | - É o segundo maior planeta do Sistema Solar, a seguir a Júpiter. | ||
51 | - Tal como Júpiter, é um gigante gasoso. | ||
52 | - Os seus anéis são formados por partículas de gelo. | ||
53 | - | ||
54 | -  | ||
55 | - | ||
56 | -# --------------------------------------------------------------------------- | ||
57 | -- type: textarea | ||
58 | - ref: first_3_planets | ||
59 | - title: Sistema solar | ||
60 | - text: | | ||
61 | - Qual o nome dos três planetas mais próximos do Sol? | ||
62 | - Exemplo `Lua, Ceres e Vulcano` | ||
63 | - correct: correct-first_3_planets.py | ||
64 | - # correct: correct-timeout.py | ||
65 | - # opcional | ||
66 | - answer: Vulcano, Krypton, Plutão | ||
67 | - timeout: 3 | ||
68 | - solution: | | ||
69 | - Os 3 planetas mais perto do Sol são: Mercúrio, Vénus e Terra. |