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 | ... | ... |