Commit fa02522c2e869d2b441571279fa5e2be47b2483f

Authored by Miguel Barão
1 parent 34b58efe
Exists in master and in 1 other branch dev

- corrections in the MANUAL.md

- corrections in the /config/server.conf
- fixed paths so that server can be run from other directories.
- fixed paths so that questions, tests and logs do not have to be in the server directory.
- check existence of some files, directories and database
- removed offensive comments
@@ -2,10 +2,10 @@ @@ -2,10 +2,10 @@
2 2
3 # BUGS 3 # BUGS
4 4
5 -!!! - questions type script, necessário dar um caminho exacto relativamete ao directorio do server em vez da pergunta. deveria ser possivel mover as perguntas de directorio sem rebentar os caminhos.  
6 - 5 +- check if script to generate questions exist before instantiation.
  6 +- paths manipulation in strings is unix only ('/something'). use os.path to create paths.
  7 +- fix ans directory. relative to what?? current dir?
7 - parece que é preciso criar à mão a pasta para as respostas (ans/...) depois apercebo-me que os caminhos no teste dizem respeito à directoria donde o teste é corrido... as respostas deveriam guardadas no directório dado. 8 - parece que é preciso criar à mão a pasta para as respostas (ans/...) depois apercebo-me que os caminhos no teste dizem respeito à directoria donde o teste é corrido... as respostas deveriam guardadas no directório dado.
8 -- se database for mal configurada, é criada uma base de dados vazia e rebenta na autenticacao.  
9 - testar regex na definicao das perguntas. como se faz rawstring em yaml? singlequote? problemas de backslash??? sim... necessário fazer \\ em varios casos, mas não é claro! e.g. \n é convertido em espaço mas \w é convertido em \\ e w. 9 - testar regex na definicao das perguntas. como se faz rawstring em yaml? singlequote? problemas de backslash??? sim... necessário fazer \\ em varios casos, mas não é claro! e.g. \n é convertido em espaço mas \w é convertido em \\ e w.
10 10
11 # TODO 11 # TODO
@@ -25,6 +25,10 @@ @@ -25,6 +25,10 @@
25 25
26 # FIXED 26 # FIXED
27 27
  28 +- se database for mal configurada, é criada uma base de dados vazia e rebenta na autenticacao.
  29 +- questions type script, necessário dar um caminho exacto relativamete ao directorio do server em vez da pergunta. deveria ser possivel mover as perguntas de directorio sem rebentar os caminhos.
  30 +- check that files exist in questions generator e correct textarea. add path in test.yaml
  31 +- scripts generator and correct should consider the questions path.
28 - testar envio de parametros para stdin para perguntas tipo generator. 32 - testar envio de parametros para stdin para perguntas tipo generator.
29 - mathjax e jquery no login 33 - mathjax e jquery no login
30 - mostrar erro quando nao consegue importar questions files 34 - mostrar erro quando nao consegue importar questions files
@@ -11,10 +11,27 @@ Install python 3.4 and the following packages from pip: @@ -11,10 +11,27 @@ Install python 3.4 and the following packages from pip:
11 11
12 Before using the program you need to 12 Before using the program you need to
13 13
14 -1. Create the students database  
15 -1. Create questions  
16 -1. Create a test  
17 -1. Configure the server (the default should be enough) 14 +1. Edit `config/server.conf` in the server directory and define
  15 + - Logging
  16 +
  17 + `log.error_file= '/Users/USERNAME/Library/Logs/Perguntations/errors.log'`
  18 +
  19 + `log.access_file= '/Users/USERNAME/Library/Logs/Perguntations/access.log'`
  20 +
  21 + You must create the directories if they do not exist already.
  22 +
  23 + Setting these locations to empty strings `''` disables logging.
  24 +
  25 + - Sessions
  26 + If `tools.sessions.storage_type='file'` sessions are saved on the file system in the location given in `tools.sessions.storage_path`. Restarting the server will maintain the sessions active.
  27 +
  28 + If `storage_type='ram'` (default) no files are stored but restaring the server will reset sessions.
  29 +
  30 + You should give enough time in the `tools.sessions.timeout` to complete an exam. The default is 240 minutes (4 hours).
  31 +
  32 +1. Create the students database (see below)
  33 +1. Create questions (see below)
  34 +1. Create a test (see below)
