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,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 |
MANUAL.md
@@ -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
myauth.py
@@ -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 |
questions.py
@@ -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 |
serve.py
@@ -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> 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 | </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> |
test.py
@@ -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 |