Commit a4e0e360f3441c328b9d86a162fe70600f9b26bc

Authored by Miguel Barao
1 parent 5c9ee59b
Exists in master and in 1 other branch dev

- 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.
@@ -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
@@ -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()
@@ -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>
@@ -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.')
tools.py 0 → 100644
@@ -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 +