18 35
19 ### Create students database 36 ### Create students database
20 37
@@ -103,13 +120,17 @@ debug: False @@ -103,13 +120,17 @@ debug: False
103 # Show the file and ref field of each question 120 # Show the file and ref field of each question
104 show_ref: True 121 show_ref: True
105 122
106 -# -------------------------------------------------------------------------  
107 -# This are the questions database to be imported. 123 +# ----------------------------------------------------------------------------
  124 +# Location of the questions files (absolute path or relative to current dir)
  125 +path: questions
  126 +
  127 +# This are the questions files to be imported.
108 files: 128 files:
109 - - questions/file1.yaml  
110 - - questions/file2.yaml  
111 - - questions/file3.yaml  
112 -# ------------------------------------------------------------------------- 129 + - file1.yaml
  130 + - file2.yaml
  131 + - file3.yaml
  132 +
  133 +# ----------------------------------------------------------------------------
113 # This is the actual test configuration. Selection of questions and points 134 # This is the actual test configuration. Selection of questions and points
114 # It'a defined as a list of questions. Each question can be a single 135 # It'a defined as a list of questions. Each question can be a single
115 # question key or a list of keys from which one is chosen at random. 136 # question key or a list of keys from which one is chosen at random.
@@ -129,7 +150,7 @@ questions: @@ -129,7 +150,7 @@ questions:
129 This following one is wrong: 150 This following one is wrong:
130 151
131 - wrong-question # missing "ref:" key 152 - wrong-question # missing "ref:" key
132 - points: 2 153 + points: 2
133 ``` 154 ```
134 155
135 Some of the options have default values if they are omitted. The defaults are the following: 156 Some of the options have default values if they are omitted. The defaults are the following:
@@ -265,9 +286,10 @@ The server will try to convert the printed message to a float, a failure will gi @@ -265,9 +286,10 @@ The server will try to convert the printed message to a float, a failure will gi
265 ref: some-key 286 ref: some-key
266 type: textarea 287 type: textarea
267 text: write an expression to add x and y. # optional (default: '') 288 text: write an expression to add x and y. # optional (default: '')
268 - correct: path/to/myscript 289 + correct: myscript
269 ``` 290 ```
270 291
  292 +The script location is the same as the questions file.
