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,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 |
README.md
@@ -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: |