Commit 7aac15935f30d1a27b841e5684c00aa67a1d7da3

Authored by Miguel Barão
2 parents 07654fe0 8cc73e47
Exists in master and in 1 other branch dev

Merge branch 'dev' of https://git.xdi.uevora.pt/mjsb/aprendizations into dev

@@ -2,9 +2,12 @@ @@ -2,9 +2,12 @@
2 # BUGS 2 # BUGS
3 3
4 - se na especificacao de um curso, a referencia do topico nao existir como directorio, rebenta. 4 - se na especificacao de um curso, a referencia do topico nao existir como directorio, rebenta.
  5 +- internal server error ao fazer logout no macos python3.8
  6 +- GET can get filtered by browser cache
5 - topicos chapter devem ser automaticamente completos assim que as dependencias são satisfeitas. Nao devia ser necessario (ou possivel?) clicar neles. 7 - topicos chapter devem ser automaticamente completos assim que as dependencias são satisfeitas. Nao devia ser necessario (ou possivel?) clicar neles.
6 - topicos do tipo learn deviam por defeito nao ser randomizados e assumir ficheiros `learn.yaml`. 8 - topicos do tipo learn deviam por defeito nao ser randomizados e assumir ficheiros `learn.yaml`.
7 - internal server error 500... experimentar cenario: aluno tem login efectuado, prof muda pw e faz login/logout. aluno obtem erro 500. 9 - internal server error 500... experimentar cenario: aluno tem login efectuado, prof muda pw e faz login/logout. aluno obtem erro 500.
  10 +- radio sem options rebenta com aprendizations --check
8 - chapters deviam ser mostrados unlocked, antes de mostrar a medalha. alunos pensam que já terminaram e não conseguem progredir por causa das dependencias. 11 - chapters deviam ser mostrados unlocked, antes de mostrar a medalha. alunos pensam que já terminaram e não conseguem progredir por causa das dependencias.
9 - if topic deps on invalid ref terminates server with "Unknown error". 12 - if topic deps on invalid ref terminates server with "Unknown error".
10 - warning nos topics que não são usados em nenhum curso 13 - warning nos topics que não são usados em nenhum curso
@@ -16,6 +19,7 @@ @@ -16,6 +19,7 @@
16 19
17 # TODO 20 # TODO
18 21
  22 +- shuffle das perguntas dentro de um topico
