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: | ... | ... |