271 An example of a script in python that validades an answer is 293 An example of a script in python that validades an answer is
272 294
273 ```python 295 ```python
conf/server.conf
@@ -1,37 +0,0 @@ @@ -1,37 +0,0 @@
1 -# -*- coding: utf-8 -*-  
2 -  
3 -[global]  
4 -;environment= 'production'  
5 -  
6 -; number of threads running  
7 -server.thread_pool= 10  
8 -  
9 -; Host address and port  
10 -; set socket_port = 443 if SSL is enabled below, 8080 otherwise  
11 -; if port is 443 then the server should be run as root. SSL also works on 8080.  
12 -server.socket_host = '0.0.0.0'  
13 -server.socket_port = 8080  
14 -  
15 -; Uncomment to enable SSL (see README.txt on how to generate certificates)  
16 -; server.ssl_module can be 'builtin' or 'pyopenssl'.  
17 -; server.ssl_module = 'builtin'  
18 -; server.ssl_certificate = 'certs/webserver.crt'  
19 -; server.ssl_private_key = 'certs/webserver.key'  
20 -; not required for snakeoil:  
21 -; server.ssl_certificate_chain = 'ca_certs.crt'  
22 -  
23 -log.screen = False  
24 -log.error_file = 'logs/errors.log'  
25 -log.access_file = 'logs/access.log'  
26 -  
27 -tools.sessions.on = True  
28 -tools.sessions.timeout = 240  
29 -tools.sessions.storage_type = 'ram'  
30 -tools.sessions.storage_path = 'sessions'  
31 -tools.auth.on = True  
32 -  
33 -[/]  
34 -tools.staticdir.root = os.path.normpath(os.path.abspath(os.path.curdir))  
35 -tools.staticdir.dir = 'static'  
36 -tools.staticdir.on = True  
37 -; tools.staticdir.debug = True  
demo/questions.yaml
@@ -44,13 +44,13 @@ @@ -44,13 +44,13 @@
44 ref: question-colors 44 ref: question-colors
45 type: textarea 45 type: textarea
46 text: Write names of the three basic colors. 46 text: Write names of the three basic colors.
47 - correct: demo/correct-question.py 47 + correct: correct-question.py
48 hint: They start by RGB and order does not matter. 48 hint: They start by RGB and order does not matter.
49 # --------------------------------------------------------------------------- 49 # ---------------------------------------------------------------------------
50 - 50 -
51 ref: question-whatever 51 ref: question-whatever
52 type: generator 52 type: generator
53 - script: demo/generate-question.py 53 + script: generate-question.py
54 arg: "11,120" 54 arg: "11,120"
55 # the script should print a question in yaml format like the ones above. 55 # the script should print a question in yaml format like the ones above.
56 # Print only the dictionary, not the list (hiffen). 56 # Print only the dictionary, not the list (hiffen).
demo/test.yaml
@@ -4,7 +4,7 @@ title: Teste de Demonstração @@ -4,7 +4,7 @@ title: Teste de Demonstração
4 4
5 # database contains students+passwords, final results of the tests, and questions done 5 # database contains students+passwords, final results of the tests, and questions done
6 # database: path/to/students.db 6 # database: path/to/students.db
7 -database: db/students.db 7 +database: inscritos.db
8 8
9 # this will generate a file for each test done. The file is like a replacement 9 # this will generate a file for each test done. The file is like a replacement
10 # for a test done in paper. 10 # for a test done in paper.
@@ -16,19 +16,20 @@ show_hints: True @@ -16,19 +16,20 @@ show_hints: True
16 practice: True 16 practice: True
17 # debug: False 17 # debug: False
18 18
19 -# When in practice mode and an answer is wrong (i.e. less that 0.5 correct),  
20 -# an insult is chosen from the list (optional)  
21 -offensive:  
22 - - Ó meu grande asno, então não sabes esta?  
23 - - Pois, pois... não estudes não...  
24 - - E eu sou o Elvis Presley...  
25 - - Pois, e bróculos também. 19 +#-----------------------------------------------------------------------------
  20 +# This is the base path applied to the questions files and all the scripts
  21 +# including generators and correctors.
  22 +# Either absolute path or relative to current directory.
  23 +path: demo
  24 +# (Note: answers are saved in a different path defined in answers_dir)
26 25
27 #----------------------------------------------------------------------------- 26 #-----------------------------------------------------------------------------
28 # List of files containing questions in yaml format. 27 # List of files containing questions in yaml format.
29 # Selected questions will be obtained from these files. 28 # Selected questions will be obtained from these files.
  29 +# (search in absolute path or current working directory)
30 files: 30 files:
31 - - demo/questions.yaml 31 + - questions.yaml
  32 +
32 #----------------------------------------------------------------------------- 33 #-----------------------------------------------------------------------------
33 # This is the list of questions. If a "ref:" has a list of keys, then 34 # This is the list of questions. If a "ref:" has a list of keys, then
34 # one question is selected from the list. 35 # one question is selected from the list.
logs/.gitignore
@@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
1 -# ignore everything except .gitignore  
2 -*  
3 -!.gitignore  
4 \ No newline at end of file 0 \ No newline at end of file
@@ -11,10 +11,14 @@ from hashlib import sha256 @@ -11,10 +11,14 @@ from hashlib import sha256
11 from mako.lookup import TemplateLookup 11 from mako.lookup import TemplateLookup
12 import urllib 12 import urllib
13 import html 13 import html
  14 +from os import path
  15 +
  16 +# path where this file is located
  17 +server_path = path.dirname(path.realpath(__file__))