19 - alterar tabelas para incluir email de recuperacao de password (e outros avisos) 23 - alterar tabelas para incluir email de recuperacao de password (e outros avisos)
20 - registar last_seen e remover os antigos de cada vez que houver um login. 24 - registar last_seen e remover os antigos de cada vez que houver um login.
21 - indicar qtos topicos faltam (>=50%) para terminar o curso. 25 - indicar qtos topicos faltam (>=50%) para terminar o curso.
@@ -37,7 +41,6 @@ @@ -37,7 +41,6 @@
37 41
38 # FIXED 42 # FIXED
39 43
40 -- GET can get filtered by browser cache. the problem was with animate.css  
41 - templates question-*.html tem input hidden question_ref que não é usado. remover? 44 - templates question-*.html tem input hidden question_ref que não é usado. remover?
42 - goals se forem do tipo chapter deve importar todas as dependencias do chapter. 45 - goals se forem do tipo chapter deve importar todas as dependencias do chapter.
43 - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) 46 - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a)
1 # Questions 1 # Questions
2 2
3 -Questions are saved in files in the [YAML](http://www.yaml.org/start.html) format. Each file contains a list of questions like 3 +Questions are saved in files in the [YAML](http://www.yaml.org/start.html)
  4 +format. Each file contains a list of questions like
4 5
5 ```yaml 6 ```yaml
6 - type: radio 7 - type: radio
@@ -12,10 +13,11 @@ Questions are saved in files in the [YAML](http://www.yaml.org/start.html) forma @@ -12,10 +13,11 @@ Questions are saved in files in the [YAML](http://www.yaml.org/start.html) forma
12 ... 13 ...
13 ``` 14 ```
14 15
15 -where each question is specified in a dictionary.  
16 -The `type` key is mandatory and specifies the type of question (multiple choice, text, etc).  
17 -The other keys available will depend on the type of question.  
18 -The field `ref` is not strictly required but still recommended, if not defined it will default to a string with the filename and the question index, e.g., `questions.yaml:12`. 16 +where each question is specified in a dictionary. The `type` key is mandatory
  17 +and specifies the type of question (multiple choice, text, etc). The other
  18 +keys available will depend on the type of question. The field `ref` is not
  19 +strictly required but still recommended, if not defined it will default to a
  20 +string with the filename and the question index, e.g., `questions.yaml:12`.
19 21
20 The following types of questions are supported: 22 The following types of questions are supported:
21 23
@@ -34,15 +36,16 @@ type | kind of answer @@ -34,15 +36,16 @@ type | kind of answer
34 36
35 ### radio 37 ### radio
36 38
37 -Only one option can be selected as the answer. If no option is selected, the question is considered unanswered. 39 +Only one option can be selected as the answer. If no option is selected, the
  40 +question is considered unanswered.
38 41
39 The general format is 42 The general format is
40 43
41 ```yaml 44 ```yaml
42 - type: radio 45 - type: radio
43 - ref: question_reference 46 + ref: question_reference
44 title: My first question 47 title: My first question
45 - text: | 48 + text: |
46 Please select one option. 49 Please select one option.
47 options: 50 options:
48 - this one is the correct one 51 - this one is the correct one
@@ -54,30 +57,37 @@ The general format is @@ -54,30 +57,37 @@ The general format is
54 discount: yes # default: yes 57 discount: yes # default: yes
55 ``` 58 ```
56 59
57 -All fields are optional except `type` and `options`. `title` and `text` default to empty strings, `shuffle` and `discount` to `true`. 60 +All fields are optional except `type` and `options`. `title` and `text` default
  61 +to empty strings, `shuffle` and `discount` to `true`.
58 62
59 -The `correct` field can be used in multiple ways and in combination with `shuffle`, `discount` and `choose` fields: 63 +The `correct` field can be used in multiple ways and in combination with
  64 +`shuffle`, `discount` and `choose` fields:
60 65
61 -- if not present, the first option is considered correct (options are shuffled by default when presented to the student).  
62 -- it can be the index (0-based) of the correct option, e.g., `correct: 0` for the first option.  
63 -- it can be a list of numbers between 0 and 1, e.g., `correct: [1, 0, 0]`. In this case, the first option is 100% correct while the others are 0%. If `discount: true` (the default), then the wrong ones will be penalized by $-1/(n-1)=-\tfrac{1}{2}$, where $n$ is the number of options. 66 +- if not present, the first option is considered correct (options are shuffled
  67 + by default when presented to the student).
  68 +- it can be the index (0-based) of the correct option, e.g., `correct: 0` for
  69 + the first option.
  70 +- it can be a list of numbers between 0 and 1, e.g., `correct: [1, 0, 0]`. In
  71 + this case, the first option is 100% correct while the others are 0%. If `discount: true` (the default), then the wrong ones will be penalized by $-1/(n-1)=-\tfrac{1}{2}$, where $n$ is the number of options.
64 - there can be more than one correct option in the list, which is then marked in the correct field, e.g. `correct: [1, 1, 0]`. In this case, one of the correct options will be randomly selected, and the remaining wrong ones appended. 72 - there can be more than one correct option in the list, which is then marked in the correct field, e.g. `correct: [1, 1, 0]`. In this case, one of the correct options will be randomly selected, and the remaining wrong ones appended.
65 - there can also be a long list of right and wrong options from which to build the question options. E.g. if `correct: [1,1,1,0,0,0,0]` and `choose: 3` is defined, then 1 correct option and 2 wrong ones are randomly selected from the list. 73 - there can also be a long list of right and wrong options from which to build the question options. E.g. if `correct: [1,1,1,0,0,0,0]` and `choose: 3` is defined, then 1 correct option and 2 wrong ones are randomly selected from the list.
66 - finally it's also possible to have a question that is *"not-completely-right"* or *"not-completely-wrong"*. This can be done using numbers between 0 and 1, e.g., `correct: [1, 0.3, 0]`. This practice is discouraged. 74 - finally it's also possible to have a question that is *"not-completely-right"* or *"not-completely-wrong"*. This can be done using numbers between 0 and 1, e.g., `correct: [1, 0.3, 0]`. This practice is discouraged.
67 75
68 -In some situations one may not want the options to be shuffled. In that case use `shuffle: false`. 76 +In some situations one may not want the options to be shuffled. In that case
  77 +use `shuffle: false`.
69 78
70 ### checkbox 79 ### checkbox
71 80
72 -Zero, one or multiple options can be selected. The question is always considered as answered, even if no options are selected. 81 +Zero, one or multiple options can be selected. The question is always
  82 +considered as answered, even if no options are selected.
73 83
74 -The simplest format is 84 +The simplest format is
75 85
76 ```yaml 86 ```yaml
77 - type: checkbox 87 - type: checkbox
78 ref: question_reference 88 ref: question_reference
79 title: My second question 89 title: My second question
80 - text: | 90 + text: |
81 Please mark the correct options. 91 Please mark the correct options.
82 options: 92 options:
83 - this one is correct 93 - this one is correct
@@ -89,16 +99,22 @@ The simplest format is @@ -89,16 +99,22 @@ The simplest format is
89 discount: yes # default: yes 99 discount: yes # default: yes
90 ``` 100 ```
91 101
92 -All fields are optional except `type` and `options`. `title` and `text` default to empty strings, `shuffle` and `discount` to `true` and `choose` to the total number of options. 102 +All fields are optional except `type` and `options`. `title` and `text` default
  103 +to empty strings, `shuffle` and `discount` to `true` and `choose` to the total
  104 +number of options.
93 105
94 -When correcting an answer, each correctly marked/unmarked option gets the corresponding value from the list `correct: [1, -1, 1]` and each wrong gets its symmetrical. So in the previous example, to have a completely right answer the checboxes should be: marked, unmarked, marked. 106 +When correcting an answer, each correctly marked/unmarked option gets the
  107 +corresponding value from the list `correct: [1, -1, 1]` and each wrong gets its
  108 +symmetrical. So in the previous example, to have a completely right answer the
  109 +checboxes should be: marked, unmarked, marked.
95 110
96 -If `discount: no` then wrong options are given a value of 0.  
97 -Options are shuffled by default. A smaller number of options may be randomly selected by setting the option `choose`. 111 +If `discount: no` then wrong options are given a value of 0. Options are
  112 +shuffled by default. A smaller number of options may be randomly selected by
  113 +setting the option `choose`.
98 114
99 -A more advanced format is to have two versions for each option, one right and one wrong.  
100 -One of the versions is randomly selected when the question is generated.  
101 -For example, 115 +A more advanced format is to have two versions for each option, one right and
  116 +one wrong. One of the versions is randomly selected when the question is
  117 +generated. For example,
102 118
103 ```yaml 119 ```yaml
104 options: 120 options:
@@ -108,11 +124,12 @@ For example, @@ -108,11 +124,12 @@ For example,
108 correct: [1, -1, -1] 124 correct: [1, -1, -1]
109 ``` 125 ```
110 126
111 -If the first version is selected then the corresponding `correct` value is used, otherwise if  
112 -the second version is selected, then the symmetrical value is used instead. 127 +If the first version is selected then the corresponding `correct` value is
  128 +used, otherwise if the second version is selected, then the symmetrical value
  129 +is used instead.
113 130
114 -This format is useful to write questions that are presented in different ways to different students.  
115 -It also minimizes solution memorization. Example: 131 +This format is useful to write questions that are presented in different ways
  132 +to different students. It also minimizes solution memorization. Example:
116 133
117 ```yaml 134 ```yaml
118 options: 135 options:
@@ -122,7 +139,8 @@ It also minimizes solution memorization. Example: @@ -122,7 +139,8 @@ It also minimizes solution memorization. Example:
122 139
123 ### text 140 ### text
124 141
125 -The answer is a single line of text. Just compare the answered text with the strings provided in a list of answers considered to be right. 142 +The answer is a single line of text. Just compare the answered text with the
  143 +strings provided in a list of answers considered to be right.
126 144
127 ```yaml 145 ```yaml
128 - type: text 146 - type: text
@@ -144,12 +162,14 @@ Similar to text, but answers are validated by a regular expression. @@ -144,12 +162,14 @@ Similar to text, but answers are validated by a regular expression.
144 correct: !regex '[wW]eek' # default: '$.^' always wrong 162 correct: !regex '[wW]eek' # default: '$.^' always wrong
145 ``` 163 ```
146 164
147 -The regular expression is in a string and must be prefixed by the keyword `!regex`. 165 +The regular expression is in a string and must be prefixed by the keyword
  166 +`!regex`.
148 167
149 ### numeric-interval 168 ### numeric-interval
150 169
151 -Similar to text, but expects an integer or floating point number.  
152 -The answer is converted to a float and is considered correct if the number is in the given closed interval. 170 +Similar to text, but expects an integer or floating point number. The answer
  171 +is converted to a float and is considered correct if the number is in the given
  172 +closed interval.
153 173
154 ```yaml 174 ```yaml
155 - type: numeric-interval 175 - type: numeric-interval
@@ -161,9 +181,10 @@ The answer is converted to a float and is considered correct if the number is in @@ -161,9 +181,10 @@ The answer is converted to a float and is considered correct if the number is in
161 181
162 ### textarea 182 ### textarea
163 183
164 -Provides a multiline textarea for the answer.  
165 -The answered text is sent to the standard input of an external program for grading.  
166 -The printed output to standard output of the program is parsed as YAML to get the grade and optional comments. 184 +Provides a multiline textarea for the answer. The answered text is sent to the
  185 +standard input of an external program for grading. The printed output to
  186 +standard output of the program is parsed as YAML to get the grade and optional
  187 +comments.
167 188
168 ```yaml 189 ```yaml
169 - type: textarea 190 - type: textarea
@@ -185,7 +206,6 @@ comments: Almost there @@ -185,7 +206,6 @@ comments: Almost there
185 206
186 It can also just print the grade as a single number. 207 It can also just print the grade as a single number.
187 208
188 -  
189 ### information, warning, alert and success 209 ### information, warning, alert and success
190 210
191 These are not really questions, but just provides information for the student. 211 These are not really questions, but just provides information for the student.
@@ -201,8 +221,8 @@ Grading these type of "questions" yields always correct. @@ -201,8 +221,8 @@ Grading these type of "questions" yields always correct.
201 221
202 ### generator 222 ### generator
203 223
204 -Questions can be generated by external programs instead of being defined directly.  
205 -This allows great flexibility, and allows each instance of a question to be always different. 224 +This allows great flexibility, and allows each instance of a question to be
  225 +always different.
206 226
207 ```yaml 227 ```yaml
208 type: generator 228 type: generator
@@ -211,16 +231,18 @@ script: executable_program @@ -211,16 +231,18 @@ script: executable_program
211 arg: 10,20 231 arg: 10,20
212 ``` 232 ```
213 233
214 -A generator is an external program that generates a question dynamically. 234 +A generator is an external program that generates a question dynamically.
215 In the example above, the program to be run is `executable_program`. 235 In the example above, the program to be run is `executable_program`.
216 The `arg` is sent to the standard input of the `executable_program`. 236 The `arg` is sent to the standard input of the `executable_program`.
217 237
218 -Questions should be printed to the stdout in YAML format, similarly to how they are defined above (but without the list dash).  
219 -The printed question is then parsed to a dictionary which is then used to update the question.  
220 -The `type` is redefined from generator to something else and the other fields are also updated. 238 +Questions should be printed to the stdout in YAML format, similarly to how they
  239 +are defined above (but without the list dash). The printed question is then
  240 +parsed to a dictionary which is then used to update the question. The `type`
  241 +is redefined from generator to something else and the other fields are also
  242 +updated.
221 243
222 -A generator can be any executable program (written in any language) that prints to the standard output.  
223 -Example of a generator written in python: 244 +A generator can be any executable program (written in any language) that prints
  245 +to the standard output. Example of a generator written in python:
224 246
225 ```python 247 ```python
226 #!/usr/bin/env python3 248 #!/usr/bin/env python3
@@ -234,7 +256,8 @@ a,b = (int(n) for n in arg.split(',')) @@ -234,7 +256,8 @@ a,b = (int(n) for n in arg.split(','))
234 q = fr''' 256 q = fr'''
235 type: checkbox 257 type: checkbox
236 text: | 258 text: |
237 - Indique quais das seguintes adições resultam em overflow quando se considera a adição de números com sinal (complemento para 2) em registos de 8 bits. 259 + Indique quais das seguintes adições resultam em overflow quando se considera
  260 + a adição de números com sinal (complemento para 2) em registos de 8 bits.
238 261
239 Os números foram gerados aleatoriamente no intervalo de {a} a {b}. 262 Os números foram gerados aleatoriamente no intervalo de {a} a {b}.
240 options: 263 options:
@@ -254,14 +277,14 @@ print(q) @@ -254,14 +277,14 @@ print(q)
254 277
255 A generator cannot generate another generator, only real questions are acceptable. 278 A generator cannot generate another generator, only real questions are acceptable.
256 279
257 -# Writing text 280 +## Writing text
258 281
259 The text in the questions is interpreted as markdown with support for LaTeX formulas. 282 The text in the questions is interpreted as markdown with support for LaTeX formulas.
260 The best way to write text is to use indentation like this: 283 The best way to write text is to use indentation like this:
261 284
262 ```yaml 285 ```yaml
263 text: | 286 text: |
264 - Yes. this is ok: If not indented, "Yes" would be a boolean 287 + Yes. this is ok: If not indented, "Yes" would be a boolean
265 and colon would be interpreted as a dictionary key. 288 and colon would be interpreted as a dictionary key.
266 289
267 Images placed in the `public` subdirectory are accessible by 290 Images placed in the `public` subdirectory are accessible by
aprendizations/learnapp.py
@@ -17,6 +17,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict @@ -17,6 +17,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict
17 import bcrypt 17 import bcrypt
18 import networkx as nx 18 import networkx as nx
19 import sqlalchemy as sa 19 import sqlalchemy as sa
  20 +import sqlalchemy.orm as orm
20 21
21 # this project 22 # this project
22 from aprendizations.models import Student, Answer, Topic, StudentTopic 23 from aprendizations.models import Student, Answer, Topic, StudentTopic
@@ -112,7 +113,7 @@ class LearnApp(): @@ -112,7 +113,7 @@ class LearnApp():
112 self.courses = config['courses'] 113 self.courses = config['courses']
113 logger.info('Courses: %s', ', '.join(self.courses.keys())) 114 logger.info('Courses: %s', ', '.join(self.courses.keys()))
114 for cid, course in self.courses.items(): 115 for cid, course in self.courses.items():
115 - course.setdefault('title', '') # course title undefined 116 + course.setdefault('title', cid) # course title undefined
116 for goal in course['goals']: 117 for goal in course['goals']:
117 if goal not in self.deps.nodes(): 118 if goal not in self.deps.nodes():
118 msg = f'Goal "{goal}" from course "{cid}" does not exist' 119 msg = f'Goal "{goal}" from course "{cid}" does not exist'
@@ -249,14 +250,14 @@ class LearnApp(): @@ -249,14 +250,14 @@ class LearnApp():
249 return False 250 return False
250 251
251 loop = asyncio.get_running_loop() 252 loop = asyncio.get_running_loop()
252 - password = await loop.run_in_executor(None,  
253 - bcrypt.hashpw,  
254 - password.encode('utf-8'),  
255 - bcrypt.gensalt()) 253 + pw = await loop.run_in_executor(None,
  254 + bcrypt.hashpw,
  255 + password.encode('utf-8'),
  256 + bcrypt.gensalt())
256 257
257 with self._db_session() as sess: 258 with self._db_session() as sess:
258 user = sess.query(Student).get(uid) 259 user = sess.query(Student).get(uid)
259 - user.password = password 260 + user.password = pw
260 261
261 logger.info('User "%s" changed password', uid) 262 logger.info('User "%s" changed password', uid)
262 return True 263 return True
@@ -364,7 +365,7 @@ class LearnApp(): @@ -364,7 +365,7 @@ class LearnApp():
364 # ------------------------------------------------------------------------ 365 # ------------------------------------------------------------------------
365 # 366 #
366 # ------------------------------------------------------------------------ 367 # ------------------------------------------------------------------------
367 - def _add_missing_topics(self, topics: List[str]) -> None: 368 + def _add_missing_topics(self, topics: Iterable[str]) -> None:
368 ''' 369 '''
369 Fill db table 'Topic' with topics from the graph, if new 370 Fill db table 'Topic' with topics from the graph, if new
370 ''' 371 '''
@@ -386,7 +387,7 @@ class LearnApp(): @@ -386,7 +387,7 @@ class LearnApp():
386 'Use "initdb-aprendizations" to create') 387 'Use "initdb-aprendizations" to create')
387 388
388 engine = sa.create_engine(f'sqlite:///{database}', echo=False) 389 engine = sa.create_engine(f'sqlite:///{database}', echo=False)
389 - self.Session = sa.orm.sessionmaker(bind=engine) 390 + self.Session = orm.sessionmaker(bind=engine)
390 try: 391 try:
391 with self._db_session() as sess: 392 with self._db_session() as sess:
392 count_students: int = sess.query(Student).count() 393 count_students: int = sess.query(Student).count()
@@ -501,6 +502,7 @@ class LearnApp(): @@ -501,6 +502,7 @@ class LearnApp():
501 qref = question.get('ref', str(i)) # ref or number 502 qref = question.get('ref', str(i)) # ref or number
502 if qref in localrefs: 503 if qref in localrefs:
503 msg = f'Duplicate ref "{qref}" in "{topic["path"]}"' 504 msg = f'Duplicate ref "{qref}" in "{topic["path"]}"'
  505 + logger.error(msg)
