Commit 9d2af8636c907decb9f2b8726e0c64b5fdb3c8b7
1 parent
50eac3c3
Exists in
master
and in
1 other branch
Allow generator to send args to correct script.
fix yaml.load() to yaml.safe_load() as per deprecation warning. fix flake8 errors
Showing
8 changed files
with
121 additions
and
92 deletions
Show diff stats
BUGS.md
... | ... | @@ -8,7 +8,6 @@ e.g. retornar None quando nao ha alteracoes relativamente à última vez. |
8 | 8 | ou usar push (websockets?) |
9 | 9 | - pymips: nao pode executar syscalls do spim. |
10 | 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 | 11 | - eventos unfocus? |
13 | 12 | - servidor nao esta a lidar com eventos scroll/resize. ignorar? |
14 | 13 | - Test.reset_answers() unused. |
... | ... | @@ -62,6 +61,8 @@ ou usar push (websockets?) |
62 | 61 | |
63 | 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 | 66 | - usar npm para instalar javascript |
66 | 67 | - se aluno entrar com l12345 rebenta. numa funcao get_... ver imagem no ipad |
67 | 68 | - no test3 está contar 1.0 valores numa pergunta do tipo info? acontece para type: info, e não para type: information | ... | ... |
README.md
... | ... | @@ -149,7 +149,7 @@ Instead, tcp traffic can be forwarded from port 443 to 8443 where the server is |
149 | 149 | listening. |
150 | 150 | The details depend on the operating system/firewall. |
151 | 151 | |
152 | -### debian: | |
152 | +### debian | |
153 | 153 | |
154 | 154 | FIXME: Untested |
155 | 155 | |
... | ... | @@ -226,34 +226,34 @@ directory. |
226 | 226 | |
227 | 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 | 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 | 233 | terminal is not supporting UTF-8. |
234 | 234 | This error may occur when a unicode character is printed to the screen by the |
235 | 235 | server or, when running question generator or correction scripts, a message is |
236 | 236 | piped between the server and the scripts that includes unicode characters. |
237 | 237 | Try running `locale` on the terminal and see if there are any error messages. |
238 | 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 | 32 | hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, password) |
33 | 33 | return password == hashed |
34 | 34 | |
35 | + | |
35 | 36 | async def hash_password(pw): |
36 | 37 | pw = pw.encode('utf-8') |
37 | 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 | 111 | |
110 | 112 | # get name+password from db |
111 | 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 | 118 | # first login updates the password |
115 | 119 | if password == '': # update password on first login |
... | ... | @@ -140,7 +144,8 @@ class App(object): |
140 | 144 | if uid in self.online: |
141 | 145 | logger.info(f'Student {uid}: started generating new test.') |
142 | 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 | 149 | logger.debug(f'Student {uid}: finished generating test.') |
145 | 150 | return self.online[uid]['test'] |
146 | 151 | else: |
... | ... | @@ -156,7 +161,8 @@ class App(object): |
156 | 161 | grade = await t.correct() |
157 | 162 | |
158 | 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 | 166 | fpath = path.join(t['answers_dir'], fname) |
161 | 167 | t.save_json(fpath) |
162 | 168 | |
... | ... | @@ -189,9 +195,8 @@ class App(object): |
189 | 195 | t.giveup() |
190 | 196 | |
191 | 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 | 200 | fpath = path.join(t['answers_dir'], fname) |
196 | 201 | t.save_json(fpath) |
197 | 202 | |
... | ... | @@ -225,22 +230,28 @@ class App(object): |
225 | 230 | |
226 | 231 | def get_student_grades_from_all_tests(self, uid): |
227 | 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 | 237 | def get_json_filename_of_test(self, test_id): |
231 | 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 | 243 | def get_all_students(self): |
238 | 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 | 249 | def get_student_grades_from_test(self, uid, testid): |
242 | 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 | 256 | def get_students_state(self): |
246 | 257 | return [{ |
... | ... | @@ -248,7 +259,8 @@ class App(object): |
248 | 259 | 'name': name, |
249 | 260 | 'allowed': uid in self.allowed, |
250 | 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 | 264 | 'password_defined': pw != '', |
253 | 265 | 'grades': self.get_student_grades_from_test(uid, self.testfactory['ref']), |
254 | 266 | # 'focus': self.online.get(uid, {}).get('student', {}).get('focus', True), # FIXME | ... | ... |
perguntations/factory.py
... | ... | @@ -32,9 +32,10 @@ import logging |
32 | 32 | |
33 | 33 | # this project |
34 | 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 | 40 | # setup logger for this module |
40 | 41 | logger = logging.getLogger(__name__) |
... | ... | @@ -73,16 +74,18 @@ class QuestionFactory(dict): |
73 | 74 | # After this, each question will have at least 'ref' and 'type' keys. |
74 | 75 | # ------------------------------------------------------------------------ |
75 | 76 | def add_question(self, question): |
77 | + q = question | |
78 | + | |
76 | 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 | 91 | # load single YAML questions file |
... | ... | @@ -140,10 +143,11 @@ class QuestionFactory(dict): |
140 | 143 | # which will print a valid question in yaml format to stdout. This |
141 | 144 | # output is then converted to a dictionary and `q` becomes that dict. |
142 | 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 | 151 | try: |
148 | 152 | q.update(out) |
149 | 153 | except Exception: |
... | ... | @@ -152,6 +156,7 @@ class QuestionFactory(dict): |
152 | 156 | 'title': 'Erro interno', |
153 | 157 | 'text': 'Ocorreu um erro a gerar esta pergunta.' |
154 | 158 | }) |
159 | + | |
155 | 160 | # The generator was replaced by a question but not yet instantiated |
156 | 161 | |
157 | 162 | # Finally we create an instance of Question() | ... | ... |
perguntations/initdb.py
... | ... | @@ -181,7 +181,7 @@ def main(): |
181 | 181 | for s in args.update: |
182 | 182 | print(f'Updating password of: {s}') |
183 | 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 | 185 | u.password = bcrypt.hashpw(pw, bcrypt.gensalt()) |
186 | 186 | session.commit() |
187 | 187 | ... | ... |
perguntations/questions.py
... | ... | @@ -57,7 +57,7 @@ class Question(dict): |
57 | 57 | |
58 | 58 | def set_defaults(self, d): |
59 | 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 | 61 | self.setdefault(k, v) |
62 | 62 | |
63 | 63 | |
... | ... | @@ -74,7 +74,7 @@ class QuestionRadio(Question): |
74 | 74 | choose (int) # only used if shuffle=True |
75 | 75 | ''' |
76 | 76 | |
77 | - #------------------------------------------------------------------------ | |
77 | + # ------------------------------------------------------------------------ | |
78 | 78 | def __init__(self, q): |
79 | 79 | super().__init__(q) |
80 | 80 | |
... | ... | @@ -91,7 +91,12 @@ class QuestionRadio(Question): |
91 | 91 | # always convert to list, e.g. correct: 2 --> correct: [0,0,1,0,0] |
92 | 92 | # correctness levels from 0.0 to 1.0 (no discount here!) |
93 | 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 | 101 | if self['shuffle']: |
97 | 102 | # separate right from wrong options |
... | ... | @@ -102,8 +107,8 @@ class QuestionRadio(Question): |
102 | 107 | |
103 | 108 | # choose 1 correct option |
104 | 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 | 113 | # choose remaining wrong options |
109 | 114 | random.shuffle(wrong) |
... | ... | @@ -113,10 +118,10 @@ class QuestionRadio(Question): |
113 | 118 | |
114 | 119 | # final shuffle of the options |
115 | 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 | 125 | # can return negative values for wrong answers |
121 | 126 | def correct(self): |
122 | 127 | super().correct() |
... | ... | @@ -145,7 +150,7 @@ class QuestionCheckbox(Question): |
145 | 150 | answer (None or an actual answer) |
146 | 151 | ''' |
147 | 152 | |
148 | - #------------------------------------------------------------------------ | |
153 | + # ------------------------------------------------------------------------ | |
149 | 154 | def __init__(self, q): |
150 | 155 | super().__init__(q) |
151 | 156 | |
... | ... | @@ -154,14 +159,15 @@ class QuestionCheckbox(Question): |
154 | 159 | # set defaults if missing |
155 | 160 | self.set_defaults({ |
156 | 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 | 163 | 'shuffle': True, |
159 | 164 | 'discount': True, |
160 | - 'choose': n, # number of options | |
165 | + 'choose': n, # number of options | |
161 | 166 | }) |
162 | 167 | |
163 | 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 | 172 | # if an option is a list of (right, wrong), pick one |
167 | 173 | # FIXME it's possible that all options are chosen wrong |
... | ... | @@ -169,9 +175,9 @@ class QuestionCheckbox(Question): |
169 | 175 | correct = [] |
170 | 176 | for o, c in zip(self['options'], self['correct']): |
171 | 177 | if isinstance(o, list): |
172 | - r = random.randint(0,1) | |
178 | + r = random.randint(0, 1) | |
173 | 179 | o = o[r] |
174 | - c = c if r==0 else -c | |
180 | + c = c if r == 0 else -c | |
175 | 181 | options.append(str(o)) |
176 | 182 | correct.append(float(c)) |
177 | 183 | |
... | ... | @@ -182,7 +188,7 @@ class QuestionCheckbox(Question): |
182 | 188 | self['options'] = [options[i] for i in perm] |
183 | 189 | self['correct'] = [correct[i] for i in perm] |
184 | 190 | |
185 | - #------------------------------------------------------------------------ | |
191 | + # ------------------------------------------------------------------------ | |
186 | 192 | # can return negative values for wrong answers |
187 | 193 | def correct(self): |
188 | 194 | super().correct() |
... | ... | @@ -207,7 +213,7 @@ class QuestionCheckbox(Question): |
207 | 213 | return self['grade'] |
208 | 214 | |
209 | 215 | |
210 | -# =========================================================================== | |
216 | +# ============================================================================ | |
211 | 217 | class QuestionText(Question): |
212 | 218 | '''An instance of QuestionText will always have the keys: |
213 | 219 | type (str) |
... | ... | @@ -216,7 +222,7 @@ class QuestionText(Question): |
216 | 222 | answer (None or an actual answer) |
217 | 223 | ''' |
218 | 224 | |
219 | - #------------------------------------------------------------------------ | |
225 | + # ------------------------------------------------------------------------ | |
220 | 226 | def __init__(self, q): |
221 | 227 | super().__init__(q) |
222 | 228 | |
... | ... | @@ -232,7 +238,7 @@ class QuestionText(Question): |
232 | 238 | # make sure all elements of the list are strings |
233 | 239 | self['correct'] = [str(a) for a in self['correct']] |
234 | 240 | |
235 | - #------------------------------------------------------------------------ | |
241 | + # ------------------------------------------------------------------------ | |
236 | 242 | # can return negative values for wrong answers |
237 | 243 | def correct(self): |
238 | 244 | super().correct() |
... | ... | @@ -252,7 +258,7 @@ class QuestionTextRegex(Question): |
252 | 258 | answer (None or an actual answer) |
253 | 259 | ''' |
254 | 260 | |
255 | - #------------------------------------------------------------------------ | |
261 | + # ------------------------------------------------------------------------ | |
256 | 262 | def __init__(self, q): |
257 | 263 | super().__init__(q) |
258 | 264 | |
... | ... | @@ -261,15 +267,17 @@ class QuestionTextRegex(Question): |
261 | 267 | 'correct': '$.^', # will always return false |
262 | 268 | }) |
263 | 269 | |
264 | - #------------------------------------------------------------------------ | |
270 | + # ------------------------------------------------------------------------ | |
265 | 271 | # can return negative values for wrong answers |
266 | 272 | def correct(self): |
267 | 273 | super().correct() |
268 | 274 | if self['answer'] is not None: |
269 | 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 | 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 | 282 | return self['grade'] |
275 | 283 | |
... | ... | @@ -284,7 +292,7 @@ class QuestionNumericInterval(Question): |
284 | 292 | An answer is correct if it's in the closed interval. |
285 | 293 | ''' |
286 | 294 | |
287 | - #------------------------------------------------------------------------ | |
295 | + # ------------------------------------------------------------------------ | |
288 | 296 | def __init__(self, q): |
289 | 297 | super().__init__(q) |
290 | 298 | |
... | ... | @@ -293,7 +301,7 @@ class QuestionNumericInterval(Question): |
293 | 301 | 'correct': [1.0, -1.0], # will always return false |
294 | 302 | }) |
295 | 303 | |
296 | - #------------------------------------------------------------------------ | |
304 | + # ------------------------------------------------------------------------ | |
297 | 305 | # can return negative values for wrong answers |
298 | 306 | def correct(self): |
299 | 307 | super().correct() |
... | ... | @@ -321,7 +329,7 @@ class QuestionTextArea(Question): |
321 | 329 | lines (int) |
322 | 330 | ''' |
323 | 331 | |
324 | - #------------------------------------------------------------------------ | |
332 | + # ------------------------------------------------------------------------ | |
325 | 333 | def __init__(self, q): |
326 | 334 | super().__init__(q) |
327 | 335 | |
... | ... | @@ -332,33 +340,34 @@ class QuestionTextArea(Question): |
332 | 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 | 346 | # can return negative values for wrong answers |
340 | 347 | def correct(self): |
341 | 348 | super().correct() |
342 | 349 | |
343 | 350 | if self['answer'] is not None: |
344 | - # correct answer | |
345 | 351 | out = run_script( # and parse yaml ouput |
346 | 352 | script=self['correct'], |
353 | + args=self['args'], | |
347 | 354 | stdin=self['answer'], |
348 | 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 | 359 | self['comments'] = out.get('comments', '') |
356 | 360 | try: |
357 | 361 | self['grade'] = float(out['grade']) |
358 | 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 | 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 | 372 | return self['grade'] |
364 | 373 | |
... | ... | @@ -370,14 +379,14 @@ class QuestionInformation(Question): |
370 | 379 | text (str) |
371 | 380 | points (0.0) |
372 | 381 | ''' |
373 | - #------------------------------------------------------------------------ | |
382 | + # ------------------------------------------------------------------------ | |
374 | 383 | def __init__(self, q): |
375 | 384 | super().__init__(q) |
376 | 385 | self.set_defaults({ |
377 | 386 | 'text': '', |
378 | 387 | }) |
379 | 388 | |
380 | - #------------------------------------------------------------------------ | |
389 | + # ------------------------------------------------------------------------ | |
381 | 390 | # can return negative values for wrong answers |
382 | 391 | def correct(self): |
383 | 392 | super().correct() | ... | ... |
perguntations/serve.py
... | ... | @@ -280,7 +280,8 @@ class ReviewHandler(BaseHandler): |
280 | 280 | else: |
281 | 281 | with f: |
282 | 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 | 287 | # --- ADMIN ------------------------------------------------------------------ |
... | ... | @@ -406,7 +407,7 @@ def get_logger_config(debug=False): |
406 | 407 | 'level': level, |
407 | 408 | 'propagate': False, |
408 | 409 | } for module in ['app', 'models', 'factory', 'questions', |
409 | - 'test', 'tools', 'serve']}) | |
410 | + 'test', 'tools']}) | |
410 | 411 | |
411 | 412 | return load_yaml(config_file, default=default_config) |
412 | 413 | |
... | ... | @@ -465,8 +466,8 @@ def main(): |
465 | 466 | except ValueError: |
466 | 467 | logging.critical('Certificates cert.pem, privkey.pem not found') |
467 | 468 | sys.exit(-1) |
468 | - else: | |
469 | - httpserver.listen(8443) | |
469 | + | |
470 | + httpserver.listen(8443) | |
470 | 471 | |
471 | 472 | # --- run webserver |
472 | 473 | logging.info('Webserver running... (Ctrl-C to stop)') | ... | ... |
perguntations/tools.py
... | ... | @@ -145,7 +145,7 @@ def md_to_html(qref='.'): |
145 | 145 | def load_yaml(filename, default=None): |
146 | 146 | try: |
147 | 147 | with open(path.expanduser(filename), 'r', encoding='utf-8') as f: |
148 | - default = yaml.load(f) | |
148 | + default = yaml.safe_load(f) | |
149 | 149 | except FileNotFoundError: |
150 | 150 | logger.error(f'File not found: "{filename}".') |
151 | 151 | except PermissionError: |
... | ... | @@ -164,10 +164,11 @@ def load_yaml(filename, default=None): |
164 | 164 | # The script is run in another process but this function blocks waiting |
165 | 165 | # for its termination. |
166 | 166 | # --------------------------------------------------------------------------- |
167 | -def run_script(script, stdin='', timeout=5): | |
167 | +def run_script(script, args=[], stdin='', timeout=5): | |
168 | 168 | script = path.expanduser(script) |
169 | 169 | try: |
170 | - p = subprocess.run([script], | |
170 | + cmd = [script] + [str(a) for a in args] | |
171 | + p = subprocess.run(cmd, | |
171 | 172 | input=stdin, |
172 | 173 | stdout=subprocess.PIPE, |
173 | 174 | stderr=subprocess.STDOUT, |
... | ... | @@ -187,7 +188,7 @@ def run_script(script, stdin='', timeout=5): |
187 | 188 | logger.error(f'Script "{script}" returned code {p.returncode}') |
188 | 189 | else: |
189 | 190 | try: |
190 | - output = yaml.load(p.stdout) | |
191 | + output = yaml.safe_load(p.stdout) | |
191 | 192 | except Exception: |
192 | 193 | logger.error(f'Error parsing yaml output of "{script}"') |
193 | 194 | else: | ... | ... |