Commit f4809e6f0f647e10fb039749d838ffe47f913a3f

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

- code cleaning questions.py

- fix image size in solution box
- update demo to match the new courses organization
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
demo/astronomy.yaml 0 → 100644
@@ -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
demo/astronomy/solar-system/correct-first_3_planets.py 0 → 100755
@@ -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)
demo/astronomy/solar-system/correct-timeout.py 0 → 100755
@@ -0,0 +1,11 @@ @@ -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/astronomy/solar-system/public/earth.jpg 0 → 100644

1.02 MB

demo/astronomy/solar-system/public/jupiter.gif 0 → 100644

2.96 MB

demo/astronomy/solar-system/public/planets.png 0 → 100644

419 KB

demo/astronomy/solar-system/public/saturn.jpg 0 → 100644

117 KB

demo/astronomy/solar-system/questions.yaml 0 → 100644
@@ -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 + ![Terra](earth.jpg)
  14 +
  15 +# ----------------------------------------------------------------------------
  16 +- type: radio
  17 + ref: largest-planet
  18 + title: Sistema solar
  19 + text: |
  20 + ![planetas](planets.png "Planetas do Sistema Solar")
  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 + ![Júpiter](jupiter.gif)
  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 + ![Saturno](saturn.jpg)
  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.
demo/courses.yaml 0 → 100644
@@ -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  
demo/math.yaml 0 → 100644
@@ -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
demo/math/addition/addition-two-digits.py 0 → 100755
@@ -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}.''')
demo/math/addition/questions.yaml 0 → 100644
@@ -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)  
demo/math/multiplication/multiplication-table.py 0 → 100755
@@ -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}. $$''')
demo/math/multiplication/questions.yaml 0 → 100644
@@ -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.
demo/math/prime-numbers/questions.yaml 0 → 100644
@@ -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
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
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/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 - ![Terra](earth.jpg)  
16 -  
17 -# ---------------------------------------------------------------------------  
18 -- type: radio  
19 - ref: solar-system  
20 - title: Sistema solar  
21 - text: |  
22 - ![planetas](planets.png " Planetas do Sistema Solar")  
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 - ![Júpiter](jupiter.gif)  
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 - ![Saturno](saturn.jpg)  
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.