Commit b55c1442a462544142e735490a43391d13f23419
1 parent
8cc73e47
Exists in
master
and in
1 other branch
- fixed most mypy and pyright errors
- questions.py cleanup. Replaced __init__() by gen() method - setup.py now correctly specifies compatible sqlalchemy version
Showing
11 changed files
with
248 additions
and
196 deletions
Show diff stats
BUGS.md
@@ -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 | - | ||
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 | + | ||
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", |
setup.py
@@ -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 | ], |