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 |