Commit 9d2af8636c907decb9f2b8726e0c64b5fdb3c8b7

Authored by Miguel Barão
1 parent 50eac3c3
Exists in master and in 1 other branch dev

Allow generator to send args to correct script.

fix yaml.load() to yaml.safe_load() as per deprecation warning.
fix flake8 errors
@@ -8,7 +8,6 @@ e.g. retornar None quando nao ha alteracoes relativamente à última vez. @@ -8,7 +8,6 @@ e.g. retornar None quando nao ha alteracoes relativamente à última vez.
8 ou usar push (websockets?) 8 ou usar push (websockets?)
9 - pymips: nao pode executar syscalls do spim. 9 - pymips: nao pode executar syscalls do spim.
10 - perguntas checkbox [right,wrong] com pelo menos uma opção correcta. 10 - perguntas checkbox [right,wrong] com pelo menos uma opção correcta.
11 -- questions.py textarea has a abspath which does not make sense! why is it there? not working for perguntations, but seems to work for aprendizations  
12 - eventos unfocus? 11 - eventos unfocus?
13 - servidor nao esta a lidar com eventos scroll/resize. ignorar? 12 - servidor nao esta a lidar com eventos scroll/resize. ignorar?
14 - Test.reset_answers() unused. 13 - Test.reset_answers() unused.
@@ -62,6 +61,8 @@ ou usar push (websockets?) @@ -62,6 +61,8 @@ ou usar push (websockets?)
62 61
63 # FIXED 62 # FIXED
64 63
  64 +- questions.py textarea has a abspath which does not make sense! why is it there? not working for perguntations, but seems to work for aprendizations
  65 +- textarea foi modificado em aprendizations para receber cmd line args. corrigir aqui tb.
65 - usar npm para instalar javascript 66 - usar npm para instalar javascript
66 - se aluno entrar com l12345 rebenta. numa funcao get_... ver imagem no ipad 67 - se aluno entrar com l12345 rebenta. numa funcao get_... ver imagem no ipad
67 - no test3 está contar 1.0 valores numa pergunta do tipo info? acontece para type: info, e não para type: information 68 - no test3 está contar 1.0 valores numa pergunta do tipo info? acontece para type: info, e não para type: information
@@ -149,7 +149,7 @@ Instead, tcp traffic can be forwarded from port 443 to 8443 where the server is @@ -149,7 +149,7 @@ Instead, tcp traffic can be forwarded from port 443 to 8443 where the server is
149 listening. 149 listening.
150 The details depend on the operating system/firewall. 150 The details depend on the operating system/firewall.
151 151
152 -### debian: 152 +### debian
153 153
154 FIXME: Untested 154 FIXME: Untested
155 155
@@ -226,34 +226,34 @@ directory. @@ -226,34 +226,34 @@ directory.
226 226
227 ## Troubleshooting 227 ## Troubleshooting
228 228
229 -* The server tries to run `python3` so this command must be accessible from 229 +- The server tries to run `python3` so this command must be accessible from
230 user accounts. Currently, the minimum supported python version is 3.6. 230 user accounts. Currently, the minimum supported python version is 3.6.
231 231
232 -* If you are getting any `UnicodeEncodeError` type of errors that's because the 232 +- If you are getting any `UnicodeEncodeError` type of errors that's because the
233 terminal is not supporting UTF-8. 233 terminal is not supporting UTF-8.
234 This error may occur when a unicode character is printed to the screen by the 234 This error may occur when a unicode character is printed to the screen by the
235 server or, when running question generator or correction scripts, a message is 235 server or, when running question generator or correction scripts, a message is
236 piped between the server and the scripts that includes unicode characters. 236 piped between the server and the scripts that includes unicode characters.
237 Try running `locale` on the terminal and see if there are any error messages. 237 Try running `locale` on the terminal and see if there are any error messages.
238 Solutions: 238 Solutions:
239 - - debian: `sudo dpkg-reconfigure locales` and select your UTF-8 locales.  
240 - - FreeBSD: edit `~/.login_conf` to use UTF-8, for example: 239 + - debian: `sudo dpkg-reconfigure locales` and select your UTF-8 locales.
  240 + - FreeBSD: edit `~/.login_conf` to use UTF-8, for example:
