Commit fa02522c2e869d2b441571279fa5e2be47b2483f
1 parent
34b58efe
Exists in
master
and in
1 other branch
- 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
Showing
13 changed files
with
122 additions
and
118 deletions
Show diff stats
BUGS.md
... | ... | @@ -2,10 +2,10 @@ |
2 | 2 | |
3 | 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 | 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 | 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 | 11 | # TODO |
... | ... | @@ -25,6 +25,10 @@ |
25 | 25 | |
26 | 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 | 32 | - testar envio de parametros para stdin para perguntas tipo generator. |
29 | 33 | - mathjax e jquery no login |
30 | 34 | - mostrar erro quando nao consegue importar questions files | ... | ... |
MANUAL.md
... | ... | @@ -11,10 +11,27 @@ Install python 3.4 and the following packages from pip: |
11 | 11 | |
12 | 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 | 36 | ### Create students database |
20 | 37 | |
... | ... | @@ -103,13 +120,17 @@ debug: False |
103 | 120 | # Show the file and ref field of each question |
104 | 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 | 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 | 134 | # This is the actual test configuration. Selection of questions and points |
114 | 135 | # It'a defined as a list of questions. Each question can be a single |
115 | 136 | # question key or a list of keys from which one is chosen at random. |
... | ... | @@ -129,7 +150,7 @@ questions: |
129 | 150 | This following one is wrong: |
130 | 151 | |
131 | 152 | - wrong-question # missing "ref:" key |
132 | - points: 2 | |
153 | + points: 2 | |
133 | 154 | ``` |
134 | 155 | |
135 | 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 | 286 | ref: some-key |
266 | 287 | type: textarea |
267 | 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 | 293 | An example of a script in python that validades an answer is |
272 | 294 | |
273 | 295 | ```python | ... | ... |
conf/server.conf
... | ... | @@ -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 | 44 | ref: question-colors |
45 | 45 | type: textarea |
46 | 46 | text: Write names of the three basic colors. |
47 | - correct: demo/correct-question.py | |
47 | + correct: correct-question.py | |
48 | 48 | hint: They start by RGB and order does not matter. |
49 | 49 | # --------------------------------------------------------------------------- |
50 | 50 | - |
51 | 51 | ref: question-whatever |
52 | 52 | type: generator |
53 | - script: demo/generate-question.py | |
53 | + script: generate-question.py | |
54 | 54 | arg: "11,120" |
55 | 55 | # the script should print a question in yaml format like the ones above. |
56 | 56 | # Print only the dictionary, not the list (hiffen). | ... | ... |
demo/test.yaml
... | ... | @@ -4,7 +4,7 @@ title: Teste de Demonstração |
4 | 4 | |
5 | 5 | # database contains students+passwords, final results of the tests, and questions done |
6 | 6 | # database: path/to/students.db |
7 | -database: db/students.db | |
7 | +database: inscritos.db | |
8 | 8 | |
9 | 9 | # this will generate a file for each test done. The file is like a replacement |
10 | 10 | # for a test done in paper. |
... | ... | @@ -16,19 +16,20 @@ show_hints: True |
16 | 16 | practice: True |
17 | 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 | 27 | # List of files containing questions in yaml format. |
29 | 28 | # Selected questions will be obtained from these files. |
29 | +# (search in absolute path or current working directory) | |
30 | 30 | files: |
31 | - - demo/questions.yaml | |
31 | + - questions.yaml | |
32 | + | |
32 | 33 | #----------------------------------------------------------------------------- |
33 | 34 | # This is the list of questions. If a "ref:" has a list of keys, then |
34 | 35 | # one question is selected from the list. | ... | ... |
logs/.gitignore
myauth.py
... | ... | @@ -11,10 +11,14 @@ from hashlib import sha256 |
11 | 11 | from mako.lookup import TemplateLookup |
12 | 12 | import urllib |
13 | 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 | 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 | 24 | def credentials_ok(uid, password, db): |
... | ... | @@ -152,7 +156,7 @@ class AuthController(object): |
152 | 156 | cherrypy.session[SESSION_KEY] = cherrypy.request.login = uid |
153 | 157 | cherrypy.session['name'] = name |
154 | 158 | raise cherrypy.HTTPRedirect(from_page) |
155 | - logintemplate = templates.get_template('login.html') | |
159 | + logintemplate = templates.get_template('/login.html') | |
156 | 160 | return logintemplate.render(from_page=from_page) |
157 | 161 | |
158 | 162 | @cherrypy.expose | ... | ... |
questions.py
... | ... | @@ -4,6 +4,7 @@ import random |
4 | 4 | import re |
5 | 5 | import subprocess |
6 | 6 | import sys |
7 | +import os.path | |
7 | 8 | |
8 | 9 | # Example usage: |
9 | 10 | # |
... | ... | @@ -18,19 +19,19 @@ import sys |
18 | 19 | # test[0]['answer'] = 42 # insert answer |
19 | 20 | # grade = test[0].correct() # correct answer |
20 | 21 | |
21 | - | |
22 | 22 | # =========================================================================== |
23 | 23 | class QuestionsPool(dict): |
24 | 24 | '''This class contains base questions read from files, but which are |
25 | 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 | 30 | try: |
30 | - with open(filename, 'r') as f: | |
31 | + with open(path_file, 'r') as f: | |
31 | 32 | questions = yaml.load(f) |
32 | 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 | 35 | sys.exit(1) |
35 | 36 | except(yaml.parser.ParserError): |
36 | 37 | print(' * Error in questions file "{0}". Aborting...'.format(filename)) |
... | ... | @@ -47,6 +48,7 @@ class QuestionsPool(dict): |
47 | 48 | |
48 | 49 | # filename and index (number in the file, 0 based) |
49 | 50 | q['filename'] = filename |
51 | + q['path'] = path | |
50 | 52 | q['index'] = i |
51 | 53 | |
52 | 54 | # ref (if missing, add 'filename.yaml:3') |
... | ... | @@ -59,9 +61,9 @@ class QuestionsPool(dict): |
59 | 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 | 118 | |
117 | 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 | 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 | 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 | 128 | try: |
125 | 129 | qyaml = p.communicate(input=q['arg'].encode('utf-8'), timeout=5)[0].decode('utf-8') |
... | ... | @@ -386,8 +390,9 @@ class QuestionTextArea(Question): |
386 | 390 | |
387 | 391 | # The correction program expects data from stdin and prints the result to stdout. |
388 | 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 | 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 | 396 | except FileNotFoundError as e: |
392 | 397 | print(' * Script "{0}" defined in question "{1}" of file "{2}" could not be found'.format(self['correct'], self['ref'], self['filename'])) |
393 | 398 | raise e | ... | ... |
serve.py
... | ... | @@ -7,12 +7,19 @@ |
7 | 7 | import cherrypy |
8 | 8 | from mako.lookup import TemplateLookup |
9 | 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 | 15 | # my code |
12 | 16 | from myauth import AuthController, require |
13 | 17 | import test |
14 | 18 | import database |
15 | 19 | |
20 | +TEMPLATES_DIR = server_path + '/templates' | |
21 | + | |
22 | + | |
16 | 23 | # ============================================================================ |
17 | 24 | # Classes that respond to HTTP |
18 | 25 | # ============================================================================ |
... | ... | @@ -22,7 +29,7 @@ class Root(object): |
22 | 29 | self.testconf = testconf # base test dict (not instance) |
23 | 30 | self.database = database.Database(testconf['database']) |
24 | 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 | 33 | self.tags = {'online': set(), 'finished': set()} # FIXME should be in application, not server |
27 | 34 | |
28 | 35 | # --- DEFAULT ------------------------------------------------------------ |
... | ... | @@ -32,7 +39,7 @@ class Root(object): |
32 | 39 | def default(self, *args): |
33 | 40 | raise cherrypy.HTTPRedirect('/test') |
34 | 41 | |
35 | - # --- LOGOUT ------------------------------------------------------------ | |
42 | + # --- LOGOUT ------------------------------------------------------------- | |
36 | 43 | @cherrypy.expose |
37 | 44 | @require() |
38 | 45 | def logout(self): |
... | ... | @@ -57,7 +64,7 @@ class Root(object): |
57 | 64 | cherrypy.log.error('Password updated for student %s.' % str(num), 'APPLICATION') |
58 | 65 | |
59 | 66 | students = self.database.get_students() |
60 | - template = self.templates.get_template('students.html') | |
67 | + template = self.templates.get_template('/students.html') | |
61 | 68 | return template.render(students=students, tags=self.tags) |
62 | 69 | |
63 | 70 | # --- RESULTS ------------------------------------------------------------ |
... | ... | @@ -65,7 +72,7 @@ class Root(object): |
65 | 72 | def results(self): |
66 | 73 | if self.testconf.get('practice', False): |
67 | 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 | 76 | return template.render(t=self.testconf, results=r) |
70 | 77 | else: |
71 | 78 | raise cherrypy.HTTPRedirect('/') |
... | ... | @@ -88,7 +95,7 @@ class Root(object): |
88 | 95 | self.tags['online'].add(uid) # track logged in students |
89 | 96 | |
90 | 97 | # Generate question |
91 | - template = self.templates.get_template('test.html') | |
98 | + template = self.templates.get_template('/test.html') | |
92 | 99 | return template.render(t=t, questions=t['questions']) |
93 | 100 | |
94 | 101 | # --- CORRECT ------------------------------------------------------------ |
... | ... | @@ -146,7 +153,8 @@ class Root(object): |
146 | 153 | # ============================================================================ |
147 | 154 | def parse_arguments(): |
148 | 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 | 158 | argparser.add_argument('--debug', action='store_true', |
151 | 159 | help='Show datastructures when rendering questions') |
152 | 160 | argparser.add_argument('--show_ref', action='store_true', |
... | ... | @@ -159,7 +167,7 @@ def parse_arguments(): |
159 | 167 | help='Saves answers in JSON format') |
160 | 168 | argparser.add_argument('--practice', action='store_true', |
161 | 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 | 171 | return argparser.parse_args() |
164 | 172 | |
165 | 173 | # ============================================================================ |
... | ... | @@ -180,7 +188,8 @@ if __name__ == '__main__': |
180 | 188 | root = Root(testconf) |
181 | 189 | |
182 | 190 | # --- Mount and run server. |
191 | + cherrypy.config.update({'tools.staticdir.root': server_path}) | |
183 | 192 | cherrypy.quickstart(root, '/', config=arg.server) |
184 | - cherrypy.log.error('Terminated OK ------------------------', 'APPLICATION') | |
193 | + cherrypy.log('Terminated OK ------------------------', 'APPLICATION') | |
185 | 194 | print('\n- Server terminated OK') |
186 | 195 | print('=' * 79) | ... | ... |
templates/grade.html
... | ... | @@ -68,8 +68,8 @@ |
68 | 68 | <div class="container"> |
69 | 69 | <div class="jumbotron drop-shadow"> |
70 | 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 | 73 | <!-- <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a></p> --> |
74 | 74 | </div> |
75 | 75 | |
... | ... | @@ -107,7 +107,7 @@ |
107 | 107 | </tr> |
108 | 108 | % endfor |
109 | 109 | </tbody> |
110 | - <tfoot> | |
110 | +<!-- <tfoot> | |
111 | 111 | <tr> |
112 | 112 | <th>Média (não ponderada)</th> |
113 | 113 | <th colspan="2"> |
... | ... | @@ -127,7 +127,7 @@ |
127 | 127 | </div> |
128 | 128 | </th> |
129 | 129 | </tr> |
130 | - </tfoot> | |
130 | + </tfoot> --> | |
131 | 131 | </table> |
132 | 132 | </div> <!-- panel --> |
133 | 133 | </div> <!-- container --> | ... | ... |
templates/login.html
... | ... | @@ -62,32 +62,25 @@ |
62 | 62 | <div class="col-xs-4"> |
63 | 63 | <img src="/logo_horizontal.png" class="img-responsive" /> |
64 | 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> 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> Entrar | |
79 | + </button> | |
80 | + </form> | |
81 | + </div> | |
87 | 82 | </div> |
88 | - </form> | |
89 | 83 | </div> |
90 | - | |
91 | 84 | </div> <!-- /container --> |
92 | 85 | </body> |
93 | 86 | </html> | ... | ... |
templates/test.html
... | ... | @@ -128,9 +128,8 @@ |
128 | 128 | % if t['practice'] and 'grade' in t: |
129 | 129 | <div class="jumbotron drop-shadow"> |
130 | 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 | 132 | <p>Se quiser, pode corrigir e submeter o teste novamente.<br>Para terminar escolha a opção 'Sair' no menu.</p> |
133 | - | |
134 | 133 | </div> |
135 | 134 | % endif |
136 | 135 | |
... | ... | @@ -241,9 +240,6 @@ |
241 | 240 | </div> |
242 | 241 | % else: |
243 | 242 | <div class="alert alert-danger" role="alert"> |
244 | - <p> | |
245 | - ${random.choice(t['offensive']) if 'offensive' in t else ''} | |
246 | - </p> | |
247 | 243 | <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> |
248 | 244 | ${round(q['grade'] * q['points'] / total_points * 20.0, 1)} pontos |
249 | 245 | </div> | ... | ... |
test.py
... | ... | @@ -32,6 +32,11 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals |
32 | 32 | test['practice'] = bool(test.get('practice', practice)) |
33 | 33 | test['debug'] = bool(test.get('debug', debug)) |
34 | 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 | 40 | test['save_answers'] = bool(test.get('save_answers', save_answers)) |
36 | 41 | if test['save_answers']: |
37 | 42 | if 'answers_dir' not in test: |
... | ... | @@ -43,13 +48,18 @@ def read_configuration(filename, debug=False, show_points=False, show_hints=Fals |
43 | 48 | if 'database' not in test: |
44 | 49 | print(' * Missing database in the test configuration.') |
45 | 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 | 56 | if isinstance(test['files'], str): |
48 | 57 | test['files'] = [test['files']] |
49 | 58 | |
50 | 59 | # replace ref,points by actual questions from pool |
51 | 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 | 64 | for i, q in enumerate(test['questions']): |
55 | 65 | # each question is a list of alternative versions, even if the list | ... | ... |