14 18
15 SESSION_KEY = 'userid' 19 SESSION_KEY = 'userid'
16 20
17 -templates = TemplateLookup(directories=['templates'], input_encoding='utf-8') 21 +templates = TemplateLookup(directories=[server_path+'/templates'], input_encoding='utf-8')
18 22
19 23
20 def credentials_ok(uid, password, db): 24 def credentials_ok(uid, password, db):
@@ -152,7 +156,7 @@ class AuthController(object): @@ -152,7 +156,7 @@ class AuthController(object):
152 cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid 156 cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid
153 cherrypy.session['name'] = name 157 cherrypy.session['name'] = name
154 raise cherrypy.HTTPRedirect(from_page) 158 raise cherrypy.HTTPRedirect(from_page)
155 - logintemplate = templates.get_template('login.html') 159 + logintemplate = templates.get_template('/login.html')
156 return logintemplate.render(from_page=from_page) 160 return logintemplate.render(from_page=from_page)
157 161
158 @cherrypy.expose 162 @cherrypy.expose
@@ -4,6 +4,7 @@ import random @@ -4,6 +4,7 @@ import random
4 import re 4 import re
5 import subprocess 5 import subprocess
6 import sys 6 import sys
  7 +import os.path
7 8
8 # Example usage: 9 # Example usage:
9 # 10 #
@@ -18,19 +19,19 @@ import sys @@ -18,19 +19,19 @@ import sys
18 # test[0]['answer'] = 42 # insert answer 19 # test[0]['answer'] = 42 # insert answer
19 # grade = test[0].correct() # correct answer 20 # grade = test[0].correct() # correct answer
20 21
21 -  
22 # =========================================================================== 22 # ===========================================================================
23 class QuestionsPool(dict): 23 class QuestionsPool(dict):
24 '''This class contains base questions read from files, but which are 24 '''This class contains base questions read from files, but which are
25 not ready yet. They have to be instantiated separatly for each student.''' 25 not ready yet. They have to be instantiated separatly for each student.'''
26 26
27 #------------------------------------------------------------------------ 27 #------------------------------------------------------------------------
28 - def add_from_file(self, filename): 28 + def add_from_file(self, filename, path='.'):
  29 + path_file = os.path.normpath(os.path.join(path, filename))
29 try: 30 try:
30 - with open(filename, 'r') as f: 31 + with open(path_file, 'r') as f:
31 questions = yaml.load(f) 32 questions = yaml.load(f)
32 except(FileNotFoundError): 33 except(FileNotFoundError):
33 - print(' * Questions file "{0}" not found. Aborting...'.format(filename)) 34 + print(' * Questions file "{0}" not found. Aborting...'.format(path_file))
34 sys.exit(1) 35 sys.exit(1)
35 except(yaml.parser.ParserError): 36 except(yaml.parser.ParserError):
36 print(' * Error in questions file "{0}". Aborting...'.format(filename)) 37 print(' * Error in questions file "{0}". Aborting...'.format(filename))
@@ -47,6 +48,7 @@ class QuestionsPool(dict): @@ -47,6 +48,7 @@ class QuestionsPool(dict):
47 48
48 # filename and index (number in the file, 0 based) 49 # filename and index (number in the file, 0 based)
49 q['filename'] = filename 50 q['filename'] = filename
  51 + q['path'] = path
50 q['index'] = i 52 q['index'] = i
51 53
52 # ref (if missing, add 'filename.yaml:3') 54 # ref (if missing, add 'filename.yaml:3')
@@ -59,9 +61,9 @@ class QuestionsPool(dict): @@ -59,9 +61,9 @@ class QuestionsPool(dict):
59 self[q['ref']] = q 61 self[q['ref']] = q
60 62
61 #------------------------------------------------------------------------ 63 #------------------------------------------------------------------------
62 - def add_from_files(self, file_list):  
63 - for filename in file_list:  
64 - self.add_from_file(filename) 64 + def add_from_files(self, files, path='.'):
  65 + for filename in files:
  66 + self.add_from_file(filename, path)