241 241
242 - ```  
243 - me:\  
244 - :charset=UTF-8:\  
245 - :lang=en_US.UTF-8:  
246 - ``` 242 + ```text
  243 + me:\
  244 + :charset=UTF-8:\
  245 + :lang=en_US.UTF-8:
  246 + ```
247 247
248 --- 248 ---
249 249
250 -## Contribute ### 250 +## Contribute
251 251
252 -* Writing questions in yaml format  
253 -* Testing and reporting bugs  
254 -* Code review  
255 -* New features and ideas 252 +- Writing questions in yaml format
  253 +- Testing and reporting bugs
  254 +- Code review
  255 +- New features and ideas
256 256
257 -### Contacts ### 257 +### Contacts
258 258
259 -* Miguel Barão mjsb@uevora.pt 259 +- Miguel Barão mjsb@uevora.pt
perguntations/app.py
@@ -32,10 +32,12 @@ async def check_password(try_pw, password): @@ -32,10 +32,12 @@ async def check_password(try_pw, password):
32 hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password) 32 hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password)
33 return password == hashed 33 return password == hashed
34 34
  35 +
35 async def hash_password(pw): 36 async def hash_password(pw):
36 pw = pw.encode('utf-8') 37 pw = pw.encode('utf-8')
37 loop = asyncio.get_running_loop() 38 loop = asyncio.get_running_loop()
38 - return await loop.run_in_executor(None, bcrypt.hashpw, pw, bcrypt.gensalt()) 39 + r = await loop.run_in_executor(None, bcrypt.hashpw, pw, bcrypt.gensalt())
  40 + return r
39 41
40 42
41 # ============================================================================ 43 # ============================================================================
@@ -109,7 +111,9 @@ class App(object): @@ -109,7 +111,9 @@ class App(object):
109 111
110 # get name+password from db 112 # get name+password from db
111 with self.db_session() as s: 113 with self.db_session() as s:
112 - name, password = s.query(Student.name, Student.password).filter_by(id=uid).one() 114 + name, password = s.query(Student.name, Student.password)\
  115 + .filter_by(id=uid)\
  116 + .one()
113 117
114 # first login updates the password 118 # first login updates the password
115 if password == '': # update password on first login 119 if password == '': # update password on first login
@@ -140,7 +144,8 @@ class App(object): @@ -140,7 +144,8 @@ class App(object):
140 if uid in self.online: 144 if uid in self.online:
141 logger.info(f'Student {uid}: started generating new test.') 145 logger.info(f'Student {uid}: started generating new test.')
142 student_id = self.online[uid]['student'] # {number, name} 146 student_id = self.online[uid]['student'] # {number, name}
143 - self.online[uid]['test'] = await self.testfactory.generate(student_id) 147 + test = await self.testfactory.generate(student_id)
  148 + self.online[uid]['test'] = test
144 logger.debug(f'Student {uid}: finished generating test.') 149 logger.debug(f'Student {uid}: finished generating test.')
145 return self.online[uid]['test'] 150 return self.online[uid]['test']
146 else: 151 else:
@@ -156,7 +161,8 @@ class App(object): @@ -156,7 +161,8 @@ class App(object):
156 grade = await t.correct() 161 grade = await t.correct()
157 162
158 # save test in JSON format 163 # save test in JSON format
159 - fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json' 164 + fields = (t['student']['number'], t['ref'], str(t['finish_time']))
  165 + fname = ' -- '.join(fields) + '.json'
160 fpath = path.join(t['answers_dir'], fname) 166 fpath = path.join(t['answers_dir'], fname)
161 t.save_json(fpath) 167 t.save_json(fpath)
162 168
@@ -189,9 +195,8 @@ class App(object): @@ -189,9 +195,8 @@ class App(object):
189 t.giveup() 195 t.giveup()
190 196
191 # save JSON with the test 197 # save JSON with the test
192 - # fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json'  
193 - # fpath = path.abspath(path.join(t['answers_dir'], fname))  
194 - fname = ' -- '.join((t['student']['number'], t['ref'], str(t['finish_time']))) + '.json' 198 + fields = (t['student']['number'], t['ref'], str(t['finish_time']))
  199 + fname = ' -- '.join(fields) + '.json'
