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

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">
... ...