Commit a4e0e360f3441c328b9d86a162fe70600f9b26bc
1 parent
5c9ee59b
Exists in
master
and in
1 other branch
- moved some 'run_script' and 'load_yaml' functions to a new module
'tools'. - use default 'alert' question when a question generator fails. - fixed logout link in /admin. - replaced exit() calls from test.py by TestFactoryException exception.
Showing
7 changed files
with
98 additions
and
94 deletions
Show diff stats
BUGS.md
@@ -4,11 +4,12 @@ | @@ -4,11 +4,12 @@ | ||
4 | - usar thread.Lock para aceder a variaveis de estado. | 4 | - usar thread.Lock para aceder a variaveis de estado. |
5 | - permitir adicionar imagens nas perguntas. | 5 | - permitir adicionar imagens nas perguntas. |
6 | - debug mode: log levels not working | 6 | - debug mode: log levels not working |
7 | +- replace sys.exit calls | ||
8 | +- if does not find questions, aborts silently | ||
7 | 9 | ||
8 | # TODO | 10 | # TODO |
9 | 11 | ||
10 | - implementar practice mode. | 12 | - implementar practice mode. |
11 | -- enviar logs para web? | ||
12 | - SQLAlchemy em vez da classe database. | 13 | - SQLAlchemy em vez da classe database. |
13 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). | 14 | - single page web no teste/correcçao. Página construída em javascript, obter perguntas com ajax (para practice?). |
14 | - aviso na pagina principal para quem usa browser da treta | 15 | - aviso na pagina principal para quem usa browser da treta |
@@ -19,6 +20,7 @@ | @@ -19,6 +20,7 @@ | ||
19 | - fazer uma calculadora javascript e por no menu. surge como modal | 20 | - fazer uma calculadora javascript e por no menu. surge como modal |
20 | - GeoIP? | 21 | - GeoIP? |
21 | - alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) | 22 | - alunos online têm acesso a /correct e servidor rebenta. (não é fácil impedir...) |
23 | +- enviar logs para web? | ||
22 | 24 | ||
23 | # FIXED | 25 | # FIXED |
24 | 26 |
questions.py
@@ -46,36 +46,8 @@ else: | @@ -46,36 +46,8 @@ else: | ||
46 | # correct: !regex '[aA]zul' | 46 | # correct: !regex '[aA]zul' |
47 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) | 47 | yaml.add_constructor('!regex', lambda l, n: re.compile(l.construct_scalar(n))) |
48 | 48 | ||
49 | -# --------------------------------------------------------------------------- | ||
50 | -# Runs a script and returns its stdout parsed as yaml, or None on error. | ||
51 | -# Note: requires python 3.5+ | ||
52 | -# --------------------------------------------------------------------------- | ||
53 | -def run_script(script, stdin='', timeout=5): | ||
54 | - try: | ||
55 | - p = subprocess.run([script], | ||
56 | - input=stdin, | ||
57 | - stdout=subprocess.PIPE, | ||
58 | - stderr=subprocess.STDOUT, | ||
59 | - universal_newlines=True, | ||
60 | - timeout=timeout, | ||
61 | - ) | ||
62 | - except FileNotFoundError: | ||
63 | - logger.error('Script not found: "{0}".'.format(script)) | ||
64 | - # return qerror | ||
65 | - except PermissionError: | ||
66 | - logger.error('Script "{0}" not executable (wrong permissions?).'.format(script)) | ||
67 | - except subprocess.TimeoutExpired: | ||
68 | - logger.error('Timeout {0}s exceeded while running script "{1}"'.format(timeout, script)) | ||
69 | - else: | ||
70 | - if p.returncode != 0: | ||
71 | - logger.warning('Script "{0}" returned error code {1}.'.format(script, p.returncode)) | ||
72 | - else: | ||
73 | - try: | ||
74 | - output = yaml.load(p.stdout) | ||
75 | - except: | ||
76 | - logger.error('Error parsing yaml output of script "{0}"'.format(script)) | ||
77 | - else: | ||
78 | - return output | 49 | +from tools import load_yaml, run_script |
50 | + | ||
79 | 51 | ||
80 | # =========================================================================== | 52 | # =========================================================================== |
81 | # This class contains a pool of questions generators from which particular | 53 | # This class contains a pool of questions generators from which particular |
@@ -111,15 +83,8 @@ class QuestionFactory(dict): | @@ -111,15 +83,8 @@ class QuestionFactory(dict): | ||
111 | # load single YAML questions file | 83 | # load single YAML questions file |
112 | # ----------------------------------------------------------------------- | 84 | # ----------------------------------------------------------------------- |
113 | def load_file(self, filename, questions_dir=''): | 85 | def load_file(self, filename, questions_dir=''): |
114 | - try: | ||
115 | - with open(path.normpath(path.join(questions_dir, filename)), 'r', encoding='utf-8') as f: | ||
116 | - questions = yaml.load(f) | ||
117 | - except EnvironmentError: | ||
118 | - logger.error('Couldn''t open "{0}". Skipped!'.format(file)) | ||
119 | - questions = [] | ||
120 | - except yaml.parser.ParserError: | ||
121 | - logger.error('While loading questions from "{0}". Skipped!'.format(file)) | ||
122 | - questions = [] | 86 | + f = path.normpath(path.join(questions_dir, filename)) |
87 | + questions = load_yaml(f, default=[]) | ||
123 | 88 | ||
124 | n = 0 | 89 | n = 0 |
125 | for i, q in enumerate(questions): | 90 | for i, q in enumerate(questions): |
@@ -174,7 +139,15 @@ class QuestionFactory(dict): | @@ -174,7 +139,15 @@ class QuestionFactory(dict): | ||
174 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) | 139 | logger.debug('Running script to generate question "{0}".'.format(q['ref'])) |
175 | q.setdefault('arg', '') # optional arguments will be sent to stdin | 140 | q.setdefault('arg', '') # optional arguments will be sent to stdin |
176 | script = path.normpath(path.join(q['path'], q['script'])) | 141 | script = path.normpath(path.join(q['path'], q['script'])) |
177 | - q.update(run_script(script=script, stdin=q['arg'])) | 142 | + out = run_script(script=script, stdin=q['arg']) |
143 | + try: | ||
144 | + q.update(out) | ||
145 | + except: | ||
146 | + q.update({ | ||
147 | + 'type': 'alert', | ||
148 | + 'title': 'Erro interno', | ||
149 | + 'text': 'Ocorreu um erro a gerar esta pergunta.' | ||
150 | + }) | ||
178 | # The generator was replaced by a question but not yet instantiated | 151 | # The generator was replaced by a question but not yet instantiated |
179 | 152 | ||
180 | # Finally we create an instance of Question() | 153 | # Finally we create an instance of Question() |
serve.py
@@ -8,7 +8,6 @@ import argparse | @@ -8,7 +8,6 @@ import argparse | ||
8 | import logging.config | 8 | import logging.config |
9 | import html | 9 | import html |
10 | import json | 10 | import json |
11 | -import yaml | ||
12 | 11 | ||
13 | try: | 12 | try: |
14 | import cherrypy | 13 | import cherrypy |
@@ -18,6 +17,7 @@ except ImportError: | @@ -18,6 +17,7 @@ except ImportError: | ||
18 | print('Some python packages are missing. See README.md for instructions.') | 17 | print('Some python packages are missing. See README.md for instructions.') |
19 | sys.exit(1) | 18 | sys.exit(1) |
20 | 19 | ||
20 | +from tools import load_yaml | ||
21 | 21 | ||
22 | # ============================================================================ | 22 | # ============================================================================ |
23 | # Authentication | 23 | # Authentication |
@@ -239,15 +239,19 @@ if __name__ == '__main__': | @@ -239,15 +239,19 @@ if __name__ == '__main__': | ||
239 | filename = path.abspath(path.expanduser(arg.testfile[0])) | 239 | filename = path.abspath(path.expanduser(arg.testfile[0])) |
240 | 240 | ||
241 | # --- Setup logging | 241 | # --- Setup logging |
242 | - with open(LOGGER_CONF,'r') as f: | ||
243 | - logging.config.dictConfig(yaml.load(f)) | 242 | + logging.config.dictConfig(load_yaml(LOGGER_CONF)) |
243 | + | ||
244 | + # with open(LOGGER_CONF,'r') as f: | ||
245 | + # logging.config.dictConfig(yaml.load(f)) | ||
244 | 246 | ||
245 | # --- start application | 247 | # --- start application |
246 | from app import App | 248 | from app import App |
247 | 249 | ||
248 | try: | 250 | try: |
249 | app = App(filename, vars(arg)) | 251 | app = App(filename, vars(arg)) |
250 | - except: | 252 | + except Exception as e: |
253 | + logging.critical('Cannot start application.') | ||
254 | + raise e # FIXME just for testing | ||
251 | sys.exit(1) | 255 | sys.exit(1) |
252 | 256 | ||
253 | # --- create webserver | 257 | # --- create webserver |
templates/admin.html
@@ -59,8 +59,8 @@ | @@ -59,8 +59,8 @@ | ||
59 | <span class="caret"></span> | 59 | <span class="caret"></span> |
60 | </a> | 60 | </a> |
61 | <ul class="dropdown-menu"> | 61 | <ul class="dropdown-menu"> |
62 | - <li class="active"><a href="/test">Teste</a></li> | ||
63 | - <li><a data-toggle="modal" data-target="#sair" id="form-button-submit"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> | 62 | + <li class="active"><a href="/test"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span> Teste</a></li> |
63 | + <li><a href="/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Sair</a></li> | ||
64 | </ul> | 64 | </ul> |
65 | </li> | 65 | </li> |
66 | </ul> | 66 | </ul> |
templates/grade.html
@@ -46,20 +46,12 @@ | @@ -46,20 +46,12 @@ | ||
46 | <a class="navbar-brand" href="#">UÉvora</a> | 46 | <a class="navbar-brand" href="#">UÉvora</a> |
47 | </div> | 47 | </div> |
48 | <div class="collapse navbar-collapse" id="myNavbar"> | 48 | <div class="collapse navbar-collapse" id="myNavbar"> |
49 | -<!-- <ul class="nav navbar-nav"> | ||
50 | - <li><a href="#">Teoria</a></li> | ||
51 | - <li><a href="#">Exemplos</a></li> | ||
52 | - <li><a href="#">Exercícios</a></li> | ||
53 | - </ul> | ||
54 | - --> | ||
55 | <ul class="nav navbar-nav navbar-right"> | 49 | <ul class="nav navbar-nav navbar-right"> |
56 | <li class="dropdown"> | 50 | <li class="dropdown"> |
57 | - <a class="dropdown-toggle" data-toggle="dropdown" href="#">${student_id} <span class="caret"></span></a> | ||
58 | -<!-- <ul class="dropdown-menu"> | ||
59 | - <li><a href="#">Toggle colors (day/night)</a></li> | ||
60 | - <li><a href="#">Change password</a></li> | ||
61 | - </ul> | ||
62 | - --> </li> | 51 | + <a class="dropdown-toggle" data-toggle="dropdown" href="#"> |
52 | + ${student_id} <span class="caret"></span> | ||
53 | + </a> | ||
54 | + </li> | ||
63 | </ul> | 55 | </ul> |
64 | </div> | 56 | </div> |
65 | </div> | 57 | </div> |
test.py
@@ -19,32 +19,7 @@ except ImportError: | @@ -19,32 +19,7 @@ except ImportError: | ||
19 | # my code | 19 | # my code |
20 | import questions | 20 | import questions |
21 | import database | 21 | import database |
22 | - | ||
23 | -# =========================================================================== | ||
24 | - | ||
25 | - | ||
26 | - | ||
27 | -# FIXME replace sys.exit calls by exceptions | ||
28 | - | ||
29 | -# ----------------------------------------------------------------------- | ||
30 | -# load dictionary from yaml file | ||
31 | -# ----------------------------------------------------------------------- | ||
32 | -def load_yaml(filename): | ||
33 | - try: | ||
34 | - f = open(filename, 'r', encoding='utf-8') | ||
35 | - except IOError: | ||
36 | - logger.critical('Cannot open YAML file "{}"'.format(filename)) | ||
37 | - sys.exit(1) # FIXME | ||
38 | - else: | ||
39 | - with f: | ||
40 | - try: | ||
41 | - d = yaml.load(f) | ||
42 | - except yaml.YAMLError as e: | ||
43 | - mark = e.problem_mark | ||
44 | - logger.critical('In YAML file "{0}" near line {1}, column {2}.'.format(filename, mark.line, mark.column+1)) | ||
45 | - sys.exit(1) # FIXME | ||
46 | - return d | ||
47 | - | 22 | +from tools import load_yaml |
48 | 23 | ||
49 | # =========================================================================== | 24 | # =========================================================================== |
50 | class TestFactoryException(Exception): # FIXME unused | 25 | class TestFactoryException(Exception): # FIXME unused |
@@ -81,7 +56,7 @@ class TestFactory(dict): | @@ -81,7 +56,7 @@ class TestFactory(dict): | ||
81 | for q in self['questions']: | 56 | for q in self['questions']: |
82 | for r in q['ref']: | 57 | for r in q['ref']: |
83 | if r not in self.question_factory: | 58 | if r not in self.question_factory: |
84 | - logger.error('Can''t find question "{}".'.format(r)) | 59 | + logger.error('Can\'t find question "{}".'.format(r)) |
85 | 60 | ||
86 | logger.info('Test factory ready for "{}".'.format(self['ref'])) | 61 | logger.info('Test factory ready for "{}".'.format(self['ref'])) |
87 | 62 | ||
@@ -96,8 +71,8 @@ class TestFactory(dict): | @@ -96,8 +71,8 @@ class TestFactory(dict): | ||
96 | 71 | ||
97 | # check for important missing keys in the test configuration file | 72 | # check for important missing keys in the test configuration file |
98 | if 'database' not in self: | 73 | if 'database' not in self: |
99 | - logger.critical('Missing "database"!') | ||
100 | - sys.exit(1) # FIXME | 74 | + logger.critical('Missing "database" key in configuration.') |
75 | + raise TestFactoryException() | ||
101 | 76 | ||
102 | if 'ref' not in self: | 77 | if 'ref' not in self: |
103 | logger.warning('Missing "ref". Will use current date/time.') | 78 | logger.warning('Missing "ref". Will use current date/time.') |
@@ -126,11 +101,11 @@ class TestFactory(dict): | @@ -126,11 +101,11 @@ class TestFactory(dict): | ||
126 | 101 | ||
127 | if not path.isfile(self['database']): | 102 | if not path.isfile(self['database']): |
128 | logger.critical('Cannot find database "{}"'.format(self['database'])) | 103 | logger.critical('Cannot find database "{}"'.format(self['database'])) |
129 | - sys.exit(1) | 104 | + raise TestFactoryException() |
130 | 105 | ||
131 | if not path.isdir(self['questions_dir']): | 106 | if not path.isdir(self['questions_dir']): |
132 | logger.critical('Cannot find questions directory "{}"'.format(self['questions_dir'])) | 107 | logger.critical('Cannot find questions directory "{}"'.format(self['questions_dir'])) |
133 | - sys.exit(1) | 108 | + raise TestFactoryException() |
134 | 109 | ||
135 | # make sure we have a list of question files. | 110 | # make sure we have a list of question files. |
136 | # no files were defined ==> load all YAML files from questions_dir | 111 | # no files were defined ==> load all YAML files from questions_dir |
@@ -138,8 +113,8 @@ class TestFactory(dict): | @@ -138,8 +113,8 @@ class TestFactory(dict): | ||
138 | try: | 113 | try: |
139 | self['files'] = fnmatch.filter(listdir(self['questions_dir']), '*.yaml') | 114 | self['files'] = fnmatch.filter(listdir(self['questions_dir']), '*.yaml') |
140 | except EnvironmentError: | 115 | except EnvironmentError: |
141 | - logger.critical('Could not get list of YAML question files.') | ||
142 | - sys.exit(1) | 116 | + logger.critical('Couldn\'t get list of YAML question files.') |
117 | + raise TestFactoryException() | ||
143 | 118 | ||
144 | if isinstance(self['files'], str): | 119 | if isinstance(self['files'], str): |
145 | self['files'] = [self['files']] | 120 | self['files'] = [self['files']] |
@@ -147,11 +122,11 @@ class TestFactory(dict): | @@ -147,11 +122,11 @@ class TestFactory(dict): | ||
147 | # FIXME if 'questions' not in self: load all of them | 122 | # FIXME if 'questions' not in self: load all of them |
148 | 123 | ||
149 | 124 | ||
150 | - try: # FIXME write logs to answers_dir? | 125 | + try: |
151 | f = open(path.join(self['answers_dir'],'REMOVE-ME'), 'w') | 126 | f = open(path.join(self['answers_dir'],'REMOVE-ME'), 'w') |
152 | except EnvironmentError: | 127 | except EnvironmentError: |
153 | logger.critical('Cannot write answers to "{0}".'.format(self['answers_dir'])) | 128 | logger.critical('Cannot write answers to "{0}".'.format(self['answers_dir'])) |
154 | - sys.exit(1) | 129 | + raise TestFactoryException() |
155 | else: | 130 | else: |
156 | with f: | 131 | with f: |
157 | f.write('You can safely remove this file.') | 132 | f.write('You can safely remove this file.') |
@@ -0,0 +1,58 @@ | @@ -0,0 +1,58 @@ | ||
1 | + | ||
2 | + | ||
3 | +import subprocess | ||
4 | +import logging | ||
5 | +import yaml | ||
6 | + | ||
7 | +# setup logger for this module | ||
8 | +logger = logging.getLogger(__name__) | ||
9 | + | ||
10 | +# --------------------------------------------------------------------------- | ||
11 | +# load data from yaml file | ||
12 | +# --------------------------------------------------------------------------- | ||
13 | +def load_yaml(filename, default=None): | ||
14 | + try: | ||
15 | + f = open(filename, 'r', encoding='utf-8') | ||
16 | + except IOError: | ||
17 | + logger.error('Can\'t open file "{}"'.format(filename)) | ||
18 | + return default | ||
19 | + else: | ||
20 | + with f: | ||
21 | + try: | ||
22 | + return yaml.load(f) | ||
23 | + except yaml.YAMLError as e: | ||
24 | + # except yaml.parser.ParserError: | ||
25 | + mark = e.problem_mark | ||
26 | + logger.error('In YAML file "{0}" near line {1}, column {2}.'.format(filename, mark.line, mark.column+1)) | ||
27 | + return default | ||
28 | + | ||
29 | +# --------------------------------------------------------------------------- | ||
30 | +# Runs a script and returns its stdout parsed as yaml, or None on error. | ||
31 | +# Note: requires python 3.5+ | ||
32 | +# --------------------------------------------------------------------------- | ||
33 | +def run_script(script, stdin='', timeout=5): | ||
34 | + try: | ||
35 | + p = subprocess.run([script], | ||
36 | + input=stdin, | ||
37 | + stdout=subprocess.PIPE, | ||
38 | + stderr=subprocess.STDOUT, | ||
39 | + universal_newlines=True, | ||
40 | + timeout=timeout, | ||
41 | + ) | ||
42 | + except FileNotFoundError: | ||
43 | + logger.error('Script not found: "{0}".'.format(script)) | ||
44 | + except PermissionError: | ||
45 | + logger.error('Script "{0}" not executable (wrong permissions?).'.format(script)) | ||
46 | + except subprocess.TimeoutExpired: | ||
47 | + logger.error('Timeout {0}s exceeded while running script "{1}"'.format(timeout, script)) | ||
48 | + else: | ||
49 | + if p.returncode != 0: | ||
50 | + logger.warning('Script "{0}" returned error code {1}.'.format(script, p.returncode)) | ||
51 | + else: | ||
52 | + try: | ||
53 | + output = yaml.load(p.stdout) | ||
54 | + except: | ||
55 | + logger.error('Error parsing yaml output of script "{0}"'.format(script)) | ||
56 | + else: | ||
57 | + return output | ||
58 | + |