195 fpath = path.join(t['answers_dir'], fname) 200 fpath = path.join(t['answers_dir'], fname)
196 t.save_json(fpath) 201 t.save_json(fpath)
197 202
@@ -225,22 +230,28 @@ class App(object): @@ -225,22 +230,28 @@ class App(object):
225 230
226 def get_student_grades_from_all_tests(self, uid): 231 def get_student_grades_from_all_tests(self, uid):
227 with self.db_session() as s: 232 with self.db_session() as s:
228 - return s.query(Test.title, Test.grade, Test.finishtime).filter_by(student_id=uid).order_by(Test.finishtime) 233 + return s.query(Test.title, Test.grade, Test.finishtime)\
  234 + .filter_by(student_id=uid)\
  235 + .order_by(Test.finishtime)
229 236
230 def get_json_filename_of_test(self, test_id): 237 def get_json_filename_of_test(self, test_id):
231 with self.db_session() as s: 238 with self.db_session() as s:
232 - return s.query(Test.filename).filter_by(id=test_id).scalar()  
233 -  
234 - # def get_online_students(self): # [('uid', 'name', 'starttime')]  
235 - # return [(k, v['student']['name'], str(v.get('test', {}).get('start_time', '---'))) for k, v in self.online.items() if k != '0'] 239 + return s.query(Test.filename)\
  240 + .filter_by(id=test_id)\
  241 + .scalar()
236 242
237 def get_all_students(self): 243 def get_all_students(self):
238 with self.db_session() as s: 244 with self.db_session() as s:
239 - return s.query(Student.id, Student.name, Student.password).filter(Student.id != '0').order_by(Student.id) 245 + return s.query(Student.id, Student.name, Student.password)\
  246 + .filter(Student.id != '0')\
  247 + .order_by(Student.id)
240 248
241 def get_student_grades_from_test(self, uid, testid): 249 def get_student_grades_from_test(self, uid, testid):
242 with self.db_session() as s: 250 with self.db_session() as s:
243 - return s.query(Test.grade, Test.finishtime, Test.id).filter_by(student_id=uid).filter_by(ref=testid).all() 251 + return s.query(Test.grade, Test.finishtime, Test.id)\
  252 + .filter_by(student_id=uid)\
  253 + .filter_by(ref=testid)\
  254 + .all()
