Commit acf932552121cf8434bc9303fdada33136b44e24

Authored by Miguel Barão
1 parent 506755ac
Exists in master and in 1 other branch dev

- cosmetic changes in the test and results template.

- simplified code in some questions.
- added more error checks.
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
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 2
3 [global] 3 [global]
4 -environment= 'production' 4 +; environment= 'production'
5 5
6 ; number of threads running 6 ; number of threads running
7 server.thread_pool= 10 7 server.thread_pool= 10
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 -  
@@ -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 - &nbsp;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&nbsp;
  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