Commit b55c1442a462544142e735490a43391d13f23419

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

- fixed most mypy and pyright errors

- questions.py cleanup. Replaced __init__() by gen() method
- setup.py now correctly specifies compatible sqlalchemy version
@@ -3,14 +3,19 @@ @@ -3,14 +3,19 @@
3 3
4 - internal server error ao fazer logout no macos python3.8 4 - internal server error ao fazer logout no macos python3.8
5 - GET can get filtered by browser cache 5 - GET can get filtered by browser cache
6 -- topicos chapter devem ser automaticamente completos assim que as dependencias são satisfeitas. Nao devia ser necessario (ou possivel?) clicar neles.  
7 -- topicos do tipo learn deviam por defeito nao ser randomizados e assumir ficheiros `learn.yaml`.  
8 -- internal server error 500... experimentar cenario: aluno tem login efectuado, prof muda pw e faz login/logout. aluno obtem erro 500. 6 +- topicos chapter devem ser automaticamente completos assim que as dependencias
  7 + são satisfeitas. Nao devia ser necessario (ou possivel?) clicar neles.
  8 +- topicos do tipo learn deviam por defeito nao ser randomizados e assumir
  9 + ficheiros `learn.yaml`.
  10 +- internal server error 500... experimentar cenario: aluno tem login efectuado,
  11 + prof muda pw e faz login/logout. aluno obtem erro 500.
9 - radio sem options rebenta com aprendizations --check 12 - radio sem options rebenta com aprendizations --check
10 -- chapters deviam ser mostrados unlocked, antes de mostrar a medalha. alunos pensam que já terminaram e não conseguem progredir por causa das dependencias. 13 +- chapters deviam ser mostrados unlocked, antes de mostrar a medalha. alunos
  14 + pensam que já terminaram e não conseguem progredir por causa das
  15 + dependencias.
11 - if topic deps on invalid ref terminates server with "Unknown error". 16 - if topic deps on invalid ref terminates server with "Unknown error".
12 - warning nos topics que não são usados em nenhum curso 17 - warning nos topics que não são usados em nenhum curso
13 -- nao esta a seguir o max_tries definido no ficheiro de dependencias. 18 +- nao esta a seguir o `max_tries` definido no ficheiro de dependencias.
14 - devia mostrar timeout para o aluno saber a razao. 19 - devia mostrar timeout para o aluno saber a razao.
15 - permitir configuracao para escolher entre static files locais ou remotos 20 - permitir configuracao para escolher entre static files locais ou remotos
16 - shift-enter não está a funcionar 21 - shift-enter não está a funcionar
@@ -19,67 +24,92 @@ @@ -19,67 +24,92 @@
19 # TODO 24 # TODO
20 25
21 - shuffle das perguntas dentro de um topico 26 - shuffle das perguntas dentro de um topico
22 -- alterar tabelas para incluir email de recuperacao de password (e outros avisos)  
23 -- registar last_seen e remover os antigos de cada vez que houver um login. 27 +- alterar tabelas para incluir email de recuperacao de password (e outros
  28 + avisos)
  29 +- registar `last_seen` e remover os antigos de cada vez que houver um login.
24 - indicar qtos topicos faltam (>=50%) para terminar o curso. 30 - indicar qtos topicos faltam (>=50%) para terminar o curso.
25 - ao fim de 3 tentativas com password errada, envia email com nova password. 31 - ao fim de 3 tentativas com password errada, envia email com nova password.
26 -- mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo expande as dependencias.  
27 -- mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado topicos. 32 +- mostrar capitulos e subtopicos de forma hierarquica. clicar no capitulo
  33 + expande as dependencias.
  34 +- mostrar rankings alunos/perguntas respondidas/% correctas/valor esperado
  35 + topicos.
28 - botão não sei... 36 - botão não sei...
29 - mostrar icon "loading..." enquanto está a corrigir uma pergunta. 37 - mostrar icon "loading..." enquanto está a corrigir uma pergunta.
30 - session management. close after inactive time. 38 - session management. close after inactive time.
31 - radio e checkboxes, aceitar numeros como seleccao das opcoes. 39 - radio e checkboxes, aceitar numeros como seleccao das opcoes.
32 -- reload das perguntas enquanto online. ver signal em http://stackabuse.com/python-async-await-tutorial/ 40 +- reload das perguntas enquanto online. ver signal em
  41 + [](http://stackabuse.com/python-async-await-tutorial/)
33 - tabela de progresso de todos os alunos por topico. 42 - tabela de progresso de todos os alunos por topico.
34 - tabela com perguntas / quantidade de respostas certas/erradas. 43 - tabela com perguntas / quantidade de respostas certas/erradas.
35 - tabela com topicos / quantidade de estrelas. 44 - tabela com topicos / quantidade de estrelas.
36 - pymips: activar/desactivar instruções 45 - pymips: activar/desactivar instruções
37 - titulos das perguntas não suportam markdown. 46 - titulos das perguntas não suportam markdown.
38 -- pagina report que permita ver tabela alunos/topicos, estatisticas perguntas mais falhadas, tempo médio por pergunta. 47 +- pagina report que permita ver tabela alunos/topicos, estatisticas perguntas
  48 + mais falhadas, tempo médio por pergunta.
39 - normalizar com perguntations. 49 - normalizar com perguntations.
40 50
41 # FIXED 51 # FIXED
42 52
43 -- templates question-*.html tem input hidden question_ref que não é usado. remover? 53 +- templates question-*.html tem input hidden question_ref que não é usado.
  54 + remover?
44 - goals se forem do tipo chapter deve importar todas as dependencias do chapter. 55 - goals se forem do tipo chapter deve importar todas as dependencias do chapter.
45 -- initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a)  
46 -- dependencias que não são goals de um curso, só devem aparecer se ainda não tiverem sido feitas. 56 +- initdb da integrity error se no mesmo comando existirem alunos repetidos
  57 + (p.ex em ficheiros csv diferentes ou entre csv e opcao -a)
  58 +- dependencias que não são goals de um curso, só devem aparecer se ainda não
  59 + tiverem sido feitas.
47 - ir para inicio da pagina quando le nova pergunta. 60 - ir para inicio da pagina quando le nova pergunta.
48 - CRITICAL nao esta a guardar o progresso na base de dados. 61 - CRITICAL nao esta a guardar o progresso na base de dados.
49 - mesma ref no mesmo ficheiro não é detectado. 62 - mesma ref no mesmo ficheiro não é detectado.
50 - enter nas respostas mostra json 63 - enter nas respostas mostra json
51 -- apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande) 64 +- apos clicar no botao responder, inactivar o input (importante quando o tempo
  65 + de correcção é grande)
