Commit acf932552121cf8434bc9303fdada33136b44e24
1 parent
506755ac
Exists in
master
and in
1 other branch
- cosmetic changes in the test and results template.
- simplified code in some questions. - added more error checks.
Showing
7 changed files
with
197 additions
and
151 deletions
Show diff stats
BUGS.md
1 | 1 | ||
2 | # BUGS | 2 | # BUGS |
3 | 3 | ||
4 | +- mesmo aluno pode entrar várias vezes em simultaneo... | ||
5 | +- ordenar lista de alunos pelos online/offline, e depois pelo numero. | ||
6 | +- qd scripts não são executáveis rebenta. Testar isso e dar uma mensagem de erro. | ||
4 | - paths manipulation in strings is unix only ('/something'). use os.path to create paths. | 7 | - paths manipulation in strings is unix only ('/something'). use os.path to create paths. |
5 | - alunos vêm nota final arredondada às decimas, mas é apenas um arredondamento visual. Pode acontecer o aluno chumbar, mas ver uma nota positiva (e.g. 9.46 mostra 9.5 e presume que esta aprovado). Mostrar 3 casas? | 8 | - alunos vêm nota final arredondada às decimas, mas é apenas um arredondamento visual. Pode acontecer o aluno chumbar, mas ver uma nota positiva (e.g. 9.46 mostra 9.5 e presume que esta aprovado). Mostrar 3 casas? |
6 | - alunos podem entrar duas vezes em simultaneo. impedir, e permitir ao docente fazer kick-out | 9 | - alunos podem entrar duas vezes em simultaneo. impedir, e permitir ao docente fazer kick-out |
config/server.conf
questions.py
1 | 1 | ||
2 | -import yaml | ||
3 | -import random | ||
4 | -import re | ||
5 | -import subprocess | ||
6 | -import sys | ||
7 | -import os.path | ||
8 | - | ||
9 | -# Example usage: | 2 | +# Example: |
10 | # | 3 | # |
11 | # pool = QuestionPool() | 4 | # pool = QuestionPool() |
12 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) | 5 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) |
@@ -18,6 +11,31 @@ import os.path | @@ -18,6 +11,31 @@ import os.path | ||
18 | # test[0]['answer'] = 42 # insert answer | 11 | # test[0]['answer'] = 42 # insert answer |
19 | # grade = test[0].correct() # correct answer | 12 | # grade = test[0].correct() # correct answer |
20 | 13 | ||
14 | + | ||
15 | +# Functions: | ||
16 | +# create_question(q) | ||
17 | +# q - dictionary with question in yaml format | ||
18 | +# returns - question instance with the correct class | ||
19 | + | ||
20 | + | ||
21 | +# An instance of an actual question is a Question object: | ||
22 | +# | ||
23 | +# Question - base class inherited by other classes | ||
24 | +# QuestionRadio - single choice from a list of options | ||
25 | +# QuestionCheckbox - multiple choice, equivalent to multiple true/false | ||
26 | +# QuestionText - line of text compared to a list of acceptable answers | ||
27 | +# QuestionTextRegex - line of text matched against a regular expression | ||
28 | +# QuestionTextArea - corrected by an external program | ||
29 | +# QuestionInformation - not a question, just a box with content | ||
30 | + | ||
31 | +import yaml | ||
32 | +import random | ||
33 | +import re | ||
34 | +import subprocess | ||
35 | +import sys | ||
36 | +import os.path | ||
37 | + | ||
38 | + | ||
21 | # =========================================================================== | 39 | # =========================================================================== |
22 | class QuestionsPool(dict): | 40 | class QuestionsPool(dict): |
23 | '''This class contains base questions read from files, but which are | 41 | '''This class contains base questions read from files, but which are |
@@ -79,10 +97,15 @@ def create_question(q): | @@ -79,10 +97,15 @@ def create_question(q): | ||
79 | The remaing keys depend on the type of question. | 97 | The remaing keys depend on the type of question. |
80 | ''' | 98 | ''' |
81 | 99 | ||
100 | + # If `q` is of a question generator type, an external program will be run | ||
101 | + # and expected to print a valid question in yaml format to stdout. This | ||
102 | + # output is then converted to a dictionary and `q` becomes that dict. | ||
82 | if q['type'] == 'generator': | 103 | if q['type'] == 'generator': |
83 | q.update(question_generator(q)) | 104 | q.update(question_generator(q)) |
84 | - # at this point the generator question was replaced by an actual question | 105 | + # At this point the generator question was replaced by an actual question. |
85 | 106 | ||
107 | + # Depending on the type of question, a different question class is | ||
108 | + # instantiated. All these classes derive from the base class `Question`. | ||
86 | types = { | 109 | types = { |
87 | 'radio' : QuestionRadio, | 110 | 'radio' : QuestionRadio, |
88 | 'checkbox' : QuestionCheckbox, | 111 | 'checkbox' : QuestionCheckbox, |
@@ -97,25 +120,26 @@ def create_question(q): | @@ -97,25 +120,26 @@ def create_question(q): | ||
97 | # '' : QuestionInformation, # default | 120 | # '' : QuestionInformation, # default |
98 | } | 121 | } |
99 | 122 | ||
100 | - # create instance of given type | 123 | + # Get the correct question class for the declared question type |
101 | try: | 124 | try: |
102 | questiontype = types[q['type']] | 125 | questiontype = types[q['type']] |
103 | except KeyError: | 126 | except KeyError: |
104 | print('[ ERROR ] Unsupported question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) | 127 | print('[ ERROR ] Unsupported question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) |
105 | questiontype = Question | 128 | questiontype = Question |
106 | 129 | ||
107 | - # create question instance and return | 130 | + # Create question instance and return |
108 | try: | 131 | try: |
109 | qinstance = questiontype(q) | 132 | qinstance = questiontype(q) |
110 | except: | 133 | except: |
111 | print('[ ERROR ] Creating question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) | 134 | print('[ ERROR ] Creating question "{0}" from file "{1}".'.format(q['ref'], q['filename'])) |
135 | + # FIXME if here, then no qinstance is defined to return... | ||
112 | 136 | ||
113 | return qinstance | 137 | return qinstance |
114 | 138 | ||
115 | 139 | ||
116 | # --------------------------------------------------------------------------- | 140 | # --------------------------------------------------------------------------- |
117 | def question_generator(q): | 141 | def question_generator(q): |
118 | - '''Run an external script that will generate a question in yaml format. | 142 | + '''Run an external program that will generate a question in yaml format. |
119 | This function will return the yaml converted back to a dict.''' | 143 | This function will return the yaml converted back to a dict.''' |
120 | 144 | ||
121 | q['arg'] = q.get('arg', '') # send this string to stdin | 145 | q['arg'] = q.get('arg', '') # send this string to stdin |
@@ -126,11 +150,16 @@ def question_generator(q): | @@ -126,11 +150,16 @@ def question_generator(q): | ||
126 | except FileNotFoundError: | 150 | except FileNotFoundError: |
127 | print('[ ERROR ] Script "{0}" of question "{2}:{1}" not found'.format(script, q['ref'], q['filename'])) | 151 | print('[ ERROR ] Script "{0}" of question "{2}:{1}" not found'.format(script, q['ref'], q['filename'])) |
128 | sys.exit(1) | 152 | sys.exit(1) |
153 | + except PermissionError: | ||
154 | + print('[ ERROR ] Script "{0}" of question "{2}:{1}" has wrong permissions. Is it executable?'.format(script, q['ref'], q['filename'])) | ||
155 | + sys.exit(1) | ||
129 | 156 | ||
130 | try: | 157 | try: |
131 | qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') | 158 | qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') |
132 | except subprocess.TimeoutExpired: | 159 | except subprocess.TimeoutExpired: |
133 | p.kill() | 160 | p.kill() |
161 | + print('[ ERROR ] Timeout on script "{0}" of question "{2}:{1}"'.format(script, q['ref'], q['filename'])) | ||
162 | + sys.exit(1) | ||
134 | 163 | ||
135 | return yaml.load(qyaml) | 164 | return yaml.load(qyaml) |
136 | 165 | ||
@@ -146,6 +175,11 @@ class Question(dict): | @@ -146,6 +175,11 @@ class Question(dict): | ||
146 | self['grade'] = 0.0 | 175 | self['grade'] = 0.0 |
147 | return 0.0 | 176 | return 0.0 |
148 | 177 | ||
178 | + def set_defaults(self, d): | ||
179 | + 'Add k:v pairs from default dict d for nonexistent keys' | ||
180 | + for k,v in d.items(): | ||
181 | + self.setdefault(k, v) | ||
182 | + | ||
149 | 183 | ||
150 | # =========================================================================== | 184 | # =========================================================================== |
151 | class QuestionRadio(Question): | 185 | class QuestionRadio(Question): |
@@ -153,9 +187,9 @@ class QuestionRadio(Question): | @@ -153,9 +187,9 @@ class QuestionRadio(Question): | ||
153 | type (str) | 187 | type (str) |
154 | text (str) | 188 | text (str) |
155 | options (list of strings) | 189 | options (list of strings) |
156 | - shuffle (bool, default True) | 190 | + shuffle (bool, default=True) |
157 | correct (list of floats) | 191 | correct (list of floats) |
158 | - discount (bool, default True) | 192 | + discount (bool, default=True) |
159 | answer (None or an actual answer) | 193 | answer (None or an actual answer) |
160 | ''' | 194 | ''' |
161 | 195 | ||
@@ -164,45 +198,32 @@ class QuestionRadio(Question): | @@ -164,45 +198,32 @@ class QuestionRadio(Question): | ||
164 | # create key/values as given in q | 198 | # create key/values as given in q |
165 | super().__init__(q) | 199 | super().__init__(q) |
166 | 200 | ||
167 | - self['text'] = self.get('text', '') | 201 | + # set defaults if missing |
202 | + self.set_defaults({ | ||
203 | + 'text': '', | ||
204 | + 'correct': 0, | ||
205 | + 'shuffle': True, | ||
206 | + 'discount': True, | ||
207 | + 'answer': None, | ||
208 | + }) | ||
168 | 209 | ||
169 | - # generate an order for the options, e.g. [0,1,2,3,4] | ||
170 | n = len(self['options']) | 210 | n = len(self['options']) |
171 | - perm = list(range(n)) | ||
172 | - | ||
173 | - # shuffle the order, e.g. [2,1,4,0,3] | ||
174 | - if self.get('shuffle', True): | ||
175 | - self['shuffle'] = True | ||
176 | - random.shuffle(perm) | ||
177 | - else: | ||
178 | - self['shuffle'] = False | ||
179 | - | ||
180 | - # sort options in the given order | ||
181 | - options = [None] * n # will contain list with shuffled options | ||
182 | - for i, v in enumerate(self['options']): | ||
183 | - options[perm[i]] = str(v) # always convert to string | ||
184 | - self['options'] = options | ||
185 | 211 | ||
186 | - # default correct option is the first one | ||
187 | - if 'correct' not in self: | ||
188 | - self['correct'] = 0 | ||
189 | - | ||
190 | - # correct can be either an integer with the correct option | ||
191 | - # or a list of degrees of correction 0..1 | ||
192 | - # always convert to list, e.g. [0,0,1,0,0] | 212 | + # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] |
213 | + # correctness levels from 0.0 to 1.0 (no discount here!) | ||
193 | if isinstance(self['correct'], int): | 214 | if isinstance(self['correct'], int): |
194 | - correct = [0.0] * n | ||
195 | - correct[self['correct']] = 1.0 | ||
196 | - self['correct'] = correct | 215 | + self['correct'] = [1.0 if x==self['correct'] else 0.0 for x in range(n)] |
197 | 216 | ||
198 | - # sort correct in the given order | ||
199 | - correct = [None] * n | ||
200 | - for i, v in enumerate(self['correct']): | ||
201 | - correct[perm[i]] = float(v) | ||
202 | - self['correct'] = correct | ||
203 | - self['discount'] = bool(self.get('discount', True)) | ||
204 | - self['answer'] = None | 217 | + if len(self['correct']) != n: |
218 | + print('[ ERROR ] Options and correct mismatch in "{1}", file "{0}".'.format(self['filename'], self['ref'])) | ||
205 | 219 | ||
220 | + # generate random permutation, e.g. [2,1,4,0,3] | ||
221 | + # and apply to `options` and `correct` | ||
222 | + if self['shuffle']: | ||
223 | + perm = list(range(n)) | ||
224 | + random.shuffle(perm) | ||
225 | + self['options'] = [ str(self['options'][i]) for i in perm ] | ||
226 | + self['correct'] = [ float(self['correct'][i]) for i in perm ] | ||
206 | 227 | ||
207 | #------------------------------------------------------------------------ | 228 | #------------------------------------------------------------------------ |
208 | # can return negative values for wrong answers | 229 | # can return negative values for wrong answers |
@@ -237,38 +258,28 @@ class QuestionCheckbox(Question): | @@ -237,38 +258,28 @@ class QuestionCheckbox(Question): | ||
237 | # create key/values as given in q | 258 | # create key/values as given in q |
238 | super().__init__(q) | 259 | super().__init__(q) |
239 | 260 | ||
240 | - self['text'] = self.get('text', '') | ||
241 | - | ||
242 | - # generate an order for the options, e.g. [0,1,2,3,4] | ||
243 | n = len(self['options']) | 261 | n = len(self['options']) |
244 | - perm = list(range(n)) | ||
245 | 262 | ||
246 | - # shuffle the order, e.g. [2,1,4,0,3] | ||
247 | - if self.get('shuffle', True): | ||
248 | - self['shuffle'] = True | 263 | + # set defaults if missing |
264 | + self.set_defaults({ | ||
265 | + 'text': '', | ||
266 | + 'correct': [0.0] * n, # useful for questionaries | ||
267 | + 'shuffle': True, | ||
268 | + 'discount': True, | ||
269 | + 'answer': None, | ||
270 | + }) | ||
271 | + | ||
272 | + if len(self['correct']) != n: | ||
273 | + print('[ ERROR ] Options and correct mismatch in "{1}", file "{0}".'.format(self['filename'], self['ref'])) | ||
274 | + | ||
275 | + # generate random permutation, e.g. [2,1,4,0,3] | ||
276 | + # and apply to `options` and `correct` | ||
277 | + if self['shuffle']: | ||
278 | + perm = list(range(n)) | ||
249 | random.shuffle(perm) | 279 | random.shuffle(perm) |
250 | - else: | ||
251 | - self['shuffle'] = False | ||
252 | - | ||
253 | - # sort options in the given order | ||
254 | - options = [None] * n # will contain list with shuffled options | ||
255 | - for i, v in enumerate(self['options']): | ||
256 | - options[perm[i]] = str(v) # always convert to string | ||
257 | - self['options'] = options | ||
258 | - | ||
259 | - # default is to give zero to all options [0,0,...,0] | ||
260 | - if 'correct' not in self: | ||
261 | - self['correct'] = [0.0] * n | 280 | + self['options'] = [ str(self['options'][i]) for i in perm ] |
281 | + self['correct'] = [ float(self['correct'][i]) for i in perm ] | ||
262 | 282 | ||
263 | - # sort correct in the given order | ||
264 | - correct = [None] * n | ||
265 | - for i, v in enumerate(self['correct']): | ||
266 | - correct[perm[i]] = float(v) | ||
267 | - self['correct'] = correct | ||
268 | - | ||
269 | - self['discount'] = bool(self.get('discount', True)) | ||
270 | - | ||
271 | - self['answer'] = None | ||
272 | 283 | ||
273 | #------------------------------------------------------------------------ | 284 | #------------------------------------------------------------------------ |
274 | # can return negative values for wrong answers | 285 | # can return negative values for wrong answers |
@@ -279,7 +290,7 @@ class QuestionCheckbox(Question): | @@ -279,7 +290,7 @@ class QuestionCheckbox(Question): | ||
279 | else: | 290 | else: |
280 | # answered | 291 | # answered |
281 | sum_abs = sum(abs(p) for p in self['correct']) | 292 | sum_abs = sum(abs(p) for p in self['correct']) |
282 | - if sum_abs < 1e-6: # in case correct: [0,0,0,0,0] | 293 | + if sum_abs < 1e-6: # case correct [0,...,0] avoid div-by-zero |
283 | self['grade'] = 0.0 | 294 | self['grade'] = 0.0 |
284 | 295 | ||
285 | else: | 296 | else: |
@@ -311,17 +322,18 @@ class QuestionText(Question): | @@ -311,17 +322,18 @@ class QuestionText(Question): | ||
311 | # create key/values as given in q | 322 | # create key/values as given in q |
312 | super().__init__(q) | 323 | super().__init__(q) |
313 | 324 | ||
314 | - self['text'] = self.get('text', '') | 325 | + self.set_defaults({ |
326 | + 'text': '', | ||
327 | + 'correct': [], | ||
328 | + 'answer': None, | ||
329 | + }) | ||
315 | 330 | ||
316 | # make sure its always a list of possible correct answers | 331 | # make sure its always a list of possible correct answers |
317 | if not isinstance(self['correct'], list): | 332 | if not isinstance(self['correct'], list): |
318 | self['correct'] = [self['correct']] | 333 | self['correct'] = [self['correct']] |
319 | 334 | ||
320 | - # make sure the elements of the list are strings | ||
321 | - for i, a in enumerate(self['correct']): | ||
322 | - self['correct'][i] = str(a) | ||
323 | - | ||
324 | - self['answer'] = None | 335 | + # make sure all elements of the list are strings |
336 | + self['correct'] = [str(a) for a in self['correct']] | ||
325 | 337 | ||
326 | #------------------------------------------------------------------------ | 338 | #------------------------------------------------------------------------ |
327 | # can return negative values for wrong answers | 339 | # can return negative values for wrong answers |
@@ -349,8 +361,12 @@ class QuestionTextRegex(Question): | @@ -349,8 +361,12 @@ class QuestionTextRegex(Question): | ||
349 | def __init__(self, q): | 361 | def __init__(self, q): |
350 | # create key/values as given in q | 362 | # create key/values as given in q |
351 | super().__init__(q) | 363 | super().__init__(q) |
352 | - self['text'] = self.get('text', '') | ||
353 | - self['answer'] = None | 364 | + |
365 | + self.set_defaults({ | ||
366 | + 'text': '', | ||
367 | + 'correct': '$.^', # will always return false | ||
368 | + 'answer': None, | ||
369 | + }) | ||
354 | 370 | ||
355 | #------------------------------------------------------------------------ | 371 | #------------------------------------------------------------------------ |
356 | # can return negative values for wrong answers | 372 | # can return negative values for wrong answers |
@@ -379,9 +395,14 @@ class QuestionTextArea(Question): | @@ -379,9 +395,14 @@ class QuestionTextArea(Question): | ||
379 | def __init__(self, q): | 395 | def __init__(self, q): |
380 | # create key/values as given in q | 396 | # create key/values as given in q |
381 | super().__init__(q) | 397 | super().__init__(q) |
382 | - self['text'] = self.get('text', '') | ||
383 | - self['answer'] = None | ||
384 | - self['lines'] = self.get('lines', 8) | 398 | + |
399 | + self.set_defaults({ | ||
400 | + 'text': '', | ||
401 | + 'answer': None, | ||
402 | + 'lines': 8, | ||
403 | + }) | ||
404 | + | ||
405 | + self['correct'] = os.path.abspath(os.path.normpath(os.path.join(self['path'], self['correct']))) | ||
385 | 406 | ||
386 | #------------------------------------------------------------------------ | 407 | #------------------------------------------------------------------------ |
387 | # can return negative values for wrong answers | 408 | # can return negative values for wrong answers |
@@ -394,29 +415,34 @@ class QuestionTextArea(Question): | @@ -394,29 +415,34 @@ class QuestionTextArea(Question): | ||
394 | 415 | ||
395 | # The correction program expects data from stdin and prints the result to stdout. | 416 | # The correction program expects data from stdin and prints the result to stdout. |
396 | # The result should be a string that can be parsed to a float. | 417 | # The result should be a string that can be parsed to a float. |
397 | - script = os.path.abspath(os.path.normpath(os.path.join(self['path'], self['correct']))) | ||
398 | try: | 418 | try: |
399 | - p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) | 419 | + p = subprocess.Popen([self['correct']], |
420 | + stdout=subprocess.PIPE, | ||
421 | + stdin=subprocess.PIPE, | ||
422 | + stderr=subprocess.STDOUT) | ||
400 | except FileNotFoundError as e: | 423 | except FileNotFoundError as e: |
401 | - print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(self['correct'], self['ref'], self['filename'])) | 424 | + print('[ ERROR ] Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(self['correct'], self['ref'], self['filename'])) |
425 | + raise e | ||
426 | + except PermissionError as e: | ||
427 | + print('[ ERROR ] Script "{0}" has wrong permissions. Is it executable?'.format(self['correct'])) | ||
402 | raise e | 428 | raise e |
403 | 429 | ||
404 | try: | 430 | try: |
405 | - value = p.communicate(input=self['answer'].encode('utf-8'), timeout=5)[0].decode('utf-8') # esta a dar erro! | 431 | + value = p.communicate(input=self['answer'].encode('utf-8'), timeout=5)[0].decode('utf-8') |
406 | except subprocess.TimeoutExpired: | 432 | except subprocess.TimeoutExpired: |
407 | p.kill() | 433 | p.kill() |
408 | - # p.communicate() # FIXME parece que este communicate obriga a ficar ate ao final do processo, mas nao consigo repetir o comportamento num script simples... | ||
409 | value = 0.0 # student gets a zero if timout occurs | 434 | value = 0.0 # student gets a zero if timout occurs |
410 | - printf(' * Timeout in correction script') | 435 | + print('[ WARNG ] Timeout in correction script "{0}"'.format(self['correct'])) |
411 | 436 | ||
412 | # In case the correction program returns a non float value, we assume an error | 437 | # In case the correction program returns a non float value, we assume an error |
413 | # has occurred (for instance, invalid input). We just assume its the student's | 438 | # has occurred (for instance, invalid input). We just assume its the student's |
414 | # fault and give him Zero. | 439 | # fault and give him Zero. |
415 | try: | 440 | try: |
416 | self['grade'] = float(value) | 441 | self['grade'] = float(value) |
417 | - except (ValueError): | 442 | + except ValueError as e: |
418 | self['grade'] = 0.0 | 443 | self['grade'] = 0.0 |
419 | - raise Exception('Correction of question "{0}" returned nonfloat "{1}".'.format(self['ref'], value)) | 444 | + print('[ ERROR ] Correction of question "{0}" returned nonfloat:\n{1}\n'.format(self['ref'], value)) |
445 | + raise e | ||
420 | 446 | ||
421 | return self['grade'] | 447 | return self['grade'] |
422 | 448 | ||
@@ -432,12 +458,15 @@ class QuestionInformation(Question): | @@ -432,12 +458,15 @@ class QuestionInformation(Question): | ||
432 | def __init__(self, q): | 458 | def __init__(self, q): |
433 | # create key/values as given in q | 459 | # create key/values as given in q |
434 | super().__init__(q) | 460 | super().__init__(q) |
435 | - self['text'] = self.get('text', '') | ||
436 | - self['points'] = 0.0 # always override the points | 461 | + |
462 | + self.set_defaults({ | ||
463 | + 'text': '', | ||
464 | + }) | ||
465 | + | ||
466 | + self['points'] = 0.0 # always override the default points of 1.0 | ||
437 | 467 | ||
438 | #------------------------------------------------------------------------ | 468 | #------------------------------------------------------------------------ |
439 | # can return negative values for wrong answers | 469 | # can return negative values for wrong answers |
440 | def correct(self): | 470 | def correct(self): |
441 | - self['grade'] = 1.0 # always "correct" but points should be zero! | 471 | + self['grade'] = 0.0 # always "correct" but points should be zero! |
442 | return self['grade'] | 472 | return self['grade'] |
443 | - |
serve.py
@@ -69,11 +69,15 @@ class Root(object): | @@ -69,11 +69,15 @@ class Root(object): | ||
69 | 69 | ||
70 | # --- RESULTS ------------------------------------------------------------ | 70 | # --- RESULTS ------------------------------------------------------------ |
71 | @cherrypy.expose | 71 | @cherrypy.expose |
72 | + @require() | ||
72 | def results(self): | 73 | def results(self): |
73 | if self.testconf.get('practice', False): | 74 | if self.testconf.get('practice', False): |
75 | + uid = cherrypy.session.get('userid') | ||
76 | + name = cherrypy.session.get('name') | ||
77 | + | ||
74 | r = self.database.test_grades(self.testconf['ref']) | 78 | r = self.database.test_grades(self.testconf['ref']) |
75 | template = self.templates.get_template('/results.html') | 79 | template = self.templates.get_template('/results.html') |
76 | - return template.render(t=self.testconf, results=r) | 80 | + return template.render(t=self.testconf, results=r, name=name, uid=uid) |
77 | else: | 81 | else: |
78 | raise cherrypy.HTTPRedirect('/') | 82 | raise cherrypy.HTTPRedirect('/') |
79 | 83 |
static/js/question_disabler.js
@@ -3,7 +3,7 @@ $(document).ready(function() { | @@ -3,7 +3,7 @@ $(document).ready(function() { | ||
3 | $("input.question_disabler").change(function () { | 3 | $("input.question_disabler").change(function () { |
4 | // Disables the body of the question. | 4 | // Disables the body of the question. |
5 | // The flip switch is on the bar, and is still accessible. | 5 | // The flip switch is on the bar, and is still accessible. |
6 | - $(this).parent().next().slideToggle("fast"); | 6 | + $(this).parent().parent().next().slideToggle("fast"); |
7 | // $(this).parent().parent().className = "panel panel-info"; | 7 | // $(this).parent().parent().className = "panel panel-info"; |
8 | }); | 8 | }); |
9 | $(function () { | 9 | $(function () { |
templates/results.html
@@ -33,7 +33,8 @@ | @@ -33,7 +33,8 @@ | ||
33 | </head> | 33 | </head> |
34 | <!-- ===================================================================== --> | 34 | <!-- ===================================================================== --> |
35 | <body> | 35 | <body> |
36 | -<nav class="navbar navbar-default navbar-fixed-top" role="navigation"> | 36 | + |
37 | + <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> | ||
37 | <div class="container-fluid drop-shadow"> | 38 | <div class="container-fluid drop-shadow"> |
38 | <div class="navbar-header"> | 39 | <div class="navbar-header"> |
39 | <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar"> | 40 | <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar"> |
@@ -42,30 +43,37 @@ | @@ -42,30 +43,37 @@ | ||
42 | <span class="icon-bar"></span> | 43 | <span class="icon-bar"></span> |
43 | </button> | 44 | </button> |
44 | <a class="navbar-brand" href="#"> | 45 | <a class="navbar-brand" href="#"> |
45 | - <!-- <img class="brand" alt="UEvora" src="/pomba-ue.svg"/> --> | ||
46 | - UEvora | 46 | + ${t['title']} |
47 | </a> | 47 | </a> |
48 | </div> | 48 | </div> |
49 | 49 | ||
50 | - <!-- <p class="navbar-text"> ${t['title']} </p> --> | ||
51 | - | ||
52 | <div class="collapse navbar-collapse" id="myNavbar"> | 50 | <div class="collapse navbar-collapse" id="myNavbar"> |
53 | - <ul class="nav navbar-nav"> | ||
54 | - <li><a href="/test">Teste</a></li> | ||
55 | - <li class="active"><a href="/resultados">Resultados</a></li> | ||
56 | - </ul> | ||
57 | 51 | ||
52 | + <ul class="nav navbar-nav navbar-right"> | ||
53 | + <li class="dropdown"> | ||
54 | + <a class="dropdown-toggle" data-toggle="dropdown" href="#"> | ||
55 | + <span class="glyphicon glyphicon-user" aria-hidden="true"></span> | ||
56 | + ${name} (${uid}) <span class="caret"></span> | ||
57 | + </a> | ||
58 | + <ul class="dropdown-menu"> | ||
59 | + <li><a href="/test">Teste</a></li> | ||
60 | + <li class="active"><a href="/results">Ver resultados</a></li> | ||
61 | + <li><a href="/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> | ||
62 | + </ul> | ||
63 | + </li> | ||
64 | + </ul> | ||
58 | </div> | 65 | </div> |
59 | </div> | 66 | </div> |
60 | </nav> | 67 | </nav> |
61 | 68 | ||
62 | 69 | ||
70 | + | ||
63 | <div class="container"> | 71 | <div class="container"> |
64 | <div class="panel panel-default drop-shadow"> | 72 | <div class="panel panel-default drop-shadow"> |
65 | - <div class="panel-heading"> | 73 | + <!-- <div class="panel-heading"> |
66 | ${t['title']} | 74 | ${t['title']} |
67 | </div> | 75 | </div> |
68 | - <!-- <div class="panel-body"> --> | 76 | + --><!-- <div class="panel-body"> --> |
69 | % if not results: | 77 | % if not results: |
70 | <h4 class="text-center">Ainda não há resultados</h4> | 78 | <h4 class="text-center">Ainda não há resultados</h4> |
71 | 79 | ||
@@ -84,7 +92,7 @@ | @@ -84,7 +92,7 @@ | ||
84 | from datetime import datetime | 92 | from datetime import datetime |
85 | %> | 93 | %> |
86 | % for r in results: | 94 | % for r in results: |
87 | - <tr> | 95 | + <tr class="default"> |
88 | <td class="text-center"> | 96 | <td class="text-center"> |
89 | % if loop.index == 0: | 97 | % if loop.index == 0: |
90 | <h4> | 98 | <h4> |
@@ -101,7 +109,7 @@ | @@ -101,7 +109,7 @@ | ||
101 | <!-- <td>${r[0]}</td> --> <!-- numero --> | 109 | <!-- <td>${r[0]}</td> --> <!-- numero --> |
102 | <td> | 110 | <td> |
103 | % if loop.index == 0: | 111 | % if loop.index == 0: |
104 | - <h4 class="text-uppercase"><img src="\crown.jpg" /> ${r[1]}</h4> | 112 | + <h4 class="text-uppercase"><img src="/crown.jpg" /> ${r[1]}</h4> |
105 | % else: | 113 | % else: |
106 | ${r[1]} | 114 | ${r[1]} |
107 | % endif | 115 | % endif |
templates/test.html
1 | <!DOCTYPE html> | 1 | <!DOCTYPE html> |
2 | <html> | 2 | <html> |
3 | <head> | 3 | <head> |
4 | - <meta charset="UTF-8"> | 4 | + <meta charset="utf-8"> |
5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
7 | <title> ${t['title']} </title> | 7 | <title> ${t['title']} </title> |
@@ -67,22 +67,12 @@ | @@ -67,22 +67,12 @@ | ||
67 | <span class="icon-bar"></span> | 67 | <span class="icon-bar"></span> |
68 | </button> | 68 | </button> |
69 | <a class="navbar-brand" href="#"> | 69 | <a class="navbar-brand" href="#"> |
70 | - <!-- <img class="brand" alt="UEvora" src="/pomba-ue.svg"/> --> | ||
71 | - UEvora | 70 | + ${t['title']} |
72 | </a> | 71 | </a> |
73 | </div> | 72 | </div> |
74 | 73 | ||
75 | - <!-- <p class="navbar-text"> ${t['title']} </p> --> | ||
76 | - | ||
77 | <div class="collapse navbar-collapse" id="myNavbar"> | 74 | <div class="collapse navbar-collapse" id="myNavbar"> |
78 | 75 | ||
79 | - % if t['practice']: | ||
80 | - <ul class="nav navbar-nav"> | ||
81 | - <li class="active"><a href="/test">Teste</a></li> | ||
82 | - <li><a href="/results">Resultados</a></li> | ||
83 | - </ul> | ||
84 | - % endif | ||
85 | - | ||
86 | <ul class="nav navbar-nav navbar-right"> | 76 | <ul class="nav navbar-nav navbar-right"> |
87 | <li class="dropdown"> | 77 | <li class="dropdown"> |
88 | <a class="dropdown-toggle" data-toggle="dropdown" href="#"> | 78 | <a class="dropdown-toggle" data-toggle="dropdown" href="#"> |
@@ -90,6 +80,10 @@ | @@ -90,6 +80,10 @@ | ||
90 | ${t['name']} (${t['number']}) <span class="caret"></span> | 80 | ${t['name']} (${t['number']}) <span class="caret"></span> |
91 | </a> | 81 | </a> |
92 | <ul class="dropdown-menu"> | 82 | <ul class="dropdown-menu"> |
83 | + <li class="active"><a href="/test">Teste</a></li> | ||
84 | + % if t['practice']: | ||
85 | + <li><a href="/results">Ver resultados</a></li> | ||
86 | + % endif | ||
93 | <li><a href="/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> | 87 | <li><a href="/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> |
94 | <!-- <li><a href="#">Change password</a></li> --> | 88 | <!-- <li><a href="#">Change password</a></li> --> |
95 | </ul> | 89 | </ul> |
@@ -102,6 +96,7 @@ | @@ -102,6 +96,7 @@ | ||
102 | 96 | ||
103 | <div class="container"> | 97 | <div class="container"> |
104 | <div class="row"> | 98 | <div class="row"> |
99 | + | ||
105 | <form action="/correct/" method="post" id="test"> | 100 | <form action="/correct/" method="post" id="test"> |
106 | <%! | 101 | <%! |
107 | import markdown as md | 102 | import markdown as md |
@@ -135,35 +130,40 @@ | @@ -135,35 +130,40 @@ | ||
135 | % for i,q in enumerate(questions): | 130 | % for i,q in enumerate(questions): |
136 | <div class="ui-corner-all custom-corners"> | 131 | <div class="ui-corner-all custom-corners"> |
137 | % if q['type'] == 'information': | 132 | % if q['type'] == 'information': |
138 | - <div class="panel panel-default drop-shadow"> | ||
139 | - <div class="panel-heading"> | 133 | + <div class="alert alert-warning drop-shadow" role="alert"> |
134 | + <h4> | ||
140 | <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> | 135 | <span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> |
141 | - Informação | ||
142 | - </div> | ||
143 | - <div class="panel-body"> | ||
144 | - <!-- <div class="well danger drop-shadow"> --> | ||
145 | - <h3><small>${i+1}.</small> ${q['title']}</h3> | 136 | + ${q['title']} |
137 | + </h4> | ||
138 | + <p> | ||
146 | ${pretty(q['text'])} | 139 | ${pretty(q['text'])} |
147 | - </div> | 140 | + </p> |
148 | </div> | 141 | </div> |
149 | % elif q['type'] == 'warning': | 142 | % elif q['type'] == 'warning': |
150 | - <div class="alert alert-warning drop-shadow" role="alert"> | ||
151 | - <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> | ||
152 | - ${q['text']} | 143 | + <div class="alert alert-danger drop-shadow" role="alert"> |
144 | + <h4> | ||
145 | + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> Atenção | ||
146 | + </h4> | ||
147 | + <p> | ||
148 | + ${pretty(q['text'])} | ||
149 | + </p> | ||
153 | </div> | 150 | </div> |
154 | 151 | ||
155 | % else: | 152 | % else: |
156 | <div class="panel panel-primary drop-shadow"> | 153 | <div class="panel panel-primary drop-shadow"> |
157 | - <div class="panel-heading"> | ||
158 | - <input type="checkbox" class="question_disabler" data-size="mini" name="answered-${q['ref']}" id="answered-${q['ref']}" checked=""> | ||
159 | - Classificar | 154 | + <div class="panel-heading clearfix"> |
155 | + <h4 class="panel-title pull-left"> | ||
156 | + ${i+1}. ${q['title']} | ||
157 | + </h4> | ||
158 | + <div class="pull-right"> | ||
159 | + Classificar | ||
160 | + <input type="checkbox" class="question_disabler" data-size="mini" name="answered-${q['ref']}" id="answered-${q['ref']}" checked=""> | ||
161 | + </div> | ||
160 | </div> | 162 | </div> |
161 | <div class="panel-body" id="example${i}"> | 163 | <div class="panel-body" id="example${i}"> |
162 | - <h3> <small>${i+1}.</small> ${q['title']}</h3> | ||
163 | - | ||
164 | - <p class="question"> | 164 | + <div class="question"> |
165 | ${pretty(q['text'])} | 165 | ${pretty(q['text'])} |
166 | - </p> | 166 | + </div> |
167 | 167 | ||
168 | <fieldset data-role="controlgroup"> | 168 | <fieldset data-role="controlgroup"> |
169 | % if q['type'] == 'radio': | 169 | % if q['type'] == 'radio': |
@@ -226,6 +226,13 @@ | @@ -226,6 +226,13 @@ | ||
226 | % endif # modal | 226 | % endif # modal |
227 | % endif # show_hints | 227 | % endif # show_hints |
228 | 228 | ||
229 | + % if t['show_points']: | ||
230 | + <p class="text-right"> | ||
231 | + <small>(Cotação: ${round(q['points'] / total_points * 20.0, 1)} pontos)</small> | ||
232 | + <p> | ||
233 | + % endif | ||
234 | + | ||
235 | + | ||
229 | % if t['practice'] and 'grade' in q: | 236 | % if t['practice'] and 'grade' in q: |
230 | % if q['grade'] > 0.99: | 237 | % if q['grade'] > 0.99: |
231 | <div class="alert alert-success" role="alert"> | 238 | <div class="alert alert-success" role="alert"> |
@@ -244,14 +251,9 @@ | @@ -244,14 +251,9 @@ | ||
244 | </div> | 251 | </div> |
245 | % endif | 252 | % endif |
246 | % endif | 253 | % endif |
254 | + </div> <!-- panel-body --> | ||
247 | 255 | ||
248 | - % if t['show_points']: | ||
249 | - <p class="text-right"> | ||
250 | - <small>(Cotação: ${round(q['points'] / total_points * 20.0, 1)} pontos)</small> | ||
251 | - <p> | ||
252 | - % endif | ||
253 | 256 | ||
254 | - </div> <!-- panel-body --> | ||
255 | % if t['debug'] or t['show_ref']: | 257 | % if t['debug'] or t['show_ref']: |
256 | <div class="panel-footer"> | 258 | <div class="panel-footer"> |
257 | 259 |