244 255
245 def get_students_state(self): 256 def get_students_state(self):
246 return [{ 257 return [{
@@ -248,7 +259,8 @@ class App(object): @@ -248,7 +259,8 @@ class App(object):
248 'name': name, 259 'name': name,
249 'allowed': uid in self.allowed, 260 'allowed': uid in self.allowed,
250 'online': uid in self.online, 261 'online': uid in self.online,
251 - 'start_time': self.online.get(uid, {}).get('test', {}).get('start_time', ''), 262 + 'start_time': self.online.get(uid, {}).get('test', {})
  263 + .get('start_time', ''),
252 'password_defined': pw != '', 264 'password_defined': pw != '',
253 'grades': self.get_student_grades_from_test(uid, self.testfactory['ref']), 265 'grades': self.get_student_grades_from_test(uid, self.testfactory['ref']),
254 # 'focus': self.online.get(uid, {}).get('student', {}).get('focus', True), # FIXME 266 # 'focus': self.online.get(uid, {}).get('student', {}).get('focus', True), # FIXME
perguntations/factory.py
@@ -32,9 +32,10 @@ import logging @@ -32,9 +32,10 @@ import logging
32 32
33 # this project 33 # this project
34 from perguntations.tools import load_yaml, run_script 34 from perguntations.tools import load_yaml, run_script
35 -from perguntations.questions import (QuestionRadio, QuestionCheckbox, QuestionText,  
36 - QuestionTextRegex, QuestionNumericInterval,  
37 - QuestionTextArea, QuestionInformation) 35 +from perguntations.questions import (QuestionRadio, QuestionCheckbox,
  36 + QuestionText, QuestionTextRegex,
  37 + QuestionNumericInterval, QuestionTextArea,
  38 + QuestionInformation)
38 39
39 # setup logger for this module 40 # setup logger for this module
40 logger = logging.getLogger(__name__) 41 logger = logging.getLogger(__name__)
@@ -73,16 +74,18 @@ class QuestionFactory(dict): @@ -73,16 +74,18 @@ class QuestionFactory(dict):
73 # After this, each question will have at least 'ref' and 'type' keys. 74 # After this, each question will have at least 'ref' and 'type' keys.
74 # ------------------------------------------------------------------------ 75 # ------------------------------------------------------------------------
75 def add_question(self, question): 76 def add_question(self, question):
  77 + q = question
  78 +
76 # if missing defaults to ref='/path/file.yaml:3' 79 # if missing defaults to ref='/path/file.yaml:3'
77 - question.setdefault('ref', f'{question["filename"]}:{question["index"]}') 80 + q.setdefault('ref', f'{q["filename"]}:{q["index"]}')
78 81
79 - if question['ref'] in self:  
80 - logger.error(f'Duplicate reference "{question["ref"]}".') 82 + if q['ref'] in self:
  83 + logger.error(f'Duplicate reference "{q["ref"]}".')
81 84
82 - question.setdefault('type', 'information') 85 + q.setdefault('type', 'information')
83 86
84 - self[question['ref']] = question  
85 - logger.debug(f'Added question "{question["ref"]}" to the pool.') 87 + self[q['ref']] = q
  88 + logger.debug(f'Added question "{q["ref"]}" to the pool.')
86 89
87 # ------------------------------------------------------------------------ 90 # ------------------------------------------------------------------------
88 # load single YAML questions file 91 # load single YAML questions file
@@ -140,10 +143,11 @@ class QuestionFactory(dict): @@ -140,10 +143,11 @@ class QuestionFactory(dict):
140 # which will print a valid question in yaml format to stdout. This 143 # which will print a valid question in yaml format to stdout. This
141 # output is then converted to a dictionary and `q` becomes that dict. 144 # output is then converted to a dictionary and `q` becomes that dict.
142 if q['type'] == 'generator': 145 if q['type'] == 'generator':
143 - logger.debug(f'Running script to generate question "{ref}".')  
144 - q.setdefault('arg', '') # optional arguments will be sent to stdin  
145 - script = path.normpath(path.join(q['path'], q['script']))  
146 - out = run_script(script=script, stdin=q['arg']) 146 + logger.debug(f'Generating "{ref}" from {q["script"]}')
  147 + q.setdefault('args', []) # optional arguments
  148 + q.setdefault('stdin', '')
  149 + script = path.join(q['path'], q['script'])
  150 + out = run_script(script=script, args=q['args'], stdin=q['stdin'])
147 try: 151 try:
148 q.update(out) 152 q.update(out)
149 except Exception: 153 except Exception:
@@ -152,6 +156,7 @@ class QuestionFactory(dict): @@ -152,6 +156,7 @@ class QuestionFactory(dict):
152 'title': 'Erro interno', 156 'title': 'Erro interno',
153 'text': 'Ocorreu um erro a gerar esta pergunta.' 157 'text': 'Ocorreu um erro a gerar esta pergunta.'
154 }) 158 })
  159 +
155 # The generator was replaced by a question but not yet instantiated 160 # The generator was replaced by a question but not yet instantiated
156 161
157 # Finally we create an instance of Question() 162 # Finally we create an instance of Question()
perguntations/initdb.py
@@ -181,7 +181,7 @@ def main(): @@ -181,7 +181,7 @@ def main():
181 for s in args.update: 181 for s in args.update:
182 print(f'Updating password of: {s}') 182 print(f'Updating password of: {s}')
183 u = session.query(Student).get(s) 183 u = session.query(Student).get(s)
184 - pw =(args.pw or s).encode('utf-8') 184 + pw = (args.pw or s).encode('utf-8')
185 u.password = bcrypt.hashpw(pw, bcrypt.gensalt()) 185 u.password = bcrypt.hashpw(pw, bcrypt.gensalt())
186 session.commit() 186 session.commit()
187 187
perguntations/questions.py
@@ -57,7 +57,7 @@ class Question(dict): @@ -57,7 +57,7 @@ class Question(dict):
57 57
58 def set_defaults(self, d): 58 def set_defaults(self, d):
59 'Add k:v pairs from default dict d for nonexistent keys' 59 'Add k:v pairs from default dict d for nonexistent keys'
60 - for k,v in d.items(): 60 + for k, v in d.items():
61 self.setdefault(k, v) 61 self.setdefault(k, v)
62 62
63 63
@@ -74,7 +74,7 @@ class QuestionRadio(Question): @@ -74,7 +74,7 @@ class QuestionRadio(Question):
74 choose (int) # only used if shuffle=True 74 choose (int) # only used if shuffle=True
75 ''' 75 '''
76 76
77 - #------------------------------------------------------------------------ 77 + # ------------------------------------------------------------------------
78 def __init__(self, q): 78 def __init__(self, q):
79 super().__init__(q) 79 super().__init__(q)
80 80
@@ -91,7 +91,12 @@ class QuestionRadio(Question): @@ -91,7 +91,12 @@ class QuestionRadio(Question):
91 # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] 91 # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0]
92 # correctness levels from 0.0 to 1.0 (no discount here!) 92 # correctness levels from 0.0 to 1.0 (no discount here!)
93 if isinstance(self['correct'], int): 93 if isinstance(self['correct'], int):
94 - self['correct'] = [1.0 if x==self['correct'] else 0.0 for x in range(n)] 94 + self['correct'] = [1.0 if x == self['correct'] else 0.0
  95 + for x in range(n)]
  96 +
  97 + if len(self['correct']) != n:
  98 + logger.error(f'Options and correct mismatch in '
  99 + f'"{self["ref"]}", file "{self["filename"]}".')