52 - double click submits twice. 66 - double click submits twice.
53 -- checkbox devia ter correct no intervalo [0,1] tal como radio. em caso de desconto a correccção faz 2*x-1. isto permite a mesma semantica nos dois tipos de perguntas. 67 +- checkbox devia ter correct no intervalo [0,1] tal como radio. em caso de
  68 + desconto a correccção faz 2*x-1. isto permite a mesma semantica nos dois
  69 + tipos de perguntas.
54 - marking all options right in a radio question breaks! 70 - marking all options right in a radio question breaks!
55 - implementar servidor http com redirect para https. 71 - implementar servidor http com redirect para https.
56 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. 72 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question.
57 - click numa opcao checkbox fora da checkbox+label não está a funcionar. 73 - click numa opcao checkbox fora da checkbox+label não está a funcionar.
58 -- mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam centradas em toda a coluna mas apenas na largura do parágrafo.  
59 -- QFactory.generate() devia fazer run da gen_async, ou remover. 74 +- mathjax, formulas $$f(x)$$ nas opções de escolha multipla, não ficam
  75 + centradas em toda a coluna mas apenas na largura do parágrafo.
  76 +- QFactory.generate() devia fazer run da `gen_async,` ou remover.
60 - classificacoes so devia mostrar os que ja fizeram alguma coisa 77 - classificacoes so devia mostrar os que ja fizeram alguma coisa
61 - impedir que quando students.db não é encontrado, crie um ficheiro vazio. 78 - impedir que quando students.db não é encontrado, crie um ficheiro vazio.
62 -- permite definir goal, mas nao verifica se esta no grafo. rebenta no start_topic.  
63 -- 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. 79 +- permite definir goal, mas nao verifica se esta no grafo. rebenta no
  80 + `start_topic`.
  81 +- se num topico, a ultima pergunta tem imagens, o servidor nao fornece as
  82 + imagengs porque o `current_topic` passa a None antes de carregar no botao
  83 + continuar. O caminho é prefix+None e dá erro.
64 - caixas com os cursos não se ajustam bem com ecran estreito. 84 - caixas com os cursos não se ajustam bem com ecran estreito.
65 -- obter rankings por curso GET course=course_id  
66 -- no curso de linear algebra, as perguntas estao shuffled, mas nao deviam estar... nao esta a obedecer a keyword shuffle. 85 +- obter rankings por curso `GET course=course_id`
  86 +- no curso de linear algebra, as perguntas estao shuffled, mas nao deviam
  87 + estar... nao esta a obedecer a keyword shuffle.
67 - menu nao mostra as opcoes correctamente 88 - menu nao mostra as opcoes correctamente
68 - finish topic vai para a lista de cursos. devia ficar no mesmo curso. 89 - finish topic vai para a lista de cursos. devia ficar no mesmo curso.
69 - mathjax nao esta a correr sobre o titulo. 90 - mathjax nao esta a correr sobre o titulo.
70 - forgetting factor is hardcoded in student.py 91 - forgetting factor is hardcoded in student.py
71 - add aprendizatons --version 92 - add aprendizatons --version
72 -- se aluno abre dois tabs no browser, conseque navegar em simultaneo para perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um campo hidden que tenha um céodigo único que indique qual a pergunta. do lado do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz redirect para /. 93 +- se aluno abre dois tabs no browser, conseque navegar em simultaneo para
  94 + perguntas diferentes. quando submete uma delas dá asneira. Tem de haver um
  95 + campo hidden que tenha um céodigo único que indique qual a pergunta. do lado
  96 + do servidor apnas há o codigo da pergunta corrente, se forem diferentes faz
  97 + redirect para /.
73 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido. 98 - nos topicos learn.yaml, qd falha acrescenta no fim. nao faz sentido.
74 -- não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g. information-theory/source-coding-theory/block-codes 99 +- não esta a fazer render correcto de tabelas nas opcoes checkbox. e.g.
  100 + `information-theory/source-coding-theory/block-codes`
75 - max tries nas perguntas. 101 - max tries nas perguntas.
76 - mostrar feedback/solucoes quando acerta, ou excede max tries. 102 - mostrar feedback/solucoes quando acerta, ou excede max tries.
77 -- quando se pressiona "responde" rapido (enquanto a animacao dura), a pergunta passa para a seguinte sem haver o correspondente redraw, ou seja a proxima resposta nao é a da pergunta mostrada. 103 +- quando se pressiona "responde" rapido (enquanto a animacao dura), a pergunta
  104 + passa para a seguinte sem haver o correspondente redraw, ou seja a proxima
  105 + resposta nao é a da pergunta mostrada.
78 - botao para mostrar a solução quando se acerta. 106 - botao para mostrar a solução quando se acerta.
79 - não está a guardar o resultado no final do topico 107 - não está a guardar o resultado no final do topico
80 -- esta a permitir 2 logins em simultaneo do mesmo user. fica tudo baralhado se mxerem em simultaneo... 108 +- esta a permitir 2 logins em simultaneo do mesmo user. fica tudo baralhado se
  109 + mxerem em simultaneo...
81 - errar no ultimo topico nao mostra solucao? 110 - errar no ultimo topico nao mostra solucao?
82 -- quando a pergunta devolve comments, este é apresentado, mas fica persistente nas tentativas seguintes. devia ser limpo apos a segunda submissao. 111 +- quando a pergunta devolve comments, este é apresentado, mas fica persistente
  112 + nas tentativas seguintes. devia ser limpo apos a segunda submissao.
83 - na definicao dos topicos, indicar: 113 - na definicao dos topicos, indicar:
84 "file: questions.yaml" (default questions.yaml) 114 "file: questions.yaml" (default questions.yaml)
85 "shuffle: True/False" (default False) 115 "shuffle: True/False" (default False)
@@ -94,9 +124,11 @@ @@ -94,9 +124,11 @@
94 - each topic only loads a sample of K questions (max) in random order. 124 - each topic only loads a sample of K questions (max) in random order.
95 - change password modal nao aparece no ipad (safari e firefox) 125 - change password modal nao aparece no ipad (safari e firefox)
96 - detect questions in questions.yaml without ref -> error ou generate default. 126 - detect questions in questions.yaml without ref -> error ou generate default.
97 -- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado. 127 +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do
  128 + tornado.
