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 | 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 | 7 | - paths manipulation in strings is unix only ('/something'). use os.path to create paths. |
| 5 | 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 | 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 | 4 | # pool = QuestionPool() |
| 12 | 5 | # pool.add_from_files(['file1.yaml', 'file1.yaml']) |
| ... | ... | @@ -18,6 +11,31 @@ import os.path |
| 18 | 11 | # test[0]['answer'] = 42 # insert answer |
| 19 | 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 | 40 | class QuestionsPool(dict): |
| 23 | 41 | '''This class contains base questions read from files, but which are |
| ... | ... | @@ -79,10 +97,15 @@ def create_question(q): |
| 79 | 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 | 103 | if q['type'] == 'generator': |
| 83 | 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 | 109 | types = { |
| 87 | 110 | 'radio' : QuestionRadio, |
| 88 | 111 | 'checkbox' : QuestionCheckbox, |
| ... | ... | @@ -97,25 +120,26 @@ def create_question(q): |
| 97 | 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 | 124 | try: |
| 102 | 125 | questiontype = types[q['type']] |
| 103 | 126 | except KeyError: |
| 104 | 127 | print('[ ERROR ] Unsupported question type "{0}" in "{1}:{2}".'.format(q['type'], q['filename'], q['ref'])) |
| 105 | 128 | questiontype = Question |
| 106 | 129 | |
| 107 | - # create question instance and return | |
| 130 | + # Create question instance and return | |
| 108 | 131 | try: |
| 109 | 132 | qinstance = questiontype(q) |
| 110 | 133 | except: |
| 111 | 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 | 137 | return qinstance |
| 114 | 138 | |
| 115 | 139 | |
| 116 | 140 | # --------------------------------------------------------------------------- |
| 117 | 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 | 143 | This function will return the yaml converted back to a dict.''' |
| 120 | 144 | |
| 121 | 145 | q['arg'] = q.get('arg', '') # send this string to stdin |
| ... | ... | @@ -126,11 +150,16 @@ def question_generator(q): |
| 126 | 150 | except FileNotFoundError: |
| 127 | 151 | print('[ ERROR ] Script "{0}" of question "{2}:{1}" not found'.format(script, q['ref'], q['filename'])) |
| 128 | 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 | 157 | try: |
| 131 | 158 | qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') |
| 132 | 159 | except subprocess.TimeoutExpired: |
| 133 | 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 | 164 | return yaml.load(qyaml) |
| 136 | 165 | |
| ... | ... | @@ -146,6 +175,11 @@ class Question(dict): |
| 146 | 175 | self['grade'] = 0.0 |
| 147 | 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 | 185 | class QuestionRadio(Question): |
| ... | ... | @@ -153,9 +187,9 @@ class QuestionRadio(Question): |
| 153 | 187 | type (str) |
| 154 | 188 | text (str) |
| 155 | 189 | options (list of strings) |
| 156 | - shuffle (bool, default True) | |
| 190 | + shuffle (bool, default=True) | |
| 157 | 191 | correct (list of floats) |
| 158 | - discount (bool, default True) | |
| 192 | + discount (bool, default=True) | |
| 159 | 193 | answer (None or an actual answer) |
| 160 | 194 | ''' |
| 161 | 195 | |
| ... | ... | @@ -164,45 +198,32 @@ class QuestionRadio(Question): |
| 164 | 198 | # create key/values as given in q |
| 165 | 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 | 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 | 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 | 229 | # can return negative values for wrong answers |
| ... | ... | @@ -237,38 +258,28 @@ class QuestionCheckbox(Question): |
| 237 | 258 | # create key/values as given in q |
| 238 | 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 | 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 | 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 | 285 | # can return negative values for wrong answers |
| ... | ... | @@ -279,7 +290,7 @@ class QuestionCheckbox(Question): |
| 279 | 290 | else: |
| 280 | 291 | # answered |
| 281 | 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 | 294 | self['grade'] = 0.0 |
| 284 | 295 | |
| 285 | 296 | else: |
| ... | ... | @@ -311,17 +322,18 @@ class QuestionText(Question): |
| 311 | 322 | # create key/values as given in q |
| 312 | 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 | 331 | # make sure its always a list of possible correct answers |
| 317 | 332 | if not isinstance(self['correct'], list): |
| 318 | 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 | 339 | # can return negative values for wrong answers |
| ... | ... | @@ -349,8 +361,12 @@ class QuestionTextRegex(Question): |
| 349 | 361 | def __init__(self, q): |
| 350 | 362 | # create key/values as given in q |
| 351 | 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 | 372 | # can return negative values for wrong answers |
| ... | ... | @@ -379,9 +395,14 @@ class QuestionTextArea(Question): |
| 379 | 395 | def __init__(self, q): |
| 380 | 396 | # create key/values as given in q |
| 381 | 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 | 408 | # can return negative values for wrong answers |
| ... | ... | @@ -394,29 +415,34 @@ class QuestionTextArea(Question): |
| 394 | 415 | |
| 395 | 416 | # The correction program expects data from stdin and prints the result to stdout. |
| 396 | 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 | 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 | 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 | 428 | raise e |
| 403 | 429 | |
| 404 | 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 | 432 | except subprocess.TimeoutExpired: |
| 407 | 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 | 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 | 437 | # In case the correction program returns a non float value, we assume an error |
| 413 | 438 | # has occurred (for instance, invalid input). We just assume its the student's |
| 414 | 439 | # fault and give him Zero. |
| 415 | 440 | try: |
| 416 | 441 | self['grade'] = float(value) |
| 417 | - except (ValueError): | |
| 442 | + except ValueError as e: | |
| 418 | 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 | 447 | return self['grade'] |
| 422 | 448 | |
| ... | ... | @@ -432,12 +458,15 @@ class QuestionInformation(Question): |
| 432 | 458 | def __init__(self, q): |
| 433 | 459 | # create key/values as given in q |
| 434 | 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 | 469 | # can return negative values for wrong answers |
| 440 | 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 | 472 | return self['grade'] |
| 443 | - | ... | ... |
serve.py
| ... | ... | @@ -69,11 +69,15 @@ class Root(object): |
| 69 | 69 | |
| 70 | 70 | # --- RESULTS ------------------------------------------------------------ |
| 71 | 71 | @cherrypy.expose |
| 72 | + @require() | |
| 72 | 73 | def results(self): |
| 73 | 74 | if self.testconf.get('practice', False): |
| 75 | + uid = cherrypy.session.get('userid') | |
| 76 | + name = cherrypy.session.get('name') | |
| 77 | + | |
| 74 | 78 | r = self.database.test_grades(self.testconf['ref']) |
| 75 | 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 | 81 | else: |
| 78 | 82 | raise cherrypy.HTTPRedirect('/') |
| 79 | 83 | ... | ... |
static/js/question_disabler.js
| ... | ... | @@ -3,7 +3,7 @@ $(document).ready(function() { |
| 3 | 3 | $("input.question_disabler").change(function () { |
| 4 | 4 | // Disables the body of the question. |
| 5 | 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 | 7 | // $(this).parent().parent().className = "panel panel-info"; |
| 8 | 8 | }); |
| 9 | 9 | $(function () { | ... | ... |
templates/results.html
| ... | ... | @@ -33,7 +33,8 @@ |
| 33 | 33 | </head> |
| 34 | 34 | <!-- ===================================================================== --> |
| 35 | 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 | 38 | <div class="container-fluid drop-shadow"> |
| 38 | 39 | <div class="navbar-header"> |
| 39 | 40 | <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar"> |
| ... | ... | @@ -42,30 +43,37 @@ |
| 42 | 43 | <span class="icon-bar"></span> |
| 43 | 44 | </button> |
| 44 | 45 | <a class="navbar-brand" href="#"> |
| 45 | - <!-- <img class="brand" alt="UEvora" src="/pomba-ue.svg"/> --> | |
| 46 | - UEvora | |
| 46 | + ${t['title']} | |
| 47 | 47 | </a> |
| 48 | 48 | </div> |
| 49 | 49 | |
| 50 | - <!-- <p class="navbar-text"> ${t['title']} </p> --> | |
| 51 | - | |
| 52 | 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 | 65 | </div> |
| 59 | 66 | </div> |
| 60 | 67 | </nav> |
| 61 | 68 | |
| 62 | 69 | |
| 70 | + | |
| 63 | 71 | <div class="container"> |
| 64 | 72 | <div class="panel panel-default drop-shadow"> |
| 65 | - <div class="panel-heading"> | |
| 73 | + <!-- <div class="panel-heading"> | |
| 66 | 74 | ${t['title']} |
| 67 | 75 | </div> |
| 68 | - <!-- <div class="panel-body"> --> | |
| 76 | + --><!-- <div class="panel-body"> --> | |
| 69 | 77 | % if not results: |
| 70 | 78 | <h4 class="text-center">Ainda não há resultados</h4> |
| 71 | 79 | |
| ... | ... | @@ -84,7 +92,7 @@ |
| 84 | 92 | from datetime import datetime |
| 85 | 93 | %> |
| 86 | 94 | % for r in results: |
| 87 | - <tr> | |
| 95 | + <tr class="default"> | |
| 88 | 96 | <td class="text-center"> |
| 89 | 97 | % if loop.index == 0: |
| 90 | 98 | <h4> |
| ... | ... | @@ -101,7 +109,7 @@ |
| 101 | 109 | <!-- <td>${r[0]}</td> --> <!-- numero --> |
| 102 | 110 | <td> |
| 103 | 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 | 113 | % else: |
| 106 | 114 | ${r[1]} |
| 107 | 115 | % endif | ... | ... |
templates/test.html
| 1 | 1 | <!DOCTYPE html> |
| 2 | 2 | <html> |
| 3 | 3 | <head> |
| 4 | - <meta charset="UTF-8"> | |
| 4 | + <meta charset="utf-8"> | |
| 5 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| 6 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 7 | 7 | <title> ${t['title']} </title> |
| ... | ... | @@ -67,22 +67,12 @@ |
| 67 | 67 | <span class="icon-bar"></span> |
| 68 | 68 | </button> |
| 69 | 69 | <a class="navbar-brand" href="#"> |
| 70 | - <!-- <img class="brand" alt="UEvora" src="/pomba-ue.svg"/> --> | |
| 71 | - UEvora | |
| 70 | + ${t['title']} | |
| 72 | 71 | </a> |
| 73 | 72 | </div> |
| 74 | 73 | |
| 75 | - <!-- <p class="navbar-text"> ${t['title']} </p> --> | |
| 76 | - | |
| 77 | 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 | 76 | <ul class="nav navbar-nav navbar-right"> |
| 87 | 77 | <li class="dropdown"> |
| 88 | 78 | <a class="dropdown-toggle" data-toggle="dropdown" href="#"> |
| ... | ... | @@ -90,6 +80,10 @@ |
| 90 | 80 | ${t['name']} (${t['number']}) <span class="caret"></span> |
| 91 | 81 | </a> |
| 92 | 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 | 87 | <li><a href="/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> |
| 94 | 88 | <!-- <li><a href="#">Change password</a></li> --> |
| 95 | 89 | </ul> |
| ... | ... | @@ -102,6 +96,7 @@ |
| 102 | 96 | |
| 103 | 97 | <div class="container"> |
| 104 | 98 | <div class="row"> |
| 99 | + | |
| 105 | 100 | <form action="/correct/" method="post" id="test"> |
| 106 | 101 | <%! |
| 107 | 102 | import markdown as md |
| ... | ... | @@ -135,35 +130,40 @@ |
| 135 | 130 | % for i,q in enumerate(questions): |
| 136 | 131 | <div class="ui-corner-all custom-corners"> |
| 137 | 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 | 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 | 139 | ${pretty(q['text'])} |
| 147 | - </div> | |
| 140 | + </p> | |
| 148 | 141 | </div> |
| 149 | 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 | 150 | </div> |
| 154 | 151 | |
| 155 | 152 | % else: |
| 156 | 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 | 162 | </div> |
| 161 | 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 | 165 | ${pretty(q['text'])} |
| 166 | - </p> | |
| 166 | + </div> | |
| 167 | 167 | |
| 168 | 168 | <fieldset data-role="controlgroup"> |
| 169 | 169 | % if q['type'] == 'radio': |
| ... | ... | @@ -226,6 +226,13 @@ |
| 226 | 226 | % endif # modal |
| 227 | 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 | 236 | % if t['practice'] and 'grade' in q: |
| 230 | 237 | % if q['grade'] > 0.99: |
| 231 | 238 | <div class="alert alert-success" role="alert"> |
| ... | ... | @@ -244,14 +251,9 @@ |
| 244 | 251 | </div> |
| 245 | 252 | % endif |
| 246 | 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 | 257 | % if t['debug'] or t['show_ref']: |
| 256 | 258 | <div class="panel-footer"> |
| 257 | 259 | ... | ... |