95 100
96 if self['shuffle']: 101 if self['shuffle']:
97 # separate right from wrong options 102 # separate right from wrong options
@@ -102,8 +107,8 @@ class QuestionRadio(Question): @@ -102,8 +107,8 @@ class QuestionRadio(Question):
102 107
103 # choose 1 correct option 108 # choose 1 correct option
104 r = random.choice(right) 109 r = random.choice(right)
105 - options = [ self['options'][r] ]  
106 - correct = [ 1.0 ] 110 + options = [self['options'][r]]
  111 + correct = [1.0]
107 112
108 # choose remaining wrong options 113 # choose remaining wrong options
109 random.shuffle(wrong) 114 random.shuffle(wrong)
@@ -113,10 +118,10 @@ class QuestionRadio(Question): @@ -113,10 +118,10 @@ class QuestionRadio(Question):
113 118
114 # final shuffle of the options 119 # final shuffle of the options
115 perm = random.sample(range(self['choose']), self['choose']) 120 perm = random.sample(range(self['choose']), self['choose'])
116 - self['options'] = [ str(options[i]) for i in perm ]  
117 - self['correct'] = [ float(correct[i]) for i in perm ] 121 + self['options'] = [str(options[i]) for i in perm]
  122 + self['correct'] = [float(correct[i]) for i in perm]
118 123
119 - #------------------------------------------------------------------------ 124 + # ------------------------------------------------------------------------
120 # can return negative values for wrong answers 125 # can return negative values for wrong answers
121 def correct(self): 126 def correct(self):
122 super().correct() 127 super().correct()
@@ -145,7 +150,7 @@ class QuestionCheckbox(Question): @@ -145,7 +150,7 @@ class QuestionCheckbox(Question):
145 answer (None or an actual answer) 150 answer (None or an actual answer)
146 ''' 151 '''
147 152
148 - #------------------------------------------------------------------------ 153 + # ------------------------------------------------------------------------
149 def __init__(self, q): 154 def __init__(self, q):
150 super().__init__(q) 155 super().__init__(q)
151 156
@@ -154,14 +159,15 @@ class QuestionCheckbox(Question): @@ -154,14 +159,15 @@ class QuestionCheckbox(Question):
154 # set defaults if missing 159 # set defaults if missing
155 self.set_defaults({ 160 self.set_defaults({
156 'text': '', 161 'text': '',
157 - 'correct': [1.0] * n, # Using 0.0 breaks the (right, wrong) options 162 + 'correct': [1.0] * n, # Using 0.0 breaks the (right, wrong) opts
158 'shuffle': True, 163 'shuffle': True,
159 'discount': True, 164 'discount': True,
160 - 'choose': n, # number of options 165 + 'choose': n, # number of options
161 }) 166 })
162 167
163 if len(self['correct']) != n: 168 if len(self['correct']) != n:
164 - logger.error(f'Options and correct size mismatch in "{self["ref"]}", file "{self["filename"]}".') 169 + logger.error(f'Options and correct size mismatch in '
  170 + f'"{self["ref"]}", file "{self["filename"]}".')