98 - servir imagens/ficheiros. 129 - servir imagens/ficheiros.
99 -- radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). 130 +- radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma
  131 + selecção aleatoria destas (so com 1 certa).
100 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. 132 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória.
101 - async/threadpool no bcrypt do initdb. 133 - async/threadpool no bcrypt do initdb.
102 - numero de estrelas depende da proporcao entre certas e erradas. 134 - numero de estrelas depende da proporcao entre certas e erradas.
@@ -107,10 +139,12 @@ @@ -107,10 +139,12 @@
107 - remover learn.css uma vez que nao é usado em lado nenhum? 139 - remover learn.css uma vez que nao é usado em lado nenhum?
108 - check if user already logged in 140 - check if user already logged in
109 - mover javascript para ficheiros externos e carregar com script defer src 141 - mover javascript para ficheiros externos e carregar com script defer src
110 -- implementar xsrf. Ver [http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection]()  
111 -- se refs de um topic estao invalidos, nao carrega esse topico. devia haver um error nos logs a indicar qual o ref invalido. 142 +- implementar xsrf. Ver [](http://www.tornadoweb.org/en/stable/guide/security.html#cross-site-request-forgery-protection)
  143 +- se refs de um topic estao invalidos, nao carrega esse topico. devia haver um
  144 + error nos logs a indicar qual o ref invalido.
112 - link directo para topico nao valida se topico esta unlocked. 145 - link directo para topico nao valida se topico esta unlocked.
113 -- templates not working: quesntion-information, question-warning (remove all informative panels??) 146 +- templates not working: quesntion-information, question-warning (remove all
  147 + informative panels??)
114 - enderecos errados dao internal error. 148 - enderecos errados dao internal error.
115 - barra de progresso nao está visível. 149 - barra de progresso nao está visível.
116 - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4) 150 - tabs em textarea nao funcionam correctamente (insere 1 espaco em vez de 4)
@@ -120,14 +154,15 @@ @@ -120,14 +154,15 @@
120 - animação no final de cada topico para se perceber a transição 154 - animação no final de cada topico para se perceber a transição
121 - "<" is not escaped in markdown. 155 - "<" is not escaped in markdown.
122 - Está a mostrar a solução em 'comments'!!! 156 - Está a mostrar a solução em 'comments'!!!
123 -- database: answers não tem referencia para o topico, so para question_ref 157 +- database: answers não tem referencia para o topico, so para `question_ref`
124 - melhorar markdown das tabelas. 158 - melhorar markdown das tabelas.
125 - gravar evolucao na bd no final de cada topico. 159 - gravar evolucao na bd no final de cada topico.
126 - submeter questoes radio, da erro se nao escolher nenhuma opção. 160 - submeter questoes radio, da erro se nao escolher nenhuma opção.
127 - indentação da primeira linha de código não funciona. 161 - indentação da primeira linha de código não funciona.
128 - markdown com o mistune. 162 - markdown com o mistune.
129 - change password in maintopics.html, falta menu para lançar modal 163 - change password in maintopics.html, falta menu para lançar modal
130 -- ver documentacao de migracao para networkx 2.0 https://networkx.github.io/documentation/stable/release/migration_guide_from_1.x_to_2.0.html 164 +- ver documentacao de migracao para networkx 2.0
  165 + [](https://networkx.github.io/documentation/stable/release/migration_guide_from_1.x_to_2.0.html)
131 - script para adicionar users/reset passwords. 166 - script para adicionar users/reset passwords.
132 - os topicos locked devem estar inactivos no sidebar. 167 - os topicos locked devem estar inactivos no sidebar.
133 - enter faz GET /question, que responde com json no ecran. (solution: disabled enter) 168 - enter faz GET /question, que responde com json no ecran. (solution: disabled enter)
@@ -136,7 +171,8 @@ @@ -136,7 +171,8 @@
136 - indicar o topico actual no sidebar 171 - indicar o topico actual no sidebar
137 - reload da página rebenta o estado. 172 - reload da página rebenta o estado.
138 - text deve mostrar no html os valores iniciais de ans, se existir 173 - text deve mostrar no html os valores iniciais de ans, se existir
139 -- nao permite perguntas repetidas. iterar questions da configuracao em vez das do ficheiro. ver app.py linha 223. 174 +- nao permite perguntas repetidas. iterar questions da configuracao em vez das
  175 + do ficheiro. ver app.py linha 223.
140 - level depender do numero de respostas correctas 176 - level depender do numero de respostas correctas
141 - pymips a funcionar 177 - pymips a funcionar
142 - logs mostram que está a gerar cada pergunta 2 vezes...?? 178 - logs mostram que está a gerar cada pergunta 2 vezes...??
@@ -148,15 +184,18 @@ @@ -148,15 +184,18 @@
148 - se students.db não existe, rebenta. 184 - se students.db não existe, rebenta.
149 - não entra à primeira 185 - não entra à primeira
150 - configuração e linha de comando. 186 - configuração e linha de comando.
151 -- o browser é redireccionado para /question em vez de fazer um post?? quando se pressiona enter numa caixa text edit. 187 +- o browser é redireccionado para /question em vez de fazer um post?? quando se
  188 + pressiona enter numa caixa text edit.
152 - load/save the knowledge state of the student 189 - load/save the knowledge state of the student
153 - servir ficheiros de public temporariamente 190 - servir ficheiros de public temporariamente
154 - path dos generators scripts mal construido 191 - path dos generators scripts mal construido
155 - questions hardcoded in LearnApp. 192 - questions hardcoded in LearnApp.
156 - Factory para cada pergunta individual em vez de pool 193 - Factory para cada pergunta individual em vez de pool
157 -- implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona, enter submete. 194 +- implementar navegacao radio/checkbox. cursor cima/baixo, espaco selecciona,
  195 + enter submete.
158 - logging 196 - logging
159 -- textarea tem codigo para preencher o texto, mas ja não é necessário porque pergunta não é reloaded. 197 +- textarea tem codigo para preencher o texto, mas ja não é necessário porque
  198 + pergunta não é reloaded.
160 - gravar answers -> db 199 - gravar answers -> db
161 - como gerar key para secure cookie. 200 - como gerar key para secure cookie.
162 - https. certificados selfsigned, no-ip nao suporta certificados 201 - https. certificados selfsigned, no-ip nao suporta certificados
@@ -171,4 +210,5 @@ @@ -171,4 +210,5 @@
171 - clicar texto selecciona checkboxes/radio. 210 - clicar texto selecciona checkboxes/radio.
172 - focar text/textarea 211 - focar text/textarea
173 - implementar template base das perguntas base e estender para cada tipo. 212 - implementar template base das perguntas base e estender para cada tipo.
174 -- submissão com enter em perguntas text faz get? provavelmente está a fazer o submit do form em vez de ir pelo ajax. 213 +- submissão com enter em perguntas text faz get? provavelmente está a fazer o
  214 + submit do form em vez de ir pelo ajax.
aprendizations/initdb.py
@@ -14,6 +14,8 @@ from concurrent.futures import ThreadPoolExecutor @@ -14,6 +14,8 @@ from concurrent.futures import ThreadPoolExecutor
14 # third party libraries 14 # third party libraries
15 import bcrypt 15 import bcrypt
16 import sqlalchemy as sa 16 import sqlalchemy as sa
  17 +import sqlalchemy.orm as orm
  18 +from sqlalchemy.exc import IntegrityError
17 19
18 # this project 20 # this project
19 from aprendizations.models import Base, Student 21 from aprendizations.models import Base, Student
@@ -143,7 +145,7 @@ def main(): @@ -143,7 +145,7 @@ def main():
143 print(f'Using database: {args.db}') 145 print(f'Using database: {args.db}')
144 engine = sa.create_engine(f'sqlite:///{args.db}', echo=False) 146 engine = sa.create_engine(f'sqlite:///{args.db}', echo=False)
145 Base.metadata.create_all(engine) # Creates schema if needed 147 Base.metadata.create_all(engine) # Creates schema if needed
146 - session = sa.orm.sessionmaker(bind=engine)() 148 + session = orm.sessionmaker(bind=engine)()
147 149
148 # --- build list of students to insert/update 150 # --- build list of students to insert/update
149 students = [] 151 students = []
@@ -181,7 +183,7 @@ def main(): @@ -181,7 +183,7 @@ def main():
181 password=s['pw']) 183 password=s['pw'])
182 for s in new_students]) 184 for s in new_students])
183 session.commit() 185 session.commit()
184 - except sa.exc.IntegrityError: 186 + except IntegrityError:
185 print('!!! Integrity error. Aborted !!!\n') 187 print('!!! Integrity error. Aborted !!!\n')
186 session.rollback() 188 session.rollback()
187 189
aprendizations/learnapp.py
@@ -458,7 +458,7 @@ class LearnApp(): @@ -458,7 +458,7 @@ class LearnApp():
458 458
459 logger.info('Building questions factory:') 459 logger.info('Building questions factory:')
460 factory = dict() 460 factory = dict()
461 - for tref in self.deps.nodes(): 461 + for tref in self.deps.nodes:
462 factory.update(self._factory_for(tref)) 462 factory.update(self._factory_for(tref))
463 463
464 logger.info('Factory has %s questions', len(factory)) 464 logger.info('Factory has %s questions', len(factory))
aprendizations/main.py
@@ -7,7 +7,7 @@ Setup configurations and then runs the application. @@ -7,7 +7,7 @@ Setup configurations and then runs the application.
7 7
8 # python standard library 8 # python standard library
9 import argparse 9 import argparse
10 -import logging 10 +import logging.config
11 from os import environ, path 11 from os import environ, path
12 import ssl 12 import ssl
13 import sys 13 import sys
aprendizations/questions.py
@@ -19,7 +19,6 @@ from aprendizations.tools import run_script, run_script_async @@ -19,7 +19,6 @@ from aprendizations.tools import run_script, run_script_async
19 # setup logger for this module 19 # setup logger for this module
20 logger = logging.getLogger(__name__) 20 logger = logging.getLogger(__name__)
21 21
22 -  
23 QDict = NewType('QDict', Dict[str, Any]) 22 QDict = NewType('QDict', Dict[str, Any])
24 23
25 24
@@ -37,8 +36,11 @@ class Question(dict): @@ -37,8 +36,11 @@ class Question(dict):
37 for each student. 36 for each student.
38 Instances can shuffle options or automatically generate questions. 37 Instances can shuffle options or automatically generate questions.
39 ''' 38 '''
40 - def __init__(self, q: QDict) -> None:  
41 - super().__init__(q) 39 +
  40 + def gen(self) -> None:
  41 + '''
  42 + Sets defaults that are valid for any question type
  43 + '''