504 raise LearnException(msg) 506 raise LearnException(msg)
505 localrefs.add(qref) 507 localrefs.add(qref)
506 508
aprendizations/main.py
@@ -195,7 +195,7 @@ def main(): @@ -195,7 +195,7 @@ def main():
195 '--------------------------------------------------------------', 195 '--------------------------------------------------------------',
196 sep='\n') 196 sep='\n')
197 sys.exit(1) 197 sys.exit(1)
198 - except LearnException: 198 + except LearnException as exc:
199 logging.critical('Failed to start backend') 199 logging.critical('Failed to start backend')
200 # sys.exit(1) 200 # sys.exit(1)
201 raise 201 raise
aprendizations/templates/rankings.html
@@ -83,7 +83,6 @@ @@ -83,7 +83,6 @@
83 {{ ' '.join(r[1].split()[n] for n in (0,-1)) }} 83 {{ ' '.join(r[1].split()[n] for n in (0,-1)) }}
84   84  
85 {{ '<i class="far fa-thumbs-up text-success" title="Mais de 75% de respostas correctas"></i>' if r[3] > 0.75 else '' }} 85 {{ '<i class="far fa-thumbs-up text-success" title="Mais de 75% de respostas correctas"></i>' if r[3] > 0.75 else '' }}
86 - {{ '<i class="fas fa-bug" title="Menos de 50% de respostas correctas" ></i>' if 0.0 < r[3] < 0.5 else '' }}  
87 </td> 86 </td>
88 <td> <!-- progress --> 87 <td> <!-- progress -->
89 <div class="progress"> 88 <div class="progress">