65 67
66 68
67 #============================================================================ 69 #============================================================================
@@ -116,10 +118,12 @@ def question_generator(q): @@ -116,10 +118,12 @@ def question_generator(q):
116 118
117 q['arg'] = q.get('arg', '') # send this string to stdin 119 q['arg'] = q.get('arg', '') # send this string to stdin
118 120
  121 + script = os.path.abspath(os.path.normpath(os.path.join(q['path'], q['script'])))
119 try: 122 try:
120 - p = subprocess.Popen([q['script']], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) 123 + p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
121 except FileNotFoundError: 124 except FileNotFoundError:
122 - print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(q['script'], q['ref'], q['filename'])) 125 + print(' * Script "{0}" of question "{1}" in file "{2}" could not be found'.format(script, q['ref'], q['filename']))
  126 + sys.exit(1)
123 127
124 try: 128 try:
125 qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') 129 qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8')
@@ -386,8 +390,9 @@ class QuestionTextArea(Question): @@ -386,8 +390,9 @@ class QuestionTextArea(Question):
386 390
387 # The correction program expects data from stdin and prints the result to stdout. 391 # The correction program expects data from stdin and prints the result to stdout.
388 # The result should be a string that can be parsed to a float. 392 # The result should be a string that can be parsed to a float.
  393 + script = os.path.abspath(os.path.normpath(os.path.join(self['path'], self['correct'])))
389 try: 394 try:
390 - p = subprocess.Popen([self['correct']], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) 395 + p = subprocess.Popen([script], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
391 except FileNotFoundError as e: 396 except FileNotFoundError as e:
392 print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(self['correct'], self['ref'], self['filename'])) 397 print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(self['correct'], self['ref'], self['filename']))
393 raise e 398 raise e
@@ -7,12 +7,19 @@ @@ -7,12 +7,19 @@
7 import cherrypy 7 import cherrypy
8 from mako.lookup import TemplateLookup 8 from mako.lookup import TemplateLookup
9 import argparse 9 import argparse
  10 +from os import path
  11 +
  12 +# path where this file is located
  13 +server_path = path.dirname(path.realpath(__file__))
10 14
11 # my code 15 # my code
12 from myauth import AuthController, require 16 from myauth import AuthController, require
13 import test 17 import test
14 import database 18 import database
15 19
  20 +TEMPLATES_DIR = server_path + '/templates'
  21 +
  22 +
