diff --git a/BUGS.md b/BUGS.md index ee3b2e7..33179a4 100644 --- a/BUGS.md +++ b/BUGS.md @@ -18,6 +18,7 @@ # TODO +- shuffle das perguntas dentro de um topico - alterar tabelas para incluir email de recuperacao de password (e outros avisos) - registar last_seen e remover os antigos de cada vez que houver um login. - indicar qtos topicos faltam (>=50%) para terminar o curso. diff --git a/QUESTIONS.md b/QUESTIONS.md index e5fdfda..0c22800 100644 --- a/QUESTIONS.md +++ b/QUESTIONS.md @@ -1,6 +1,7 @@ # Questions -Questions are saved in files in the [YAML](http://www.yaml.org/start.html) format. Each file contains a list of questions like +Questions are saved in files in the [YAML](http://www.yaml.org/start.html) +format. Each file contains a list of questions like ```yaml - type: radio @@ -12,10 +13,11 @@ Questions are saved in files in the [YAML](http://www.yaml.org/start.html) forma ... ``` -where each question is specified in a dictionary. -The `type` key is mandatory and specifies the type of question (multiple choice, text, etc). -The other keys available will depend on the type of question. -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`. +where each question is specified in a dictionary. The `type` key is mandatory +and specifies the type of question (multiple choice, text, etc). The other +keys available will depend on the type of question. 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`. The following types of questions are supported: @@ -34,15 +36,16 @@ type | kind of answer ### radio -Only one option can be selected as the answer. If no option is selected, the question is considered unanswered. +Only one option can be selected as the answer. If no option is selected, the +question is considered unanswered. The general format is ```yaml - type: radio - ref: question_reference + ref: question_reference title: My first question - text: | + text: | Please select one option. options: - this one is the correct one @@ -54,30 +57,37 @@ The general format is discount: yes # default: yes ``` -All fields are optional except `type` and `options`. `title` and `text` default to empty strings, `shuffle` and `discount` to `true`. +All fields are optional except `type` and `options`. `title` and `text` default +to empty strings, `shuffle` and `discount` to `true`. -The `correct` field can be used in multiple ways and in combination with `shuffle`, `discount` and `choose` fields: +The `correct` field can be used in multiple ways and in combination with +`shuffle`, `discount` and `choose` fields: -- if not present, the first option is considered correct (options are shuffled by default when presented to the student). -- it can be the index (0-based) of the correct option, e.g., `correct: 0` for the first option. -- 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. +- if not present, the first option is considered correct (options are shuffled + by default when presented to the student). +- it can be the index (0-based) of the correct option, e.g., `correct: 0` for + the first option. +- 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. - 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. - 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. - 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. -In some situations one may not want the options to be shuffled. In that case use `shuffle: false`. +In some situations one may not want the options to be shuffled. In that case +use `shuffle: false`. ### checkbox -Zero, one or multiple options can be selected. The question is always considered as answered, even if no options are selected. +Zero, one or multiple options can be selected. The question is always +considered as answered, even if no options are selected. -The simplest format is +The simplest format is ```yaml - type: checkbox ref: question_reference title: My second question - text: | + text: | Please mark the correct options. options: - this one is correct @@ -89,16 +99,22 @@ The simplest format is discount: yes # default: yes ``` -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. +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. -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. +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. -If `discount: no` then wrong options are given a value of 0. -Options are shuffled by default. A smaller number of options may be randomly selected by setting the option `choose`. +If `discount: no` then wrong options are given a value of 0. Options are +shuffled by default. A smaller number of options may be randomly selected by +setting the option `choose`. -A more advanced format is to have two versions for each option, one right and one wrong. -One of the versions is randomly selected when the question is generated. -For example, +A more advanced format is to have two versions for each option, one right and +one wrong. One of the versions is randomly selected when the question is +generated. For example, ```yaml options: @@ -108,11 +124,12 @@ For example, correct: [1, -1, -1] ``` -If the first version is selected then the corresponding `correct` value is used, otherwise if -the second version is selected, then the symmetrical value is used instead. +If the first version is selected then the corresponding `correct` value is +used, otherwise if the second version is selected, then the symmetrical value +is used instead. -This format is useful to write questions that are presented in different ways to different students. -It also minimizes solution memorization. Example: +This format is useful to write questions that are presented in different ways +to different students. It also minimizes solution memorization. Example: ```yaml options: @@ -122,7 +139,8 @@ It also minimizes solution memorization. Example: ### text -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. +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. ```yaml - type: text @@ -144,12 +162,14 @@ Similar to text, but answers are validated by a regular expression. correct: !regex '[wW]eek' # default: '$.^' always wrong ``` -The regular expression is in a string and must be prefixed by the keyword `!regex`. +The regular expression is in a string and must be prefixed by the keyword +`!regex`. ### numeric-interval -Similar to text, but expects an integer or floating point number. -The answer is converted to a float and is considered correct if the number is in the given closed interval. +Similar to text, but expects an integer or floating point number. The answer +is converted to a float and is considered correct if the number is in the given +closed interval. ```yaml - type: numeric-interval @@ -161,9 +181,10 @@ The answer is converted to a float and is considered correct if the number is in ### textarea -Provides a multiline textarea for the answer. -The answered text is sent to the standard input of an external program for grading. -The printed output to standard output of the program is parsed as YAML to get the grade and optional comments. +Provides a multiline textarea for the answer. The answered text is sent to the +standard input of an external program for grading. The printed output to +standard output of the program is parsed as YAML to get the grade and optional +comments. ```yaml - type: textarea @@ -185,7 +206,6 @@ comments: Almost there It can also just print the grade as a single number. - ### information, warning, alert and success These are not really questions, but just provides information for the student. @@ -201,8 +221,8 @@ Grading these type of "questions" yields always correct. ### generator -Questions can be generated by external programs instead of being defined directly. -This allows great flexibility, and allows each instance of a question to be always different. +This allows great flexibility, and allows each instance of a question to be +always different. ```yaml type: generator @@ -211,16 +231,18 @@ script: executable_program arg: 10,20 ``` -A generator is an external program that generates a question dynamically. +A generator is an external program that generates a question dynamically. In the example above, the program to be run is `executable_program`. The `arg` is sent to the standard input of the `executable_program`. -Questions should be printed to the stdout in YAML format, similarly to how they are defined above (but without the list dash). -The printed question is then parsed to a dictionary which is then used to update the question. -The `type` is redefined from generator to something else and the other fields are also updated. +Questions should be printed to the stdout in YAML format, similarly to how they +are defined above (but without the list dash). The printed question is then +parsed to a dictionary which is then used to update the question. The `type` +is redefined from generator to something else and the other fields are also +updated. -A generator can be any executable program (written in any language) that prints to the standard output. -Example of a generator written in python: +A generator can be any executable program (written in any language) that prints +to the standard output. Example of a generator written in python: ```python #!/usr/bin/env python3 @@ -234,7 +256,8 @@ a,b = (int(n) for n in arg.split(',')) q = fr''' type: checkbox text: | - 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. + 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. Os números foram gerados aleatoriamente no intervalo de {a} a {b}. options: @@ -254,14 +277,14 @@ print(q) A generator cannot generate another generator, only real questions are acceptable. -# Writing text +## Writing text The text in the questions is interpreted as markdown with support for LaTeX formulas. The best way to write text is to use indentation like this: ```yaml text: | - Yes. this is ok: If not indented, "Yes" would be a boolean + Yes. this is ok: If not indented, "Yes" would be a boolean and colon would be interpreted as a dictionary key. Images placed in the `public` subdirectory are accessible by diff --git a/aprendizations/learnapp.py b/aprendizations/learnapp.py index 76094de..47d3f4e 100644 --- a/aprendizations/learnapp.py +++ b/aprendizations/learnapp.py @@ -17,6 +17,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Set, DefaultDict import bcrypt import networkx as nx import sqlalchemy as sa +import sqlalchemy.orm as orm # this project from aprendizations.models import Student, Answer, Topic, StudentTopic @@ -112,7 +113,7 @@ class LearnApp(): self.courses = config['courses'] logger.info('Courses: %s', ', '.join(self.courses.keys())) for cid, course in self.courses.items(): - course.setdefault('title', '') # course title undefined + course.setdefault('title', cid) # course title undefined for goal in course['goals']: if goal not in self.deps.nodes(): msg = f'Goal "{goal}" from course "{cid}" does not exist' @@ -249,14 +250,14 @@ class LearnApp(): return False loop = asyncio.get_running_loop() - password = await loop.run_in_executor(None, - bcrypt.hashpw, - password.encode('utf-8'), - bcrypt.gensalt()) + pw = await loop.run_in_executor(None, + bcrypt.hashpw, + password.encode('utf-8'), + bcrypt.gensalt()) with self._db_session() as sess: user = sess.query(Student).get(uid) - user.password = password + user.password = pw logger.info('User "%s" changed password', uid) return True @@ -364,7 +365,7 @@ class LearnApp(): # ------------------------------------------------------------------------ # # ------------------------------------------------------------------------ - def _add_missing_topics(self, topics: List[str]) -> None: + def _add_missing_topics(self, topics: Iterable[str]) -> None: ''' Fill db table 'Topic' with topics from the graph, if new ''' @@ -386,7 +387,7 @@ class LearnApp(): 'Use "initdb-aprendizations" to create') engine = sa.create_engine(f'sqlite:///{database}', echo=False) - self.Session = sa.orm.sessionmaker(bind=engine) + self.Session = orm.sessionmaker(bind=engine) try: with self._db_session() as sess: count_students: int = sess.query(Student).count() @@ -501,6 +502,7 @@ class LearnApp(): qref = question.get('ref', str(i)) # ref or number if qref in localrefs: msg = f'Duplicate ref "{qref}" in "{topic["path"]}"' + logger.error(msg) raise LearnException(msg) localrefs.add(qref) diff --git a/aprendizations/main.py b/aprendizations/main.py index a04cc33..6020eaa 100644 --- a/aprendizations/main.py +++ b/aprendizations/main.py @@ -195,9 +195,10 @@ def main(): '--------------------------------------------------------------', sep='\n') sys.exit(1) - except LearnException: + except LearnException as exc: logging.critical('Failed to start backend') - sys.exit(1) + raise + # sys.exit(1) except Exception: logging.critical('Unknown error') # sys.exit(1) -- libgit2 0.21.2