Commit 8cc73e47cb1c9072c6bf46d80db08e90ab31cbd7
1 parent
6c018447
Exists in
master
and in
1 other branch
minor fixes
Showing
4 changed files
with
84 additions
and
57 deletions
Show diff stats
BUGS.md
| ... | ... | @@ -18,6 +18,7 @@ |
| 18 | 18 | |
| 19 | 19 | # TODO |
| 20 | 20 | |
| 21 | +- shuffle das perguntas dentro de um topico | |
| 21 | 22 | - alterar tabelas para incluir email de recuperacao de password (e outros avisos) |
| 22 | 23 | - registar last_seen e remover os antigos de cada vez que houver um login. |
| 23 | 24 | - indicar qtos topicos faltam (>=50%) para terminar o curso. | ... | ... |
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,9 +195,10 @@ 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 | - sys.exit(1) | |
| 200 | + raise | |
| 201 | + # sys.exit(1) | |
| 201 | 202 | except Exception: |
| 202 | 203 | logging.critical('Unknown error') |
| 203 | 204 | # sys.exit(1) | ... | ... |