16 # ============================================================================ 23 # ============================================================================
17 # Classes that respond to HTTP 24 # Classes that respond to HTTP
18 # ============================================================================ 25 # ============================================================================
@@ -22,7 +29,7 @@ class Root(object): @@ -22,7 +29,7 @@ class Root(object):
22 self.testconf = testconf # base test dict (not instance) 29 self.testconf = testconf # base test dict (not instance)
23 self.database = database.Database(testconf['database']) 30 self.database = database.Database(testconf['database'])
24 self.auth = AuthController(database=testconf['database']) 31 self.auth = AuthController(database=testconf['database'])
25 - self.templates = TemplateLookup(directories=['templates'], input_encoding='utf-8') 32 + self.templates = TemplateLookup(directories=[TEMPLATES_DIR], input_encoding='utf-8')
26 self.tags = {'online': set(), 'finished': set()} # FIXME should be in application, not server 33 self.tags = {'online': set(), 'finished': set()} # FIXME should be in application, not server
27 34
28 # --- DEFAULT ------------------------------------------------------------ 35 # --- DEFAULT ------------------------------------------------------------
@@ -32,7 +39,7 @@ class Root(object): @@ -32,7 +39,7 @@ class Root(object):
32 def default(self, *args): 39 def default(self, *args):
33 raise cherrypy.HTTPRedirect('/test') 40 raise cherrypy.HTTPRedirect('/test')
34 41
35 - # --- LOGOUT ------------------------------------------------------------ 42 + # --- LOGOUT -------------------------------------------------------------
36 @cherrypy.expose 43 @cherrypy.expose
37 @require() 44 @require()
38 def logout(self): 45 def logout(self):
@@ -57,7 +64,7 @@ class Root(object): @@ -57,7 +64,7 @@ class Root(object):
57 cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION') 64 cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION')
58 65
59 students = self.database.get_students() 66 students = self.database.get_students()
60 - template = self.templates.get_template('students.html') 67 + template = self.templates.get_template('/students.html')
61 return template.render(students=students, tags=self.tags) 68 return template.render(students=students, tags=self.tags)
62 69
63 # --- RESULTS ------------------------------------------------------------ 70 # --- RESULTS ------------------------------------------------------------
@@ -65,7 +72,7 @@ class Root(object): @@ -65,7 +72,7 @@ class Root(object):
65 def results(self): 72 def results(self):
66 if self.testconf.get('practice', False): 73 if self.testconf.get('practice', False):
67 r = self.database.test_grades(self.testconf['ref']) 74 r = self.database.test_grades(self.testconf['ref'])
68 - template = self.templates.get_template('results.html') 75 + template = self.templates.get_template('/results.html')
69 return template.render(t=self.testconf, results=r) 76 return template.render(t=self.testconf, results=r)
70 else: 77 else:
71 raise cherrypy.HTTPRedirect('/') 78 raise cherrypy.HTTPRedirect('/')
@@ -88,7 +95,7 @@ class Root(object): @@ -88,7 +95,7 @@ class Root(object):
88 self.tags['online'].add(uid) # track logged in students 95 self.tags['online'].add(uid) # track logged in students
89 96
90 # Generate question 97 # Generate question
91 - template = self.templates.get_template('test.html') 98 + template = self.templates.get_template('/test.html')
92 return template.render(t=t, questions=t['questions']) 99 return template.render(t=t, questions=t['questions'])
93 100
94 # --- CORRECT ------------------------------------------------------------ 101 # --- CORRECT ------------------------------------------------------------
@@ -146,7 +153,8 @@ class Root(object): @@ -146,7 +153,8 @@ class Root(object):
146 # ============================================================================ 153 # ============================================================================
147 def parse_arguments(): 154 def parse_arguments():
148 argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.') 155 argparser = argparse.ArgumentParser(description='Server for online tests. Enrolled students and tests have to be previously configured. Please read the documentation included with this software before running the server.')
149 - argparser.add_argument('--server', default='conf/server.conf', type=str, help='server configuration file') 156 + serverconf_file = path.normpath(path.join(server_path, 'config', 'server.conf'))
  157 + argparser.add_argument('--server', default=serverconf_file, type=str, help='server configuration file')
150 argparser.add_argument('--debug', action='store_true', 158 argparser.add_argument('--debug', action='store_true',
151 help='Show datastructures when rendering questions') 159 help='Show datastructures when rendering questions')
152 argparser.add_argument('--show_ref', action='store_true', 160 argparser.add_argument('--show_ref', action='store_true',
@@ -159,7 +167,7 @@ def parse_arguments(): @@ -159,7 +167,7 @@ def parse_arguments():
159 help='Saves answers in JSON format') 167 help='Saves answers in JSON format')
160 argparser.add_argument('--practice', action='store_true', 168 argparser.add_argument('--practice', action='store_true',
161 help='Show correction results and allow repetitive resubmission of the test') 169 help='Show correction results and allow repetitive resubmission of the test')
162 - argparser.add_argument('testfile', type=str, nargs='+', help='test in YAML format.') 170 + argparser.add_argument('testfile', type=str, nargs='+', help='test/exam in YAML format.') # FIXME only one exam supported at the moment
163 return argparser.parse_args() 171 return argparser.parse_args()
164 172
165 # ============================================================================ 173 # ============================================================================
@@ -180,7 +188,8 @@ if __name__ == '__main__': @@ -180,7 +188,8 @@ if __name__ == '__main__':
180 root = Root(testconf) 188 root = Root(testconf)
181 189
182 # --- Mount and run server. 190 # --- Mount and run server.
  191 + cherrypy.config.update({'tools.staticdir.root': server_path})
