Commit 34b58efe7ddde7d3dd1aec1b98410e6ec5c100dc
1 parent
9bace4f6
Exists in
master
and in
1 other branch
- replaced key 'stdin' by 'arg' in questions of type generator
- updated manual and demo
Showing
5 changed files
with
224 additions
and
181 deletions
Show diff stats
BUGS.md
... | ... | @@ -7,7 +7,6 @@ |
7 | 7 | - parece que é preciso criar à mão a pasta para as respostas (ans/...) depois apercebo-me que os caminhos no teste dizem respeito à directoria donde o teste é corrido... as respostas deveriam guardadas no directório dado. |
8 | 8 | - se database for mal configurada, é criada uma base de dados vazia e rebenta na autenticacao. |
9 | 9 | - testar regex na definicao das perguntas. como se faz rawstring em yaml? singlequote? problemas de backslash??? sim... necessário fazer \\ em varios casos, mas não é claro! e.g. \n é convertido em espaço mas \w é convertido em \\ e w. |
10 | -- testar envio de parametros para stdin para perguntas tipo generator. | |
11 | 10 | |
12 | 11 | # TODO |
13 | 12 | |
... | ... | @@ -26,6 +25,7 @@ |
26 | 25 | |
27 | 26 | # FIXED |
28 | 27 | |
28 | +- testar envio de parametros para stdin para perguntas tipo generator. | |
29 | 29 | - mathjax e jquery no login |
30 | 30 | - mostrar erro quando nao consegue importar questions files |
31 | 31 | - pacotes exactos usados para instalar. | ... | ... |
MANUAL.md
... | ... | @@ -2,29 +2,29 @@ |
2 | 2 | |
3 | 3 | ### Requirements and instalation |
4 | 4 | |
5 | -Install: | |
6 | - | |
7 | -- python3.4 | |
8 | -- cherrypy3 | |
9 | -- mako | |
10 | -- yaml | |
11 | -- markdown | |
5 | +Install python 3.4 and the following packages from pip: | |
12 | 6 | |
7 | +- CherryPy (3.7.0) | |
8 | +- Mako (1.0.1) | |
9 | +- Markdown (2.6.2) | |
10 | +- PyYAML (3.11) | |
13 | 11 | |
14 | 12 | Before using the program you need to |
15 | 13 | |
16 | 14 | 1. Create the students database |
17 | 15 | 1. Create questions |
18 | 16 | 1. Create a test |
19 | -1. Configure the server (the default may be enough) | |
17 | +1. Configure the server (the default should be enough) | |
20 | 18 | |
21 | 19 | ### Create students database |
22 | 20 | |
23 | 21 | We need a sqlite3 database to store students, passwords, test results, and questions results, etc. |
24 | 22 | |
25 | -The database can be initialized from a list of students in CSV format using the script | |
23 | +The database can be initialized from a list of students in CSV format by running in the terminal | |
26 | 24 | |
27 | - $ ./initdb_from_csv.py list_of_students.csv | |
25 | +```sh | |
26 | +./initdb_from_csv.py list_of_students.csv | |
27 | +``` | |
28 | 28 | |
29 | 29 | This script will create a new sqlite3 database with the correct tables and insert the students with empty passwords. |
30 | 30 | It also adds a special user number 0. This is the administrator user (Professor). |
... | ... | @@ -36,24 +36,26 @@ The passwords will be defined on the first login. |
36 | 36 | Questions are defined in `yaml` files and can reside anywhere in the filesystem. |
37 | 37 | Each file contains a list of questions, where each question is a dictionary. Example |
38 | 38 | |
39 | - - | |
40 | - ref: question-1 | |
41 | - type: radio | |
42 | - text: Select the correct option | |
43 | - options: | |
44 | - - correct | |
45 | - - wrong | |
46 | - | |
47 | - - | |
48 | - ref: question-2 | |
49 | - type: checkbox | |
50 | - text: Which ones are correct? | |
51 | - options: | |
52 | - - correct | |
53 | - - correct | |
54 | - - wrong | |
55 | - correct: [1, 1, -1] | |
56 | - hint: There are two correct answers! | |
39 | +```yaml | |
40 | +- | |
41 | + ref: question-1 | |
42 | + type: radio | |
43 | + text: Select the correct option | |
44 | + options: | |
45 | + - correct | |
46 | + - wrong | |
47 | + | |
48 | +- | |
49 | + ref: question-2 | |
50 | + type: checkbox | |
51 | + text: Which ones are correct? | |
52 | + options: | |
53 | + - correct | |
54 | + - correct | |
55 | + - wrong | |
56 | + correct: [1, 1, -1] | |
57 | + hint: There are two correct answers! | |
58 | +``` | |
57 | 59 | |
58 | 60 | There are several kinds of questions: |
59 | 61 | |
... | ... | @@ -72,84 +74,90 @@ Detailed information on each question type is described later on. |
72 | 74 | |
73 | 75 | A test is a file in `yaml` format that can reside anywhere on the filesystem. It has the following structure: |
74 | 76 | |
75 | - ref: this-is-a-key | |
76 | - title: Titulo do teste | |
77 | - database: db/mystudents.db | |
78 | - | |
79 | - # Will save the entire test of each student in JSON format. | |
80 | - # If tests are to be saved, we must specify the directory. | |
81 | - # The directory is created if it doesn't exist already. | |
82 | - # The name of the JSON files will include the student number, test | |
83 | - # reference key, date and time. | |
84 | - save_answers: True | |
85 | - answers_dir: ans/asc1_test4 | |
86 | - | |
87 | - # Some questions can contain hints, embedded videos, etc | |
88 | - show_hints: True | |
89 | - | |
90 | - # Each question has some number of points. Show them normalized to 0-20. | |
91 | - show_points: True | |
92 | - | |
93 | - # In train mode, the correction of the test is shown and the test can | |
94 | - # be repeated | |
95 | - practice_mode: True | |
96 | - | |
97 | - # Show the data structures obtained from the test and the questions | |
98 | - debug: False | |
99 | - | |
100 | - # Show the file and ref field of each question | |
101 | - show_ref: True | |
102 | - | |
103 | - # ------------------------------------------------------------------------- | |
104 | - # This are the questions database to be imported. | |
105 | - files: | |
106 | - - questions/file1.yaml | |
107 | - - questions/file2.yaml | |
108 | - - questions/file3.yaml | |
109 | - # ------------------------------------------------------------------------- | |
110 | - # This is the actual test configuration. Selection of questions and points | |
111 | - # It'a defined as a list of questions. Each question can be a single | |
112 | - # question key or a list of keys from which one is chosen at random. | |
113 | - # Each question has a default value of 1.0 point, but it can be overridden. | |
114 | - # The points defined here do not need to be normalized (it's automatic). | |
115 | - questions: | |
116 | - - ref: | |
117 | - - first-question-1 # randomly choose one from these 3 questions | |
118 | - - first-question-2 | |
119 | - - first-question-3 | |
120 | - points: 0.5 | |
121 | - | |
122 | - - ref: second-question # one question, 1.0 point (unnormalized) | |
123 | - | |
124 | - - third-question # "ref:" not needed in simple cases | |
77 | +```yaml | |
78 | +ref: this-is-a-key | |
79 | +title: Titulo do teste | |
80 | +database: db/mystudents.db | |
81 | + | |
82 | +# Will save the entire test of each student in JSON format. | |
83 | +# If tests are to be saved, we must specify the directory. | |
84 | +# The directory is created if it doesn't exist already. | |
85 | +# The name of the JSON files will include the student number, test | |
86 | +# reference key, date and time. | |
87 | +save_answers: True | |
88 | +answers_dir: ans/asc1_test4 | |
89 | + | |
90 | +# Some questions can contain hints, embedded videos, etc | |
91 | +show_hints: True | |
92 | + | |
93 | +# Each question has some number of points. Show them normalized to 0-20. | |
94 | +show_points: True | |
95 | + | |
96 | +# In train mode, the correction of the test is shown and the test can | |
97 | +# be repeated | |
98 | +practice_mode: True | |
99 | + | |
100 | +# Show the data structures obtained from the test and the questions | |
101 | +debug: False | |
102 | + | |
103 | +# Show the file and ref field of each question | |
104 | +show_ref: True | |
105 | + | |
106 | +# ------------------------------------------------------------------------- | |
107 | +# This are the questions database to be imported. | |
108 | +files: | |
109 | + - questions/file1.yaml | |
110 | + - questions/file2.yaml | |
111 | + - questions/file3.yaml | |
112 | +# ------------------------------------------------------------------------- | |
113 | +# This is the actual test configuration. Selection of questions and points | |
114 | +# It'a defined as a list of questions. Each question can be a single | |
115 | +# question key or a list of keys from which one is chosen at random. | |
116 | +# Each question has a default value of 1.0 point, but it can be overridden. | |
117 | +# The points defined here do not need to be normalized (it's automatic). | |
118 | +questions: | |
119 | + - ref: | |
120 | + - first-question-1 # randomly choose one from these 3 questions | |
121 | + - first-question-2 | |
122 | + - first-question-3 | |
123 | + points: 0.5 | |
124 | + | |
125 | + - ref: second-question # one question, 1.0 point (unnormalized) | |
126 | + | |
127 | + - third-question # "ref:" not needed in simple cases | |
125 | 128 | |
126 | 129 | This following one is wrong: |
127 | 130 | |
128 | - - wrong-question # missing "ref:" key | |
129 | - points: 2 | |
131 | + - wrong-question # missing "ref:" key | |
132 | + points: 2 | |
133 | +``` | |
130 | 134 | |
131 | 135 | Some of the options have default values if they are omitted. The defaults are the following: |
132 | 136 | |
133 | - ref: filename.yaml | |
134 | - title: '' | |
135 | - save_answers: False | |
136 | - show_hints: False | |
137 | - show_points: False | |
138 | - practice_mode: False | |
139 | - show_ref: False | |
140 | - debug: False | |
141 | - points: 1.0 | |
142 | - | |
137 | +```yaml | |
138 | +ref: filename.yaml | |
139 | +title: '' | |
140 | +save_answers: False | |
141 | +show_hints: False | |
142 | +show_points: False | |
143 | +practice_mode: False | |
144 | +show_ref: False | |
145 | +debug: False | |
146 | +points: 1.0 | |
147 | +``` | |
143 | 148 | ### Running an existing test |
144 | 149 | |
145 | 150 | A test is a file in `yaml` format. Just run `serve.py` with the test to run as argument: |
146 | 151 | |
147 | - $ ./serve.py tests_dir/mytest.yaml | |
152 | +```sh | |
153 | +$ ./serve.py tests_dir/mytest.yaml | |
154 | +``` | |
148 | 155 | |
149 | 156 | Some defaults can be overriden with command line options. Example |
150 | 157 | |
151 | - $ ./serve.py mytest.yaml --debug --show_points --show_hints --practice_mode --save_answers | |
152 | - | |
158 | +```sh | |
159 | +$ ./serve.py mytest.yaml --debug --show_points --show_hints --practice_mode --save_answers | |
160 | +``` | |
153 | 161 | To terminate the test just do `^C` on the keyboard. |
154 | 162 | |
155 | 163 | ## Questions |
... | ... | @@ -160,29 +168,31 @@ Every question should have a `ref` and a `type`. The other keys depend on the ty |
160 | 168 | |
161 | 169 | Not a real question. Just text to be shown without expecting an answer. |
162 | 170 | |
163 | - - | |
164 | - ref: some-key | |
165 | - type: information | |
166 | - text: Tomorrow will rain. | |
167 | - | |
171 | +```yaml | |
172 | +- | |
173 | + ref: some-key | |
174 | + type: information | |
175 | + text: Tomorrow will rain. | |
176 | +``` | |
168 | 177 | Correcting an information will always be considered correct, but the grade will be zero because it has 0.0 points by default. |
169 | 178 | |
170 | 179 | ### Radio |
171 | 180 | |
172 | 181 | Only one option is correct. |
173 | 182 | |
174 | - - | |
175 | - ref: some-key | |
176 | - type: radio | |
177 | - text: The horse is white. # optional (default: '') | |
178 | - options: | |
179 | - - The horse is white | |
180 | - - The horse is not black | |
181 | - - The horse is black | |
182 | - correct: 0 # optional (default: 0). Index is 0-based. | |
183 | - shuffle: True # optional (default: True) | |
184 | - discount: True # optional (default: True) | |
185 | - | |
183 | +```yaml | |
184 | +- | |
185 | + ref: some-key | |
186 | + type: radio | |
187 | + text: The horse is white. # optional (default: '') | |
188 | + options: | |
189 | + - The horse is white | |
190 | + - The horse is not black | |
191 | + - The horse is black | |
192 | + correct: 0 # optional (default: 0). Index is 0-based. | |
193 | + shuffle: True # optional (default: True) | |
194 | + discount: True # optional (default: True) | |
195 | +``` | |
186 | 196 | The `correct` value can also be defined as a list of degrees of correctness between 0 (wrong) and 1 (correct), e.g. if answering "the horse is not black" should be considered half-right, then we should use `correct: [1, 0.5, 0]`. |
187 | 197 | |
188 | 198 | Wrong answers discount by default. If there are half-right answers, the discount values are calculated automatically. `discount: False` disables the discount calculation and the values are the ones defined in `correct`. |
... | ... | @@ -191,20 +201,22 @@ Wrong answers discount by default. If there are half-right answers, the discount |
191 | 201 | |
192 | 202 | There can be several options correct. Each option is like answering an independent question. |
193 | 203 | |
194 | - - | |
195 | - ref: some-key | |
196 | - type: checkbox | |
197 | - text: The horse is white. # optional (default: '') | |
198 | - options: | |
199 | - - The horse is white | |
200 | - - The horse is not black | |
201 | - - The horse is black | |
202 | - correct: [1,1,-1] # optional (default: [0,0,0]). | |
203 | - shuffle: True # optional (default: True) | |
204 | - discount: True # optional (default: True) | |
205 | - | |
204 | +```yaml | |
205 | +- | |
206 | + ref: some-key | |
207 | + type: checkbox | |
208 | + text: The horse is white. # optional (default: '') | |
209 | + options: | |
210 | + - The horse is white | |
211 | + - The horse is not black | |
212 | + - The horse is black | |
213 | + correct: [1,1,-1] # optional (default: [0,0,0]). | |
214 | + shuffle: True # optional (default: True) | |
215 | + discount: True # optional (default: True) | |
216 | +``` | |
206 | 217 | Wrong answers discount by default. The discount values are calculated automatically and are simply the symmetric of the correct value. |
207 | 218 | E.g. consider `correct: [1, 0.5, -1]`, then |
219 | + | |
208 | 220 | - if the first option is marked the value is 1, otherwise if it's unmarked the value is -1. |
209 | 221 | - if the second option is marked the value is 0.5, otherwise if it's unmarked the value is -0.5. |
210 | 222 | - if the third option is marked the value is -1, otherwise if it's unmarked the value is 1. (the student shouldn't have marked this one) |
... | ... | @@ -216,26 +228,29 @@ E.g. consider `correct: [1, 0.5, -1]`, then |
216 | 228 | The answer is a line of text. |
217 | 229 | The server will check if the answer exactly matches the correct one. |
218 | 230 | |
219 | - - | |
220 | - ref: some-key | |
221 | - type: text | |
222 | - text: What's your favorite color? # optional (default: '') | |
223 | - correct: white | |
224 | - | |
231 | +```yaml | |
232 | +- | |
233 | + ref: some-key | |
234 | + type: text | |
235 | + text: What's your favorite color? # optional (default: '') | |
236 | + correct: white | |
237 | +``` | |
225 | 238 | alternatively, we can give a list of acceptable answers |
226 | - | |
227 | - correct: ['white', 'blue', 'red'] | |
228 | - | |
239 | +```yaml | |
240 | + correct: ['white', 'blue', 'red'] | |
241 | +``` | |
229 | 242 | ### Regular expression |
230 | 243 | |
231 | 244 | The answer is a line of text. |
232 | 245 | The server will check if the answer matches a regular expression. |
233 | 246 | |
234 | - - | |
235 | - ref: some-key | |
236 | - type: text_regex | |
237 | - text: What's your favorite color? # optional (default: '') | |
238 | - correct: '[Ww]hite' | |
247 | +```yaml | |
248 | +- | |
249 | + ref: some-key | |
250 | + type: text_regex | |
251 | + text: What's your favorite color? # optional (default: '') | |
252 | + correct: '[Ww]hite' | |
253 | +``` | |
239 | 254 | |
240 | 255 | Careful: yaml does not support raw text. Some characters have to be escaped. |
241 | 256 | |
... | ... | @@ -245,23 +260,27 @@ The answer is given in a textarea. The text (usually code) is sent to an externa |
245 | 260 | The external program should accept input from stdin, and print to stdout a single number in the interval 0.0 to 1.0 indicating the level of correctness. |
246 | 261 | The server will try to convert the printed message to a float, a failure will give 0.0. |
247 | 262 | |
248 | - - | |
249 | - ref: some-key | |
250 | - type: textarea | |
251 | - text: write an expression to add x and y. # optional (default: '') | |
252 | - correct: path/to/myscript | |
263 | +```yaml | |
264 | +- | |
265 | + ref: some-key | |
266 | + type: textarea | |
267 | + text: write an expression to add x and y. # optional (default: '') | |
268 | + correct: path/to/myscript | |
269 | +``` | |
253 | 270 | |
254 | 271 | An example of a script in python that validades an answer is |
255 | 272 | |
256 | - #!/usr/bin/env python3.4 | |
273 | +```python | |
274 | +#!/usr/bin/env python3.4 | |
257 | 275 | |
258 | - import sys | |
259 | - s = sys.stdin.read() | |
260 | - if s == 'Alibaba': | |
261 | - print(1.0) | |
262 | - else: | |
263 | - print(0.0) | |
264 | - exit(0) | |
276 | +import sys | |
277 | +s = sys.stdin.read() | |
278 | +if s == 'Alibaba': | |
279 | + print(1.0) | |
280 | +else: | |
281 | + print(0.0) | |
282 | +exit(0) | |
283 | +``` | |
265 | 284 | |
266 | 285 | but any script language or executable program can be used for this purpose. |
267 | 286 | |
... | ... | @@ -270,25 +289,38 @@ but any script language or executable program can be used for this purpose. |
270 | 289 | |
271 | 290 | A generator question will run an external program that is expected to print a question in yaml format to stdout. After running the generator, the question can be any of the other types (but not another generator!). |
272 | 291 | |
273 | - - | |
274 | - ref: some-key | |
275 | - type: generator | |
276 | - script: path/to/generator_script | |
292 | +```yaml | |
293 | +- | |
294 | + ref: some-key | |
295 | + type: generator | |
296 | + script: path/to/generator_script | |
297 | + # arg: "optional string passed on to stdin of the script" | |
298 | +``` | |
277 | 299 | |
278 | 300 | An example of a question generator is the following |
279 | 301 | |
280 | - #!/usr/bin/env python3.4 | |
281 | - from random import randint | |
282 | - | |
283 | - x = randint(10,20) | |
284 | - y = randint(10,20) | |
285 | - s = ''' | |
286 | - ref: addition | |
287 | - type: text | |
288 | - text: How much is {0} plus {1}? | |
289 | - correct: {2} | |
290 | - '''.format(x, y, x + y) | |
291 | - print(s) | |
302 | +```python | |
303 | +#!/usr/bin/env python3.4 | |
304 | +from random import randint | |
305 | +import sys | |
306 | + | |
307 | +# read arguments from stdin and convert to integers | |
308 | +arg = sys.stdin.read() | |
309 | +a,b = (int(n) for n in arg.split(',')) | |
310 | + | |
311 | +# generate question | |
312 | +x = randint(a, b) | |
313 | +y = randint(a, b) | |
314 | +s = ''' | |
315 | +ref: addition | |
316 | +type: text | |
317 | +text: How much is {0} plus {1}? | |
318 | +correct: {2} | |
319 | +'''.format(x, y, x + y) | |
320 | + | |
321 | +# send question to stdout | |
322 | +print(s) | |
323 | +``` | |
292 | 324 | |
293 | 325 | ## Writing good looking questions |
294 | 326 | |
... | ... | @@ -296,13 +328,16 @@ The text of the questions (and options in radio and checkbox type questios) is p |
296 | 328 | |
297 | 329 | A good way to define multiple lines of text in the questions is to use the bar |. Yaml will use all the text that is indented to the right of that column. Example |
298 | 330 | |
299 | - text: | | |
300 | - Text is parsed as __markdown__. We can include equations $\sqrt{\pi}$ like in LaTeX | |
301 | - and pretty code in several languages | |
331 | +```yaml | |
332 | + text: | | |
333 | + Text is parsed as __markdown__. We can include equations $\sqrt{\pi}$ like in LaTeX | |
334 | + and pretty code in several languages | |
335 | + | |
336 | + ```.C | |
337 | + int main(){ | |
338 | + return 0; | |
339 | + } | |
340 | + ``` | |
341 | + # this line stops the text because it is not indented | |
342 | +``` | |
302 | 343 | |
303 | - ```.C | |
304 | - int main(){ | |
305 | - return 0; | |
306 | - } | |
307 | - ``` | |
308 | - # this line does stops the text because it is not indented | ... | ... |
demo/generate-question.py
1 | 1 | #!/usr/bin/env python3.4 |
2 | 2 | |
3 | 3 | from random import randint |
4 | +import sys | |
4 | 5 | |
6 | +arg = sys.stdin.read() # read arguments | |
7 | + | |
8 | +a,b = (int(n) for n in arg.split(',')) | |
5 | 9 | |
6 | 10 | q = ''' |
7 | 11 | type: checkbox |
8 | -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. | |
12 | +text: | | |
13 | + 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. | |
14 | + | |
15 | + Os números foram gerados aleatoriamente no intervalo de {0} a {1}. | |
9 | 16 | options: |
10 | -''' | |
17 | +'''.format(a,b) | |
11 | 18 | |
12 | 19 | correct = [] |
13 | 20 | for i in range(5): |
14 | - x = randint(11,120) | |
15 | - y = randint(11,120) | |
21 | + x = randint(a, b) | |
22 | + y = randint(a, b) | |
16 | 23 | q += '- "`{} + {}`"\n'.format(x, y) |
17 | 24 | correct.append(1 if x + y > 127 else -1) |
18 | 25 | ... | ... |
demo/questions.yaml
... | ... | @@ -51,6 +51,7 @@ |
51 | 51 | ref: question-whatever |
52 | 52 | type: generator |
53 | 53 | script: demo/generate-question.py |
54 | + arg: "11,120" | |
54 | 55 | # the script should print a question in yaml format like the ones above. |
55 | 56 | # Print only the dictionary, not the list (hiffen). |
56 | 57 | # --------------------------------------------------------------------------- | ... | ... |
questions.py
... | ... | @@ -114,7 +114,7 @@ def question_generator(q): |
114 | 114 | '''Run an external script that will generate a question in yaml format. |
115 | 115 | This function will return the yaml converted back to a dict.''' |
116 | 116 | |
117 | - q['stdin'] = q.get('stdin', '') | |
117 | + q['arg'] = q.get('arg', '') # send this string to stdin | |
118 | 118 | |
119 | 119 | try: |
120 | 120 | p = subprocess.Popen([q['script']], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) |
... | ... | @@ -122,7 +122,7 @@ def question_generator(q): |
122 | 122 | print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(q['script'], q['ref'], q['filename'])) |
123 | 123 | |
124 | 124 | try: |
125 | - qyaml = p.communicate(input=q['stdin'].encode('utf-8'), timeout=5)[0].decode('utf-8') | |
125 | + qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') | |
126 | 126 | except subprocess.TimeoutExpired: |
127 | 127 | p.kill() |
128 | 128 | ... | ... |