165 171
166 # if an option is a list of (right, wrong), pick one 172 # if an option is a list of (right, wrong), pick one
167 # FIXME it's possible that all options are chosen wrong 173 # FIXME it's possible that all options are chosen wrong
@@ -169,9 +175,9 @@ class QuestionCheckbox(Question): @@ -169,9 +175,9 @@ class QuestionCheckbox(Question):
169 correct = [] 175 correct = []
170 for o, c in zip(self['options'], self['correct']): 176 for o, c in zip(self['options'], self['correct']):
171 if isinstance(o, list): 177 if isinstance(o, list):
172 - r = random.randint(0,1) 178 + r = random.randint(0, 1)
173 o = o[r] 179 o = o[r]
174 - c = c if r==0 else -c 180 + c = c if r == 0 else -c
175 options.append(str(o)) 181 options.append(str(o))
176 correct.append(float(c)) 182 correct.append(float(c))
177 183
@@ -182,7 +188,7 @@ class QuestionCheckbox(Question): @@ -182,7 +188,7 @@ class QuestionCheckbox(Question):
182 self['options'] = [options[i] for i in perm] 188 self['options'] = [options[i] for i in perm]
183 self['correct'] = [correct[i] for i in perm] 189 self['correct'] = [correct[i] for i in perm]
184 190
185 - #------------------------------------------------------------------------ 191 + # ------------------------------------------------------------------------
186 # can return negative values for wrong answers 192 # can return negative values for wrong answers
187 def correct(self): 193 def correct(self):
188 super().correct() 194 super().correct()
@@ -207,7 +213,7 @@ class QuestionCheckbox(Question): @@ -207,7 +213,7 @@ class QuestionCheckbox(Question):
207 return self['grade'] 213 return self['grade']
208 214
209 215
210 -# =========================================================================== 216 +# ============================================================================
211 class QuestionText(Question): 217 class QuestionText(Question):
212 '''An instance of QuestionText will always have the keys: 218 '''An instance of QuestionText will always have the keys:
213 type (str) 219 type (str)
@@ -216,7 +222,7 @@ class QuestionText(Question): @@ -216,7 +222,7 @@ class QuestionText(Question):
216 answer (None or an actual answer) 222 answer (None or an actual answer)
217 ''' 223 '''
218 224
219 - #------------------------------------------------------------------------ 225 + # ------------------------------------------------------------------------
220 def __init__(self, q): 226 def __init__(self, q):
221 super().__init__(q) 227 super().__init__(q)
222 228
@@ -232,7 +238,7 @@ class QuestionText(Question): @@ -232,7 +238,7 @@ class QuestionText(Question):
232 # make sure all elements of the list are strings 238 # make sure all elements of the list are strings
233 self['correct'] = [str(a) for a in self['correct']] 239 self['correct'] = [str(a) for a in self['correct']]
234 240
235 - #------------------------------------------------------------------------ 241 + # ------------------------------------------------------------------------
236 # can return negative values for wrong answers 242 # can return negative values for wrong answers
237 def correct(self): 243 def correct(self):
238 super().correct() 244 super().correct()
@@ -252,7 +258,7 @@ class QuestionTextRegex(Question): @@ -252,7 +258,7 @@ class QuestionTextRegex(Question):
252 answer (None or an actual answer) 258 answer (None or an actual answer)
253 ''' 259 '''
254 260
255 - #------------------------------------------------------------------------ 261 + # ------------------------------------------------------------------------
256 def __init__(self, q): 262 def __init__(self, q):
257 super().__init__(q) 263 super().__init__(q)
258 264
@@ -261,15 +267,17 @@ class QuestionTextRegex(Question): @@ -261,15 +267,17 @@ class QuestionTextRegex(Question):
261 'correct': '$.^', # will always return false 267 'correct': '$.^', # will always return false
262 }) 268 })
263 269
264 - #------------------------------------------------------------------------ 270 + # ------------------------------------------------------------------------
265 # can return negative values for wrong answers 271 # can return negative values for wrong answers
266 def correct(self): 272 def correct(self):
267 super().correct() 273 super().correct()
268 if self['answer'] is not None: 274 if self['answer'] is not None:
269 try: 275 try:
270 - self['grade'] = 1.0 if re.match(self['correct'], self['answer']) else 0.0 276 + self['grade'] = 1.0 if re.match(self['correct'],
  277 + self['answer']) else 0.0
271 except TypeError: 278 except TypeError:
272 - logger.error('While matching regex {self["correct"]} with answer {self["answer"]}.') 279 + logger.error('While matching regex {self["correct"]} with '
  280 + 'answer {self["answer"]}.')