183 cherrypy.quickstart(root, '/', config=arg.server) 192 cherrypy.quickstart(root, '/', config=arg.server)
184 - cherrypy.log.error('Terminated OK ------------------------', 'APPLICATION') 193 + cherrypy.log('Terminated OK ------------------------', 'APPLICATION')
185 print('\n- Server terminated OK') 194 print('\n- Server terminated OK')
186 print('=' * 79) 195 print('=' * 79)
templates/grade.html
@@ -68,8 +68,8 @@ @@ -68,8 +68,8 @@
68 <div class="container"> 68 <div class="container">
69 <div class="jumbotron drop-shadow"> 69 <div class="jumbotron drop-shadow">
70 <h1>Resultado</h1> 70 <h1>Resultado</h1>
71 - <p>Teve <strong>${'{:.1f}'.format(t['grade'])}</strong> valores no teste, numa escala de 0 a 20.</p>  
72 - <p>Pode desligar.</p> 71 + <p>Teve <strong>${'{:.1f}'.format(t['grade'])}</strong> valores na escala de 0 a 20.</p>
  72 + <p>Pode desligar o computador.</p>
73 <!-- <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p> --> 73 <!-- <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p> -->
74 </div> 74 </div>
75 75
@@ -107,7 +107,7 @@ @@ -107,7 +107,7 @@
107 </tr> 107 </tr>
108 % endfor 108 % endfor
109 </tbody> 109 </tbody>
110 - <tfoot> 110 +<!-- <tfoot>
111 <tr> 111 <tr>
112 <th>Média (não ponderada)</th> 112 <th>Média (não ponderada)</th>
113 <th colspan="2"> 113 <th colspan="2">
@@ -127,7 +127,7 @@ @@ -127,7 +127,7 @@
127 </div> 127 </div>
128 </th> 128 </th>
129 </tr> 129 </tr>
130 - </tfoot> 130 + </tfoot> -->
131 </table> 131 </table>
132 </div> <!-- panel --> 132 </div> <!-- panel -->
133 </div> <!-- container --> 133 </div> <!-- container -->
templates/login.html
@@ -62,32 +62,25 @@ @@ -62,32 +62,25 @@
62 <div class="col-xs-4"> 62 <div class="col-xs-4">
63 <img src="/logo_horizontal.png" class="img-responsive" /> 63 <img src="/logo_horizontal.png" class="img-responsive" />
64 </div> 64 </div>
65 - <div class="col-xs-8">  
66 - <h4 class="text-right">  
67 - <small>  
68 - $\sqrt{2\pi}$  
69 - </small>  
70 - </h4>  
71 - </div>  
72 - </div>  
73 -  
74 - <form method="post" action="/auth/login" class="form-signin">  
75 65
76 - <div class="row">  
77 - <input type="hidden" name="from_page" value="${from_page}">  
78 -  
79 - <input type="text" name="uid" class="form-control" placeholder="Número" required autofocus> 66 + <div class="col-xs-4">
  67 + </div>
80 68
81 - <input type="password" name="pw" class="form-control" placeholder="Password" required>  
82 - </div>  
83 - <div class="row">  
84 - <button class="btn btn-primary" type="submit">  
85 - <span class="glyphicon glyphicon-log-in"></span>&nbsp; Entrar  
86 - </button> 69 + <div class="col-xs-4">
  70 + <h4>Identificação:</h4>
  71 + <form method="post" action="/auth/login" class="form-signin">
  72 + <input type="hidden" name="from_page" value="${from_page}">
  73 + <div class="form-group">
  74 + <input type="text" name="uid" class="form-control" placeholder="Número" required autofocus>
  75 + <input type="password" name="pw" class="form-control" placeholder="Password" required>
  76 + </div>
  77 + <button class="btn btn-primary" type="submit">
  78 + <span class="glyphicon glyphicon-log-in"></span>&nbsp; Entrar
  79 + </button>
  80 + </form>
  81 + </div>
