Commit 7aac15935f30d1a27b841e5684c00aa67a1d7da3
Exists in
master
and in
1 other branch
Merge branch 'dev' of https://git.xdi.uevora.pt/mjsb/aprendizations into dev
Showing
5 changed files
with
85 additions
and
58 deletions
Show diff stats
BUGS.md
... | ... | @@ -2,9 +2,12 @@ |
2 | 2 | # BUGS |
3 | 3 | |
4 | 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 | 7 | - topicos chapter devem ser automaticamente completos assim que as dependencias são satisfeitas. Nao devia ser necessario (ou possivel?) clicar neles. |
6 | 8 | - topicos do tipo learn deviam por defeito nao ser randomizados e assumir ficheiros `learn.yaml`. |
7 | 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 | 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 | 12 | - if topic deps on invalid ref terminates server with "Unknown error". |
10 | 13 | - warning nos topics que não são usados em nenhum curso |
... | ... | @@ -16,6 +19,7 @@ |
16 | 19 | |
17 | 20 | # TODO |
18 | 21 | |
22 | +- shuffle das perguntas dentro de um topico | |
19 | 23 | - alterar tabelas para incluir email de recuperacao de password (e outros avisos) |
20 | 24 | - registar last_seen e remover os antigos de cada vez que houver um login. |
21 | 25 | - indicar qtos topicos faltam (>=50%) para terminar o curso. |
... | ... | @@ -37,7 +41,6 @@ |
37 | 41 | |
38 | 42 | # FIXED |
39 | 43 | |
40 | -- GET can get filtered by browser cache. the problem was with animate.css | |
41 | 44 | - templates question-*.html tem input hidden question_ref que não é usado. remover? |
42 | 45 | - goals se forem do tipo chapter deve importar todas as dependencias do chapter. |
43 | 46 | - initdb da integrity error se no mesmo comando existirem alunos repetidos (p.ex em ficheiros csv diferentes ou entre csv e opcao -a) | ... | ... |
QUESTIONS.md
1 | 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 | 6 | ```yaml |
6 | 7 | - type: radio |
... | ... | @@ -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 | 22 | The following types of questions are supported: |
21 | 23 | |
... | ... | @@ -34,15 +36,16 @@ type | kind of answer |
34 | 36 | |
35 | 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 | 42 | The general format is |
40 | 43 | |
41 | 44 | ```yaml |
42 | 45 | - type: radio |
43 | - ref: question_reference | |
46 | + ref: question_reference | |
44 | 47 | title: My first question |
45 | - text: | | |
48 | + text: | | |
46 | 49 | Please select one option. |
47 | 50 | options: |
48 | 51 | - this one is the correct one |
... | ... | @@ -54,30 +57,37 @@ The general format is |
54 | 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 | 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 | 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 | 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 | 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 | 86 | ```yaml |
77 | 87 | - type: checkbox |
78 | 88 | ref: question_reference |
79 | 89 | title: My second question |
80 | - text: | | |
90 | + text: | | |
81 | 91 | Please mark the correct options. |
82 | 92 | options: |
83 | 93 | - this one is correct |
... | ... | @@ -89,16 +99,22 @@ The simplest format is |
89 | 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 | 119 | ```yaml |
104 | 120 | options: |
... | ... | @@ -108,11 +124,12 @@ For example, |
108 | 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 | 134 | ```yaml |
118 | 135 | options: |
... | ... | @@ -122,7 +139,8 @@ It also minimizes solution memorization. Example: |
122 | 139 | |
123 | 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 | 145 | ```yaml |
128 | 146 | - type: text |
... | ... | @@ -144,12 +162,14 @@ Similar to text, but answers are validated by a regular expression. |
144 | 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 | 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 | 174 | ```yaml |
155 | 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 | 181 | |
162 | 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 | 189 | ```yaml |
169 | 190 | - type: textarea |
... | ... | @@ -185,7 +206,6 @@ comments: Almost there |
185 | 206 | |
186 | 207 | It can also just print the grade as a single number. |
187 | 208 | |
188 | - | |
189 | 209 | ### information, warning, alert and success |
190 | 210 | |
191 | 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 | 221 | |
202 | 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 | 227 | ```yaml |
208 | 228 | type: generator |
... | ... | @@ -211,16 +231,18 @@ script: executable_program |
211 | 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 | 235 | In the example above, the program to be run is `executable_program`. |
216 | 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 | 247 | ```python |
226 | 248 | #!/usr/bin/env python3 |
... | ... | @@ -234,7 +256,8 @@ a,b = (int(n) for n in arg.split(',')) |
234 | 256 | q = fr''' |
235 | 257 | type: checkbox |
236 | 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 | 262 | Os números foram gerados aleatoriamente no intervalo de {a} a {b}. |
240 | 263 | options: |
... | ... | @@ -254,14 +277,14 @@ print(q) |
254 | 277 | |
255 | 278 | A generator cannot generate another generator, only real questions are acceptable. |
256 | 279 | |
257 | -# Writing text | |
280 | +## Writing text | |
258 | 281 | |
259 | 282 | The text in the questions is interpreted as markdown with support for LaTeX formulas. |
260 | 283 | The best way to write text is to use indentation like this: |
261 | 284 | |
262 | 285 | ```yaml |
263 | 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 | 288 | and colon would be interpreted as a dictionary key. |
266 | 289 | |
267 | 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 | 17 | import bcrypt |
18 | 18 | import networkx as nx |
19 | 19 | import sqlalchemy as sa |
20 | +import sqlalchemy.orm as orm | |
20 | 21 | |
21 | 22 | # this project |
22 | 23 | from aprendizations.models import Student, Answer, Topic, StudentTopic |
... | ... | @@ -112,7 +113,7 @@ class LearnApp(): |
112 | 113 | self.courses = config['courses'] |
113 | 114 | logger.info('Courses: %s', ', '.join(self.courses.keys())) |
114 | 115 | for cid, course in self.courses.items(): |
115 | - course.setdefault('title', '') # course title undefined | |
116 | + course.setdefault('title', cid) # course title undefined | |
116 | 117 | for goal in course['goals']: |
117 | 118 | if goal not in self.deps.nodes(): |
118 | 119 | msg = f'Goal "{goal}" from course "{cid}" does not exist' |
... | ... | @@ -249,14 +250,14 @@ class LearnApp(): |
249 | 250 | return False |
250 | 251 | |
251 | 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 | 258 | with self._db_session() as sess: |
258 | 259 | user = sess.query(Student).get(uid) |
259 | - user.password = password | |
260 | + user.password = pw | |
260 | 261 | |
261 | 262 | logger.info('User "%s" changed password', uid) |
262 | 263 | return True |
... | ... | @@ -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 | 370 | Fill db table 'Topic' with topics from the graph, if new |
370 | 371 | ''' |
... | ... | @@ -386,7 +387,7 @@ class LearnApp(): |
386 | 387 | 'Use "initdb-aprendizations" to create') |
387 | 388 | |
388 | 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 | 391 | try: |
391 | 392 | with self._db_session() as sess: |
392 | 393 | count_students: int = sess.query(Student).count() |
... | ... | @@ -501,6 +502,7 @@ class LearnApp(): |
501 | 502 | qref = question.get('ref', str(i)) # ref or number |
502 | 503 | if qref in localrefs: |
503 | 504 | msg = f'Duplicate ref "{qref}" in "{topic["path"]}"' |
505 | + logger.error(msg) | |
504 | 506 | raise LearnException(msg) |
505 | 507 | localrefs.add(qref) |
506 | 508 | ... | ... |
aprendizations/main.py
... | ... | @@ -195,7 +195,7 @@ def main(): |
195 | 195 | '--------------------------------------------------------------', |
196 | 196 | sep='\n') |
197 | 197 | sys.exit(1) |
198 | - except LearnException: | |
198 | + except LearnException as exc: | |
199 | 199 | logging.critical('Failed to start backend') |
200 | 200 | # sys.exit(1) |
201 | 201 | raise | ... | ... |
aprendizations/templates/rankings.html
... | ... | @@ -83,7 +83,6 @@ |
83 | 83 | {{ ' '.join(r[1].split()[n] for n in (0,-1)) }} |
84 | 84 | |
85 | 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 | 86 | </td> |
88 | 87 | <td> <!-- progress --> |
89 | 88 | <div class="progress"> | ... | ... |