273 281
274 return self['grade'] 282 return self['grade']
275 283
@@ -284,7 +292,7 @@ class QuestionNumericInterval(Question): @@ -284,7 +292,7 @@ class QuestionNumericInterval(Question):
284 An answer is correct if it's in the closed interval. 292 An answer is correct if it's in the closed interval.
285 ''' 293 '''
286 294
287 - #------------------------------------------------------------------------ 295 + # ------------------------------------------------------------------------
288 def __init__(self, q): 296 def __init__(self, q):
289 super().__init__(q) 297 super().__init__(q)
290 298
@@ -293,7 +301,7 @@ class QuestionNumericInterval(Question): @@ -293,7 +301,7 @@ class QuestionNumericInterval(Question):
293 'correct': [1.0, -1.0], # will always return false 301 'correct': [1.0, -1.0], # will always return false
294 }) 302 })
295 303
296 - #------------------------------------------------------------------------ 304 + # ------------------------------------------------------------------------
297 # can return negative values for wrong answers 305 # can return negative values for wrong answers
298 def correct(self): 306 def correct(self):
299 super().correct() 307 super().correct()
@@ -321,7 +329,7 @@ class QuestionTextArea(Question): @@ -321,7 +329,7 @@ class QuestionTextArea(Question):
321 lines (int) 329 lines (int)
322 ''' 330 '''
323 331
324 - #------------------------------------------------------------------------ 332 + # ------------------------------------------------------------------------
325 def __init__(self, q): 333 def __init__(self, q):
326 super().__init__(q) 334 super().__init__(q)
327 335
@@ -332,33 +340,34 @@ class QuestionTextArea(Question): @@ -332,33 +340,34 @@ class QuestionTextArea(Question):
332 'correct': '' # trying to execute this will fail => grade 0.0 340 'correct': '' # trying to execute this will fail => grade 0.0
333 }) 341 })
334 342
335 - # self['correct'] = path.join(self['path'], self['correct'])  
336 - self['correct'] = path.abspath(path.normpath(path.join(self['path'], self['correct']))) 343 + self['correct'] = path.join(self['path'], self['correct'])
337 344
338 - #------------------------------------------------------------------------ 345 + # ------------------------------------------------------------------------
339 # can return negative values for wrong answers 346 # can return negative values for wrong answers
340 def correct(self): 347 def correct(self):
341 super().correct() 348 super().correct()
342 349
343 if self['answer'] is not None: 350 if self['answer'] is not None:
344 - # correct answer  
345 out = run_script( # and parse yaml ouput 351 out = run_script( # and parse yaml ouput
346 script=self['correct'], 352 script=self['correct'],
  353 + args=self['args'],
347 stdin=self['answer'], 354 stdin=self['answer'],
348 timeout=self['timeout'] 355 timeout=self['timeout']
349 ) 356 )
350 357
351 - if type(out) in (int, float):  
352 - self['grade'] = float(out)  
353 -  
354 - elif isinstance(out, dict): 358 + if isinstance(out, dict):
355 self['comments'] = out.get('comments', '') 359 self['comments'] = out.get('comments', '')
356 try: 360 try:
357 self['grade'] = float(out['grade']) 361 self['grade'] = float(out['grade'])
358 except ValueError: 362 except ValueError:
359 - logger.error(f'Correction script of "{self["ref"]}" returned nonfloat.') 363 + logger.error(f'Grade value error in "{self["correct"]}".')
360 except KeyError: 364 except KeyError:
361 - logger.error('Correction script of "{self["ref"]}" returned no "grade".') 365 + logger.error(f'No grade in "{self["correct"]}".')
  366 + else:
  367 + try:
  368 + self['grade'] = float(out)
  369 + except (TypeError, ValueError):
  370 + logger.error(f'Grade value error in "{self["correct"]}".')
362 371
363 return self['grade'] 372 return self['grade']
364 373
@@ -370,14 +379,14 @@ class QuestionInformation(Question): @@ -370,14 +379,14 @@ class QuestionInformation(Question):
370 text (str) 379 text (str)
371 points (0.0) 380 points (0.0)
372 ''' 381 '''
373 - #------------------------------------------------------------------------ 382 + # ------------------------------------------------------------------------
374 def __init__(self, q): 383 def __init__(self, q):
375 super().__init__(q) 384 super().__init__(q)
376 self.set_defaults({ 385 self.set_defaults({
377 'text': '', 386 'text': '',
378 }) 387 })
379 388
380 - #------------------------------------------------------------------------ 389 + # ------------------------------------------------------------------------
381 # can return negative values for wrong answers 390 # can return negative values for wrong answers
382 def correct(self): 391 def correct(self):
383 super().correct() 392 super().correct()
perguntations/serve.py
@@ -280,7 +280,8 @@ class ReviewHandler(BaseHandler): @@ -280,7 +280,8 @@ class ReviewHandler(BaseHandler):
280 else: 280 else:
281 with f: 281 with f:
282 t = json.load(f) 282 t = json.load(f)
283 - self.render('review.html', t=t, md=md_to_html, templ=self._templates) 283 + self.render('review.html', t=t, md=md_to_html,
  284 + templ=self._templates)