42 44
43 # add required keys if missing 45 # add required keys if missing
44 self.set_defaults(QDict({ 46 self.set_defaults(QDict({
@@ -82,10 +84,12 @@ class QuestionRadio(Question): @@ -82,10 +84,12 @@ class QuestionRadio(Question):
82 choose (int) # only used if shuffle=True 84 choose (int) # only used if shuffle=True
83 ''' 85 '''
84 86
85 - # ------------------------------------------------------------------------  
86 - def __init__(self, q: QDict) -> None:  
87 - super().__init__(q)  
88 - 87 + def gen(self) -> None:
  88 + '''
  89 + Sets defaults, performs checks and generates the actual question
  90 + by modifying the options and correct values
  91 + '''
  92 + super().gen()
89 try: 93 try:
90 nopts = len(self['options']) 94 nopts = len(self['options'])
91 except KeyError as exc: 95 except KeyError as exc:
@@ -212,8 +216,8 @@ class QuestionCheckbox(Question): @@ -212,8 +216,8 @@ class QuestionCheckbox(Question):
212 ''' 216 '''
213 217
214 # ------------------------------------------------------------------------ 218 # ------------------------------------------------------------------------
215 - def __init__(self, q: QDict) -> None:  
216 - super().__init__(q) 219 + def gen(self) -> None:
  220 + super().gen()
217 221
218 try: 222 try:
219 nopts = len(self['options']) 223 nopts = len(self['options'])
@@ -266,19 +270,6 @@ class QuestionCheckbox(Question): @@ -266,19 +270,6 @@ class QuestionCheckbox(Question):
266 f'Please fix "{self["ref"]}" in "{self["path"]}"') 270 f'Please fix "{self["ref"]}" in "{self["path"]}"')
267 logger.error(msg) 271 logger.error(msg)
268 raise QuestionException(msg) 272 raise QuestionException(msg)
269 - # msg0 = ('+--------------- BEHAVIOR CHANGE NOTICE ---------------+')  
270 - # msg1 = ('| Correct values in checkbox questions must be in the |')  
271 - # msg2 = ('| interval [0.0, 1.0]. I will convert to the new |')  
272 - # msg3 = ('| behavior, for now, but you should fix it. |')  
273 - # msg4 = ('+------------------------------------------------------+')  
274 - # logger.warning(msg0)  
275 - # logger.warning(msg1)  
276 - # logger.warning(msg2)  
277 - # logger.warning(msg3)  
278 - # logger.warning(msg4)  
279 - # logger.warning('please fix "%s"', self["ref"])  
280 - # # normalize to [0,1]  
281 - # self['correct'] = [(x+1)/2 for x in self['correct']]  
282 273
283 # if an option is a list of (right, wrong), pick one 274 # if an option is a list of (right, wrong), pick one
284 options = [] 275 options = []
@@ -334,9 +325,8 @@ class QuestionText(Question): @@ -334,9 +325,8 @@ class QuestionText(Question):
334 ''' 325 '''
335 326
336 # ------------------------------------------------------------------------ 327 # ------------------------------------------------------------------------
337 - def __init__(self, q: QDict) -> None:  
338 - super().__init__(q)  
339 - 328 + def gen(self) -> None:
  329 + super().gen()
340 self.set_defaults(QDict({ 330 self.set_defaults(QDict({
341 'text': '', 331 'text': '',
342 'correct': [], # no correct answers, always wrong 332 'correct': [], # no correct answers, always wrong
@@ -362,12 +352,13 @@ class QuestionText(Question): @@ -362,12 +352,13 @@ class QuestionText(Question):
362 'transformations => never correct', self["ref"]) 352 'transformations => never correct', self["ref"])
363 353
364 # ------------------------------------------------------------------------ 354 # ------------------------------------------------------------------------
365 - def transform(self, ans): 355 + def transform(self, ans: str):
366 '''apply optional filters to the answer''' 356 '''apply optional filters to the answer'''
367 357
  358 + # apply transformations in sequence
368 for transform in self['transform']: 359 for transform in self['transform']:
369 if transform == 'remove_space': # removes all spaces 360 if transform == 'remove_space': # removes all spaces
370 - ans = ans.replace(' ', '') 361 + ans = re.sub(r'\s+', '', ans)
371 elif transform == 'trim': # removes spaces around 362 elif transform == 'trim': # removes spaces around
372 ans = ans.strip() 363 ans = ans.strip()
373 elif transform == 'normalize_space': # replaces multiple spaces by one 364 elif transform == 'normalize_space': # replaces multiple spaces by one
@@ -386,7 +377,7 @@ class QuestionText(Question): @@ -386,7 +377,7 @@ class QuestionText(Question):
386 super().correct() 377 super().correct()
387 378
388 if self['answer'] is not None: 379 if self['answer'] is not None:
389 - answer = self.transform(self['answer']) # apply transformations 380 + answer = self.transform(self['answer'])
390 self['grade'] = 1.0 if answer in self['correct'] else 0.0 381 self['grade'] = 1.0 if answer in self['correct'] else 0.0
391 382
392 383
@@ -403,8 +394,8 @@ class QuestionTextRegex(Question): @@ -403,8 +394,8 @@ class QuestionTextRegex(Question):
403 ''' 394 '''
404 395
405 # ------------------------------------------------------------------------ 396 # ------------------------------------------------------------------------
406 - def __init__(self, q: QDict) -> None:  
407 - super().__init__(q) 397 + def gen(self) -> None:
  398 + super().gen()
408 399
409 self.set_defaults(QDict({ 400 self.set_defaults(QDict({
410 'text': '', 401 'text': '',
@@ -415,28 +406,19 @@ class QuestionTextRegex(Question): @@ -415,28 +406,19 @@ class QuestionTextRegex(Question):
415 if not isinstance(self['correct'], list): 406 if not isinstance(self['correct'], list):
416 self['correct'] = [self['correct']] 407 self['correct'] = [self['correct']]
417 408
418 - # converts patterns to compiled versions  
419 - try:  
420 - self['correct'] = [re.compile(a) for a in self['correct']]  
421 - except Exception as exc:  
422 - msg = f'Failed to compile regex in "{self["ref"]}"'  
423 - logger.error(msg)  
424 - raise QuestionException(msg) from exc  
425 -  
426 # ------------------------------------------------------------------------ 409 # ------------------------------------------------------------------------
427 def correct(self) -> None: 410 def correct(self) -> None:
428 super().correct() 411 super().correct()
429 if self['answer'] is not None: 412 if self['answer'] is not None:
430 - self['grade'] = 0.0  
431 for regex in self['correct']: 413 for regex in self['correct']:
432 try: 414 try:
433 - if regex.match(self['answer']): 415 + if re.fullmatch(regex, self['answer']):
434 self['grade'] = 1.0 416 self['grade'] = 1.0
435 return 417 return
436 except TypeError: 418 except TypeError:
437 - logger.error('While matching regex %s with answer "%s".',  
438 - regex.pattern, self["answer"])  
439 - 419 + logger.error('While matching regex "%s" with answer "%s".',
  420 + regex, self['answer'])
  421 + self['grade'] = 0.0
440 422
441 # ============================================================================ 423 # ============================================================================
442 class QuestionNumericInterval(Question): 424 class QuestionNumericInterval(Question):
@@ -449,8 +431,8 @@ class QuestionNumericInterval(Question): @@ -449,8 +431,8 @@ class QuestionNumericInterval(Question):
449 ''' 431 '''
450 432
451 # ------------------------------------------------------------------------ 433 # ------------------------------------------------------------------------
452 - def __init__(self, q: QDict) -> None:  
453 - super().__init__(q) 434 + def gen(self) -> None:
  435 + super().gen()
454 436
455 self.set_defaults(QDict({ 437 self.set_defaults(QDict({
456 'text': '', 438 'text': '',
@@ -510,8 +492,8 @@ class QuestionTextArea(Question): @@ -510,8 +492,8 @@ class QuestionTextArea(Question):
510 ''' 492 '''
511 493
512 # ------------------------------------------------------------------------ 494 # ------------------------------------------------------------------------
513 - def __init__(self, q: QDict) -> None:  
514 - super().__init__(q) 495 + def gen(self) -> None:
  496 + super().gen()
515 497
516 self.set_defaults(QDict({ 498 self.set_defaults(QDict({
517 'text': '', 499 'text': '',
@@ -591,8 +573,8 @@ class QuestionInformation(Question): @@ -591,8 +573,8 @@ class QuestionInformation(Question):
591 ''' 573 '''
592 574
593 # ------------------------------------------------------------------------ 575 # ------------------------------------------------------------------------
594 - def __init__(self, q: QDict) -> None:  
595 - super().__init__(q) 576 + def gen(self) -> None:
  577 + super().gen()
596 self.set_defaults(QDict({ 578 self.set_defaults(QDict({
597 'text': '', 579 'text': '',
598 })) 580 }))
@@ -604,6 +586,44 @@ class QuestionInformation(Question): @@ -604,6 +586,44 @@ class QuestionInformation(Question):
604 586
605 587
606 # ============================================================================ 588 # ============================================================================
  589 +def question_from(qdict: QDict) -> Question:
  590 + '''
  591 + Converts a question specified in a dict into an instance of Question()
  592 + '''
  593 + types = {
  594 + 'radio': QuestionRadio,
  595 + 'checkbox': QuestionCheckbox,
  596 + 'text': QuestionText,
  597 + 'text-regex': QuestionTextRegex,
  598 + 'numeric-interval': QuestionNumericInterval,
  599 + 'textarea': QuestionTextArea,
  600 + # -- informative panels --
  601 + 'information': QuestionInformation,
  602 + 'success': QuestionInformation,
  603 + 'warning': QuestionInformation,
  604 + 'alert': QuestionInformation,
  605 + }
  606 +
  607 + # Get class for this question type
  608 + try:
  609 + qclass = types[qdict['type']]
  610 + except KeyError:
  611 + logger.error('Invalid type "%s" in "%s"',
  612 + qdict['type'], qdict['ref'])
  613 + raise
  614 +
  615 + # Create an instance of Question() of appropriate type
  616 + try:
  617 + qinstance = qclass(QDict(qdict))
  618 + except QuestionException:
  619 + logger.error('Error generating "%s" in %s/%s',
  620 + qdict['ref'], qdict['path'], qdict['filename'])
  621 + raise
  622 +
  623 + return qinstance
  624 +
  625 +
  626 +# ============================================================================
607 class QFactory(): 627 class QFactory():
608 ''' 628 '''
609 QFactory is a class that can generate question instances, e.g. by shuffling 629 QFactory is a class that can generate question instances, e.g. by shuffling
@@ -636,24 +656,8 @@ class QFactory(): @@ -636,24 +656,8 @@ class QFactory():
636 grade = question['grade'] # get grade 656 grade = question['grade'] # get grade
637 ''' 657 '''
638 658
639 - # Depending on the type of question, a different question class will be  
640 - # instantiated. All these classes derive from the base class `Question`.  
641 - _types = {  
642 - 'radio': QuestionRadio,  
643 - 'checkbox': QuestionCheckbox,  
644 - 'text': QuestionText,  
645 - 'text-regex': QuestionTextRegex,  
646 - 'numeric-interval': QuestionNumericInterval,  
647 - 'textarea': QuestionTextArea,  
648 - # -- informative panels --  
649 - 'information': QuestionInformation,  
650 - 'success': QuestionInformation,  
651 - 'warning': QuestionInformation,  
652 - 'alert': QuestionInformation,  
653 - }  
654 -  
655 def __init__(self, qdict: QDict = QDict({})) -> None: 659 def __init__(self, qdict: QDict = QDict({})) -> None:
656 - self.question = qdict 660 + self.qdict = qdict
657 661
658 # ------------------------------------------------------------------------ 662 # ------------------------------------------------------------------------
659 async def gen_async(self) -> Question: 663 async def gen_async(self) -> Question:
@@ -662,44 +666,28 @@ class QFactory(): @@ -662,44 +666,28 @@ class QFactory():
662 which is a descendent of base class Question. 666 which is a descendent of base class Question.
663 ''' 667 '''
664 668
665 - logger.debug('generating %s...', self.question["ref"]) 669 + logger.debug('generating %s...', self.qdict["ref"])
666 # Shallow copy so that script generated questions will not replace 670 # Shallow copy so that script generated questions will not replace
667 # the original generators 671 # the original generators
668 - question = self.question.copy()  
669 - question['qid'] = str(uuid.uuid4()) # unique for each question 672 + qdict = QDict(self.qdict.copy())
  673 + qdict['qid'] = str(uuid.uuid4()) # unique for each question
670 674
671 # If question is of generator type, an external program will be run 675 # If question is of generator type, an external program will be run
672 # which will print a valid question in yaml format to stdout. This 676 # which will print a valid question in yaml format to stdout. This
673 # output is then yaml parsed into a dictionary `q`. 677 # output is then yaml parsed into a dictionary `q`.
674 - if question['type'] == 'generator':  
675 - logger.debug(' \\_ Running "%s".', question['script'])  
676 - question.setdefault('args', [])  
677 - question.setdefault('stdin', '')  
678 - script = path.join(question['path'], question['script']) 678 + if qdict['type'] == 'generator':
  679 + logger.debug(' \\_ Running "%s".', qdict['script'])
  680 + qdict.setdefault('args', [])
  681 + qdict.setdefault('stdin', '')
  682 + script = path.join(qdict['path'], qdict['script'])
679 out = await run_script_async(script=script, 683 out = await run_script_async(script=script,
680 - args=question['args'],  
681 - stdin=question['stdin'])  
682 - question.update(out) 684 + args=qdict['args'],
  685 + stdin=qdict['stdin'])
  686 + qdict.update(out)
683 687
684 - # Get class for this question type  
685 - try:  
686 - qclass = self._types[question['type']]  
687 - except KeyError:  
688 - logger.error('Invalid type "%s" in "%s"',  
689 - question['type'], question['ref'])  
690 - raise  
691 -  
692 - # Finally create an instance of Question()  
693 - try:  
694 - qinstance = qclass(QDict(question))  
695 - except QuestionException:  
696 - logger.error('Error generating question "%s". See "%s/%s"',  
697 - question['ref'],  
698 - question['path'],  
699 - question['filename'])  
700 - raise  
701 -  
702 - return qinstance 688 + question = question_from(qdict) # returns a Question instance
  689 + question.gen()
  690 + return question
703 691
704 # ------------------------------------------------------------------------ 692 # ------------------------------------------------------------------------
705 def generate(self) -> Question: 693 def generate(self) -> Question:
aprendizations/serve.py
@@ -7,7 +7,7 @@ Webserver @@ -7,7 +7,7 @@ Webserver
7 import asyncio 7 import asyncio
8 import base64 8 import base64
9 import functools 9 import functools
10 -import logging.config 10 +import logging
11 import mimetypes 11 import mimetypes
12 from os.path import join, dirname, expanduser 12 from os.path import join, dirname, expanduser
13 import signal 13 import signal
@@ -16,6 +16,8 @@ from typing import List, Optional, Union @@ -16,6 +16,8 @@ from typing import List, Optional, Union
16 import uuid 16 import uuid
17 17
18 # third party libraries 18 # third party libraries
  19 +import tornado.httpserver
  20 +import tornado.ioloop
19 import tornado.web 21 import tornado.web
20 from tornado.escape import to_unicode 22 from tornado.escape import to_unicode
21 23
@@ -89,11 +91,14 @@ class BaseHandler(tornado.web.RequestHandler): @@ -89,11 +91,14 @@ class BaseHandler(tornado.web.RequestHandler):
89 def get_current_user(self): 91 def get_current_user(self):
90 '''called on every method decorated with @tornado.web.authenticated''' 92 '''called on every method decorated with @tornado.web.authenticated'''
91 user_cookie = self.get_secure_cookie('aprendizations_user') 93 user_cookie = self.get_secure_cookie('aprendizations_user')
  94 + counter_cookie = self.get_secure_cookie('counter')
92 if user_cookie is not None: 95 if user_cookie is not None:
93 uid = user_cookie.decode('utf-8') 96 uid = user_cookie.decode('utf-8')
94 - counter = self.get_secure_cookie('counter').decode('utf-8')  
95 - if counter == str(self.learn.get_login_counter(uid)):  
96 - return uid 97 +
  98 + if counter_cookie is not None:
  99 + counter = counter_cookie.decode('utf-8')
  100 + if counter == str(self.learn.get_login_counter(uid)):
  101 + return uid
97 return None 102 return None
98 103
99 104
@@ -141,7 +146,7 @@ class LoginHandler(BaseHandler): @@ -141,7 +146,7 @@ class LoginHandler(BaseHandler):
141 Perform authentication and redirects to application if successful 146 Perform authentication and redirects to application if successful
142 ''' 147 '''
143 148
144 - userid = self.get_body_argument('uid').lstrip('l') 149 + userid = (self.get_body_argument('uid') or '').lstrip('l')
145 passwd = self.get_body_argument('pw') 150 passwd = self.get_body_argument('pw')
146 151
147 login_ok = await self.learn.login(userid, passwd) 152 login_ok = await self.learn.login(userid, passwd)
@@ -310,22 +315,19 @@ class FileHandler(BaseHandler): @@ -310,22 +315,19 @@ class FileHandler(BaseHandler):
310 uid = self.current_user 315 uid = self.current_user
311 public_dir = self.learn.get_current_public_dir(uid) 316 public_dir = self.learn.get_current_public_dir(uid)
312 filepath = expanduser(join(public_dir, filename)) 317 filepath = expanduser(join(public_dir, filename))
313 - content_type = mimetypes.guess_type(filename)[0]  
314 318
315 try: 319 try:
316 with open(filepath, 'rb') as file: 320 with open(filepath, 'rb') as file:
317 data = file.read() 321 data = file.read()
318 - except FileNotFoundError:  
319 - logger.error('File not found: %s', filepath)  
320 - except PermissionError:  
321 - logger.error('No permission: %s', filepath)  
322 - except Exception: 322 + except OSError:
323 logger.error('Error reading: %s', filepath) 323 logger.error('Error reading: %s', filepath)
324 raise 324 raise
325 - else: 325 +
  326 + content_type = mimetypes.guess_type(filename)[0]
  327 + if content_type is not None:
326 self.set_header("Content-Type", content_type) 328 self.set_header("Content-Type", content_type)
327 - self.write(data)  
328 - await self.flush() 329 + self.write(data)
  330 + await self.flush()
329 331
330 332
331 # ============================================================================ 333 # ============================================================================
aprendizations/student.py
@@ -81,32 +81,32 @@ class StudentState(): @@ -81,32 +81,32 @@ class StudentState():
81 self.topic_sequence = self._recommend_sequence(topics) 81 self.topic_sequence = self._recommend_sequence(topics)
82 82
83 # ------------------------------------------------------------------------ 83 # ------------------------------------------------------------------------
84 - async def start_topic(self, topic: str) -> None: 84 + async def start_topic(self, topic_ref: str) -> None:
85 ''' 85 '''
86 Start a new topic. 86 Start a new topic.
87 questions: list of generated questions to do in the given topic 87 questions: list of generated questions to do in the given topic
88 current_question: the current question to be presented 88 current_question: the current question to be presented
89 ''' 89 '''
90 90
91 - logger.debug('start topic "%s"', topic) 91 + logger.debug('start topic "%s"', topic_ref)
92 92
93 # avoid regenerating questions in the middle of the current topic 93 # avoid regenerating questions in the middle of the current topic
94 - if self.current_topic == topic and self.uid != '0': 94 + if self.current_topic == topic_ref and self.uid != '0':
95 logger.info('Restarting current topic is not allowed.') 95 logger.info('Restarting current topic is not allowed.')
96 return 96 return
97 97
98 # do not allow locked topics 98 # do not allow locked topics
99 - if self.is_locked(topic) and self.uid != '0':  
100 - logger.debug('is locked "%s"', topic) 99 + if self.is_locked(topic_ref) and self.uid != '0':
  100 + logger.debug('is locked "%s"', topic_ref)
101 return 101 return
102 102
103 self.previous_topic: Optional[str] = None 103 self.previous_topic: Optional[str] = None
104 104
105 # choose k questions 105 # choose k questions
106 - self.current_topic = topic 106 + self.current_topic = topic_ref
107 self.correct_answers = 0 107 self.correct_answers = 0
108 self.wrong_answers = 0 108 self.wrong_answers = 0
109 - topic = self.deps.nodes[topic] 109 + topic = self.deps.nodes[topic_ref]
110 k = topic['choose'] 110 k = topic['choose']
111 if topic['shuffle_questions']: 111 if topic['shuffle_questions']:
112 questions = random.sample(topic['questions'], k=k) 112 questions = random.sample(topic['questions'], k=k)
aprendizations/templates/rankings.html
@@ -69,27 +69,23 @@ @@ -69,27 +69,23 @@
69 </thead> 69 </thead>
70 <tbody> 70 <tbody>
71 {% for i,r in enumerate(rankings) %} 71 {% for i,r in enumerate(rankings) %}
72 - {% if r[0] == uid %}  
73 - <tr class="table-primary"> <!-- this is me -->  
74 - {% else %}  
75 - <tr>  
76 - {% end %}  
77 - <td class="text-center"> <!-- rank -->  
78 - <strong>  
79 - {{'<i class="fas fa-crown fa-2x text-warning"></i>' if i==0 else i+1}}  
80 - </strong>  
81 - </td>  
82 - <td> <!-- student name -->  
83 - {{ ' '.join(r[1].split()[n] for n in (0,-1)) }}  
84 - &nbsp;  
85 - {{ '<i class="far fa-thumbs-up text-success" title="Mais de 75% de respostas correctas"></i>' if r[3] > 0.75 else '' }}  
86 - </td>  
87 - <td> <!-- progress -->  
88 - <div class="progress">  
89 - <div class="progress-bar" role="progressbar" style="width: {{100*r[2]}}%;" aria-valuenow="{{round(100*r[2])}}%" aria-valuemin="0" aria-valuemax="100">{{round(100*r[2])}}%</div>  
90 - </div>  
91 - </td>  
92 - </tr> 72 + <tr class="{{ 'table-warning' if r[0] == uid else '' }}">
  73 + <td class="text-center"> <!-- rank -->
  74 + <strong>
  75 + {{ '<i class="fas fa-crown fa-2x text-warning"></i>' if i==0 else i+1 }}
  76 + </strong>
  77 + </td>
  78 + <td> <!-- student name -->
  79 + {{ ' '.join(r[1].split()[n] for n in (0,-1)) }}
  80 + &nbsp;
  81 + {{ '<i class="far fa-thumbs-up text-success" title="Mais de 75% de respostas correctas"></i>' if r[3] > 0.75 else '' }}
  82 + </td>
  83 + <td> <!-- progress -->
  84 + <div class="progress">
  85 + <div class="progress-bar" role="progressbar" style="width: {{100*r[2]}}%;" aria-valuenow="{{round(100*r[2])}}%" aria-valuemin="0" aria-valuemax="100">{{round(100*r[2])}}%</div>
  86 + </div>
  87 + </td>
  88 + </tr>
93 {% end %} 89 {% end %}
94 </tbody> 90 </tbody>
95 </table> 91 </table>
aprendizations/tools.py
@@ -228,7 +228,7 @@ async def run_script_async(script: str, @@ -228,7 +228,7 @@ async def run_script_async(script: str,
228 ) 228 )
229 229
230 try: 230 try:
231 - stdout, stderr = await asyncio.wait_for( 231 + stdout, _ = await asyncio.wait_for(
232 p.communicate(input=stdin.encode('utf-8')), 232 p.communicate(input=stdin.encode('utf-8')),
233 timeout=timeout 233 timeout=timeout
234 ) 234 )
package-lock.json
1 { 1 {
  2 + "name": "aprendizations",
  3 + "lockfileVersion": 2,
2 "requires": true, 4 "requires": true,
3 - "lockfileVersion": 1, 5 + "packages": {
  6 + "": {
  7 + "dependencies": {
  8 + "@fortawesome/fontawesome-free": "^5.15.2",
  9 + "codemirror": "^5.59.1",
  10 + "mdbootstrap": "^4.19.2"
  11 + }
  12 + },
  13 + "node_modules/@fortawesome/fontawesome-free": {
  14 + "version": "5.15.3",
  15 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
  16 + "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==",
  17 + "hasInstallScript": true,
  18 + "engines": {
  19 + "node": ">=6"
  20 + }
  21 + },
  22 + "node_modules/codemirror": {
  23 + "version": "5.61.1",
  24 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
  25 + "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
  26 + },
  27 + "node_modules/mdbootstrap": {
  28 + "version": "4.19.2",
  29 + "resolved": "https://registry.npmjs.org/mdbootstrap/-/mdbootstrap-4.19.2.tgz",
  30 + "integrity": "sha512-a+LwPflYRYwlmYKTvftW0X7SfOMrRZ02qZjrssNko1lPU/HR5JRFc1uwa3Dmmw+6TwsYH760waqdghBFrucpOw=="
  31 + }
  32 + },
4 "dependencies": { 33 "dependencies": {
5 "@fortawesome/fontawesome-free": { 34 "@fortawesome/fontawesome-free": {
6 - "version": "5.15.2",  
7 - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.2.tgz",  
8 - "integrity": "sha512-7l/AX41m609L/EXI9EKH3Vs3v0iA8tKlIOGtw+kgcoanI7p+e4I4GYLqW3UXWiTnjSFymKSmTTPKYrivzbxxqA=="  
9 - },  
10 - "animate.css": {  
11 - "version": "4.1.1",  
12 - "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",  
13 - "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==" 35 + "version": "5.15.3",
  36 + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
  37 + "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
14 }, 38 },
15 "codemirror": { 39 "codemirror": {
16 - "version": "5.59.1",  
17 - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.1.tgz",  
18 - "integrity": "sha512-d0SSW/PCCD4LoSCBPdnP0BzmZB1v3emomCUtVlIWgZHJ06yVeBOvBtOH7vYz707pfAvEeWbO9aP6akh8vl1V3w==" 40 + "version": "5.61.1",
  41 + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
  42 + "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
19 }, 43 },
20 "mdbootstrap": { 44 "mdbootstrap": {
21 "version": "4.19.2", 45 "version": "4.19.2",
@@ -18,13 +18,13 @@ setup( @@ -18,13 +18,13 @@ setup(
18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git", 18 url="https://git.xdi.uevora.pt/mjsb/aprendizations.git",
19 packages=find_packages(), 19 packages=find_packages(),
20 include_package_data=True, # install files from MANIFEST.in 20 include_package_data=True, # install files from MANIFEST.in
21 - python_requires='>=3.7.*', 21 + python_requires='>=3.8.*',
22 install_requires=[ 22 install_requires=[
23 'tornado>=6.0', 23 'tornado>=6.0',
24 'mistune', 24 'mistune',
25 'pyyaml>=5.1', 25 'pyyaml>=5.1',
26 'pygments', 26 'pygments',
27 - 'sqlalchemy', 27 + 'sqlalchemy<1.4',
28 'bcrypt>=3.1', 28 'bcrypt>=3.1',
29 'networkx>=2.4' 29 'networkx>=2.4'
30 ], 30 ],