87 </div> 82 </div>
88 - </form>  
89 </div> 83 </div>
90 -  
91 </div> <!-- /container --> 84 </div> <!-- /container -->
92 </body> 85 </body>
93 </html> 86 </html>
templates/test.html
@@ -128,9 +128,8 @@ @@ -128,9 +128,8 @@
128 % if t['practice'] and 'grade' in t: 128 % if t['practice'] and 'grade' in t:
129 <div class="jumbotron drop-shadow"> 129 <div class="jumbotron drop-shadow">
130 <h1>Resultado</h1> 130 <h1>Resultado</h1>
131 - <p>Teve <strong>${'{:.1f}'.format(t['grade'])}</strong> valores no teste.</p> 131 + <p>Teve <strong>${'{:.1f}'.format(t['grade'])}</strong> valores.</p>
132 <p>Se quiser, pode corrigir e submeter o teste novamente.<br>Para terminar escolha a opção 'Sair' no menu.</p> 132 <p>Se quiser, pode corrigir e submeter o teste novamente.<br>Para terminar escolha a opção 'Sair' no menu.</p>
133 -  
134 </div> 133 </div>
135 % endif 134 % endif
136 135
@@ -241,9 +240,6 @@ @@ -241,9 +240,6 @@
241 </div> 240 </div>
242 % else: 241 % else:
243 <div class="alert alert-danger" role="alert"> 242 <div class="alert alert-danger" role="alert">
244 - <p>  
245 - ${random.choice(t['offensive']) if 'offensive' in t else ''}  
246 - </p>  
247 <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> 243 <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
248 ${round(q['grade'] * q['points'] / total_points * 20.0, 1)} pontos 244 ${round(q['grade'] * q['points'] / total_points * 20.0, 1)} pontos
249 </div> 245 </div>
@@ -32,6 +32,11 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -32,6 +32,11 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
32 test['practice'] = bool(test.get('practice', practice)) 32 test['practice'] = bool(test.get('practice', practice))
33 test['debug'] = bool(test.get('debug', debug)) 33 test['debug'] = bool(test.get('debug', debug))
34 test['show_ref'] = bool(test.get('show_ref', show_ref)) 34 test['show_ref'] = bool(test.get('show_ref', show_ref))
  35 + test['path'] = str(test.get('path', '.')) # questions dir FIXME use os.path
  36 + # FIXME check that the directory exists
  37 + if not os.path.exists(test['path']):
  38 + print(' * Questions path "{0}" does not exist. Fix it in "{1}".'.format(test['path'], filename))
  39 + sys.exit(1)
35 test['save_answers'] = bool(test.get('save_answers', save_answers)) 40 test['save_answers'] = bool(test.get('save_answers', save_answers))
36 if test['save_answers']: 41 if test['save_answers']:
37 if 'answers_dir' not in test: 42 if 'answers_dir' not in test:
@@ -43,13 +48,18 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals @@ -43,13 +48,18 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals
43 if 'database' not in test: 48 if 'database' not in test:
44 print(' * Missing database in the test configuration.') 49 print(' * Missing database in the test configuration.')
45 sys.exit(1) 50 sys.exit(1)
  51 + if not os.path.exists(test['database']):
  52 + print(' * Database "{0}" not found.'.format(test['database']))
  53 + sys.exit(1)
  54 +
46 55
47 if isinstance(test['files'], str): 56 if isinstance(test['files'], str):
48 test['files'] = [test['files']] 57 test['files'] = [test['files']]
49 58
50 # replace ref,points by actual questions from pool 59 # replace ref,points by actual questions from pool
51 pool = questions.QuestionsPool() 60 pool = questions.QuestionsPool()
52 - pool.add_from_files(test['files']) 61 + pool.add_from_files(files=
  62 + test['files'], path=test['path'])
53 63
54 for i, q in enumerate(test['questions']): 64 for i, q in enumerate(test['questions']):
55 # each question is a list of alternative versions, even if the list 65 # each question is a list of alternative versions, even if the list