284 285
285 286
286 # --- ADMIN ------------------------------------------------------------------ 287 # --- ADMIN ------------------------------------------------------------------
@@ -406,7 +407,7 @@ def get_logger_config(debug=False): @@ -406,7 +407,7 @@ def get_logger_config(debug=False):
406 'level': level, 407 'level': level,
407 'propagate': False, 408 'propagate': False,
408 } for module in ['app', 'models', 'factory', 'questions', 409 } for module in ['app', 'models', 'factory', 'questions',
409 - 'test', 'tools', 'serve']}) 410 + 'test', 'tools']})
410 411
411 return load_yaml(config_file, default=default_config) 412 return load_yaml(config_file, default=default_config)
412 413
@@ -465,8 +466,8 @@ def main(): @@ -465,8 +466,8 @@ def main():
465 except ValueError: 466 except ValueError:
466 logging.critical('Certificates cert.pem, privkey.pem not found') 467 logging.critical('Certificates cert.pem, privkey.pem not found')
467 sys.exit(-1) 468 sys.exit(-1)
468 - else:  
469 - httpserver.listen(8443) 469 +
  470 + httpserver.listen(8443)
470 471
471 # --- run webserver 472 # --- run webserver
472 logging.info('Webserver running... (Ctrl-C to stop)') 473 logging.info('Webserver running... (Ctrl-C to stop)')
perguntations/tools.py
@@ -145,7 +145,7 @@ def md_to_html(qref='.'): @@ -145,7 +145,7 @@ def md_to_html(qref='.'):
145 def load_yaml(filename, default=None): 145 def load_yaml(filename, default=None):
146 try: 146 try:
147 with open(path.expanduser(filename), 'r', encoding='utf-8') as f: 147 with open(path.expanduser(filename), 'r', encoding='utf-8') as f:
148 - default = yaml.load(f) 148 + default = yaml.safe_load(f)
149 except FileNotFoundError: 149 except FileNotFoundError:
150 logger.error(f'File not found: "{filename}".') 150 logger.error(f'File not found: "{filename}".')
151 except PermissionError: 151 except PermissionError:
@@ -164,10 +164,11 @@ def load_yaml(filename, default=None): @@ -164,10 +164,11 @@ def load_yaml(filename, default=None):
164 # The script is run in another process but this function blocks waiting 164 # The script is run in another process but this function blocks waiting
165 # for its termination. 165 # for its termination.
166 # --------------------------------------------------------------------------- 166 # ---------------------------------------------------------------------------
167 -def run_script(script, stdin='', timeout=5): 167 +def run_script(script, args=[], stdin='', timeout=5):
168 script = path.expanduser(script) 168 script = path.expanduser(script)
169 try: 169 try:
170 - p = subprocess.run([script], 170 + cmd = [script] + [str(a) for a in args]
  171 + p = subprocess.run(cmd,
171 input=stdin, 172 input=stdin,
172 stdout=subprocess.PIPE, 173 stdout=subprocess.PIPE,
173 stderr=subprocess.STDOUT, 174 stderr=subprocess.STDOUT,
@@ -187,7 +188,7 @@ def run_script(script, stdin='', timeout=5): @@ -187,7 +188,7 @@ def run_script(script, stdin='', timeout=5):
187 logger.error(f'Script "{script}" returned code {p.returncode}') 188 logger.error(f'Script "{script}" returned code {p.returncode}')
188 else: 189 else:
189 try: 190 try:
190 - output = yaml.load(p.stdout) 191 + output = yaml.safe_load(p.stdout)
191 except Exception: 192 except Exception:
192 logger.error(f'Error parsing yaml output of "{script}"') 193 logger.error(f'Error parsing yaml output of "{script}"')
193 else: 194 else: