Commit 0dff80b71929dd791c669021ba089746487b8d4b
1 parent
1200ef9c
Exists in
master
and in
1 other branch
multiple changes:
- update .gitignore - remove explicit popper.js and use bootstrap.bundle instead - update questions.py to match aprendizations
Showing
10 changed files
with
105 additions
and
55 deletions
Show diff stats
.gitignore
demo/questions/questions-tutorial.yaml
| ... | ... | @@ -227,8 +227,8 @@ |
| 227 | 227 | |
| 228 | 228 | ```yaml |
| 229 | 229 | options: |
| 230 | - - ["O céu é azul", "O céu não é azul"] | |
| 231 | - - ["Um triangulo tem 3 lados", "Um triangulo tem 2 lados"] | |
| 230 | + - ['O céu é azul', 'O céu não é azul'] | |
| 231 | + - ['Um triangulo tem 3 lados', 'Um triangulo tem 2 lados'] | |
| 232 | 232 | - O nosso planeta tem um satélite natural |
| 233 | 233 | correct: [1, 1, 1] |
| 234 | 234 | ``` | ... | ... |
package.json
| ... | ... | @@ -3,12 +3,11 @@ |
| 3 | 3 | "email": "mjsb@uevora.pt", |
| 4 | 4 | "dependencies": { |
| 5 | 5 | "@fortawesome/fontawesome-free": "^5.15.1", |
| 6 | - "bootstrap": "^4.5.3", | |
| 6 | + "bootstrap": "^4.5", | |
| 7 | 7 | "codemirror": "^5.58.1", |
| 8 | 8 | "datatables": "^1.10", |
| 9 | 9 | "jquery": "^3.5.1", |
| 10 | 10 | "mathjax": "^3.1.2", |
| 11 | - "popper.js": "^1.16.1", | |
| 12 | 11 | "underscore": "^1.11.0" |
| 13 | 12 | } |
| 14 | 13 | } | ... | ... |
perguntations/app.py
| ... | ... | @@ -105,9 +105,9 @@ class App(): |
| 105 | 105 | logger.info('Database "%s" has %s students.', dbfile, num) |
| 106 | 106 | |
| 107 | 107 | # pre-generate tests |
| 108 | - logger.info('Generating tests for %d students:', num) | |
| 108 | + logger.info('Generating tests for %d students...', num) | |
| 109 | 109 | self._pregenerate_tests(num) |
| 110 | - logger.info('Tests are ready.') | |
| 110 | + logger.info('Tests done.') | |
| 111 | 111 | |
| 112 | 112 | # command line option --allow-all |
| 113 | 113 | if conf['allow_all']: | ... | ... |
perguntations/questions.py
| ... | ... | @@ -5,10 +5,11 @@ Classes the implement several types of questions. |
| 5 | 5 | |
| 6 | 6 | # python standard library |
| 7 | 7 | import asyncio |
| 8 | +from datetime import datetime | |
| 8 | 9 | import logging |
| 10 | +from os import path | |
| 9 | 11 | import random |
| 10 | 12 | import re |
| 11 | -from os import path | |
| 12 | 13 | from typing import Any, Dict, NewType |
| 13 | 14 | import uuid |
| 14 | 15 | |
| ... | ... | @@ -48,6 +49,11 @@ class Question(dict): |
| 48 | 49 | 'files': {}, |
| 49 | 50 | })) |
| 50 | 51 | |
| 52 | + def set_answer(self, ans) -> None: | |
| 53 | + '''set answer field and register time''' | |
| 54 | + self['answer'] = ans | |
| 55 | + self['finish_time'] = datetime.now() | |
| 56 | + | |
| 51 | 57 | def correct(self) -> None: |
| 52 | 58 | '''default correction (synchronous version)''' |
| 53 | 59 | self['comments'] = '' |
| ... | ... | @@ -80,7 +86,16 @@ class QuestionRadio(Question): |
| 80 | 86 | def __init__(self, q: QDict) -> None: |
| 81 | 87 | super().__init__(q) |
| 82 | 88 | |
| 83 | - nopts = len(self['options']) | |
| 89 | + try: | |
| 90 | + nopts = len(self['options']) | |
| 91 | + except KeyError as exc: | |
| 92 | + msg = f'Missing `options`. In question "{self["ref"]}"' | |
| 93 | + logger.error(msg) | |
| 94 | + raise QuestionException(msg) from exc | |
| 95 | + except TypeError as exc: | |
| 96 | + msg = f'`options` must be a list. In question "{self["ref"]}"' | |
| 97 | + logger.error(msg) | |
| 98 | + raise QuestionException(msg) from exc | |
| 84 | 99 | |
| 85 | 100 | self.set_defaults(QDict({ |
| 86 | 101 | 'text': '', |
| ... | ... | @@ -94,8 +109,9 @@ class QuestionRadio(Question): |
| 94 | 109 | # e.g. correct: 2 --> correct: [0,0,1,0,0] |
| 95 | 110 | if isinstance(self['correct'], int): |
| 96 | 111 | if not 0 <= self['correct'] < nopts: |
| 97 | - msg = (f'Correct option not in range 0..{nopts-1} in ' | |
| 98 | - f'"{self["ref"]}"') | |
| 112 | + msg = (f'`correct` out of range 0..{nopts-1}. ' | |
| 113 | + f'In question "{self["ref"]}"') | |
| 114 | + logger.error(msg) | |
| 99 | 115 | raise QuestionException(msg) |
| 100 | 116 | |
| 101 | 117 | self['correct'] = [1.0 if x == self['correct'] else 0.0 |
| ... | ... | @@ -104,28 +120,33 @@ class QuestionRadio(Question): |
| 104 | 120 | elif isinstance(self['correct'], list): |
| 105 | 121 | # must match number of options |
| 106 | 122 | if len(self['correct']) != nopts: |
| 107 | - msg = (f'Incompatible sizes: {nopts} options vs ' | |
| 108 | - f'{len(self["correct"])} correct in "{self["ref"]}"') | |
| 123 | + msg = (f'{nopts} options vs {len(self["correct"])} correct. ' | |
| 124 | + f'In question "{self["ref"]}"') | |
| 125 | + logger.error(msg) | |
| 109 | 126 | raise QuestionException(msg) |
| 127 | + | |
| 110 | 128 | # make sure is a list of floats |
| 111 | 129 | try: |
| 112 | 130 | self['correct'] = [float(x) for x in self['correct']] |
| 113 | 131 | except (ValueError, TypeError) as exc: |
| 114 | - msg = (f'Correct list must contain numbers [0.0, 1.0] or ' | |
| 115 | - f'booleans in "{self["ref"]}"') | |
| 132 | + msg = ('`correct` must be list of numbers or booleans.' | |
| 133 | + f'In "{self["ref"]}"') | |
| 134 | + logger.error(msg) | |
| 116 | 135 | raise QuestionException(msg) from exc |
| 117 | 136 | |
| 118 | 137 | # check grade boundaries |
| 119 | 138 | if self['discount'] and not all(0.0 <= x <= 1.0 |
| 120 | 139 | for x in self['correct']): |
| 121 | - msg = (f'Correct values must be in the interval [0.0, 1.0] in ' | |
| 122 | - f'"{self["ref"]}"') | |
| 140 | + msg = ('`correct` values must be in the interval [0.0, 1.0]. ' | |
| 141 | + f'In "{self["ref"]}"') | |
| 142 | + logger.error(msg) | |
| 123 | 143 | raise QuestionException(msg) |
| 124 | 144 | |
| 125 | 145 | # at least one correct option |
| 126 | 146 | if all(x < 1.0 for x in self['correct']): |
| 127 | - msg = (f'At least one correct option is required in ' | |
| 128 | - f'"{self["ref"]}"') | |
| 147 | + msg = ('At least one correct option is required. ' | |
| 148 | + f'In "{self["ref"]}"') | |
| 149 | + logger.error(msg) | |
| 129 | 150 | raise QuestionException(msg) |
| 130 | 151 | |
| 131 | 152 | # If shuffle==false, all options are shown as defined |
| ... | ... | @@ -158,14 +179,17 @@ class QuestionRadio(Question): |
| 158 | 179 | self['correct'] = [correct[i] for i in perm] |
| 159 | 180 | |
| 160 | 181 | # ------------------------------------------------------------------------ |
| 161 | - # can assign negative grades for wrong answers | |
| 162 | 182 | def correct(self) -> None: |
| 183 | + ''' | |
| 184 | + Correct `answer` and set `grade`. | |
| 185 | + Can assign negative grades for wrong answers | |
| 186 | + ''' | |
| 163 | 187 | super().correct() |
| 164 | 188 | |
| 165 | 189 | if self['answer'] is not None: |
| 166 | 190 | grade = self['correct'][int(self['answer'])] # grade of the answer |
| 167 | 191 | nopts = len(self['options']) |
| 168 | - grade_aver = sum(self['correct']) / nopts # expected value | |
| 192 | + grade_aver = sum(self['correct']) / nopts # expected value | |
| 169 | 193 | |
| 170 | 194 | # note: there are no numerical errors when summing 1.0s so the |
| 171 | 195 | # x_aver can be exactly 1.0 if all options are right |
| ... | ... | @@ -191,7 +215,16 @@ class QuestionCheckbox(Question): |
| 191 | 215 | def __init__(self, q: QDict) -> None: |
| 192 | 216 | super().__init__(q) |
| 193 | 217 | |
| 194 | - nopts = len(self['options']) | |
| 218 | + try: | |
| 219 | + nopts = len(self['options']) | |
| 220 | + except KeyError as exc: | |
| 221 | + msg = f'Missing `options`. In question "{self["ref"]}"' | |
| 222 | + logger.error(msg) | |
| 223 | + raise QuestionException(msg) from exc | |
| 224 | + except TypeError as exc: | |
| 225 | + msg = f'`options` must be a list. In question "{self["ref"]}"' | |
| 226 | + logger.error(msg) | |
| 227 | + raise QuestionException(msg) from exc | |
| 195 | 228 | |
| 196 | 229 | # set defaults if missing |
| 197 | 230 | self.set_defaults(QDict({ |
| ... | ... | @@ -199,47 +232,53 @@ class QuestionCheckbox(Question): |
| 199 | 232 | 'correct': [1.0] * nopts, # Using 0.0 breaks (right, wrong) |
| 200 | 233 | 'shuffle': True, |
| 201 | 234 | 'discount': True, |
| 202 | - 'choose': nopts, # number of options | |
| 235 | + 'choose': nopts, # number of options | |
| 203 | 236 | 'max_tries': max(1, min(nopts - 1, 3)) |
| 204 | 237 | })) |
| 205 | 238 | |
| 206 | 239 | # must be a list of numbers |
| 207 | 240 | if not isinstance(self['correct'], list): |
| 208 | 241 | msg = 'Correct must be a list of numbers or booleans' |
| 242 | + logger.error(msg) | |
| 209 | 243 | raise QuestionException(msg) |
| 210 | 244 | |
| 211 | 245 | # must match number of options |
| 212 | 246 | if len(self['correct']) != nopts: |
| 213 | - msg = (f'Incompatible sizes: {nopts} options vs ' | |
| 214 | - f'{len(self["correct"])} correct in "{self["ref"]}"') | |
| 247 | + msg = (f'{nopts} options vs {len(self["correct"])} correct. ' | |
| 248 | + f'In question "{self["ref"]}"') | |
| 249 | + logger.error(msg) | |
| 215 | 250 | raise QuestionException(msg) |
| 216 | 251 | |
| 217 | 252 | # make sure is a list of floats |
| 218 | 253 | try: |
| 219 | 254 | self['correct'] = [float(x) for x in self['correct']] |
| 220 | 255 | except (ValueError, TypeError) as exc: |
| 221 | - msg = (f'Correct list must contain numbers or ' | |
| 222 | - f'booleans in "{self["ref"]}"') | |
| 256 | + msg = ('`correct` must be list of numbers or booleans.' | |
| 257 | + f'In "{self["ref"]}"') | |
| 258 | + logger.error(msg) | |
| 223 | 259 | raise QuestionException(msg) from exc |
| 224 | 260 | |
| 225 | 261 | # check grade boundaries |
| 226 | 262 | if self['discount'] and not all(0.0 <= x <= 1.0 |
| 227 | 263 | for x in self['correct']): |
| 228 | - | |
| 229 | - msg0 = ('+--------------- BEHAVIOR CHANGE NOTICE ---------------+') | |
| 230 | - msg1 = ('| Correct values in checkbox questions must be in the |') | |
| 231 | - msg2 = ('| interval [0.0, 1.0]. I will convert to the new |') | |
| 232 | - msg3 = ('| behavior, for now, but you should fix it. |') | |
| 233 | - msg4 = ('+------------------------------------------------------+') | |
| 234 | - logger.warning(msg0) | |
| 235 | - logger.warning(msg1) | |
| 236 | - logger.warning(msg2) | |
| 237 | - logger.warning(msg3) | |
| 238 | - logger.warning(msg4) | |
| 239 | - logger.warning('please fix "%s"', self["ref"]) | |
| 240 | - | |
| 241 | - # normalize to [0,1] | |
| 242 | - self['correct'] = [(x+1)/2 for x in self['correct']] | |
| 264 | + msg = ('values in the `correct` field of checkboxes must be in ' | |
| 265 | + 'the [0.0, 1.0] interval. ' | |
| 266 | + f'Please fix "{self["ref"]}" in "{self["path"]}"') | |
| 267 | + logger.error(msg) | |
| 268 | + raise QuestionException(msg) | |
| 269 | + # msg0 = ('+--------------- BEHAVIOR CHANGE NOTICE ---------------+') | |
| 270 | + # msg1 = ('| Correct values in checkbox questions must be in the |') | |
| 271 | + # msg2 = ('| interval [0.0, 1.0]. I will convert to the new |') | |
| 272 | + # msg3 = ('| behavior, for now, but you should fix it. |') | |
| 273 | + # msg4 = ('+------------------------------------------------------+') | |
| 274 | + # logger.warning(msg0) | |
| 275 | + # logger.warning(msg1) | |
| 276 | + # logger.warning(msg2) | |
| 277 | + # logger.warning(msg3) | |
| 278 | + # logger.warning(msg4) | |
| 279 | + # logger.warning('please fix "%s"', self["ref"]) | |
| 280 | + # # normalize to [0,1] | |
| 281 | + # self['correct'] = [(x+1)/2 for x in self['correct']] | |
| 243 | 282 | |
| 244 | 283 | # if an option is a list of (right, wrong), pick one |
| 245 | 284 | options = [] |
| ... | ... | @@ -381,6 +420,7 @@ class QuestionTextRegex(Question): |
| 381 | 420 | self['correct'] = [re.compile(a) for a in self['correct']] |
| 382 | 421 | except Exception as exc: |
| 383 | 422 | msg = f'Failed to compile regex in "{self["ref"]}"' |
| 423 | + logger.error(msg) | |
| 384 | 424 | raise QuestionException(msg) from exc |
| 385 | 425 | |
| 386 | 426 | # ------------------------------------------------------------------------ |
| ... | ... | @@ -426,6 +466,7 @@ class QuestionNumericInterval(Question): |
| 426 | 466 | if len(self['correct']) != 2: |
| 427 | 467 | msg = (f'Numeric interval must be a list with two numbers, in ' |
| 428 | 468 | f'{self["ref"]}') |
| 469 | + logger.error(msg) | |
| 429 | 470 | raise QuestionException(msg) |
| 430 | 471 | |
| 431 | 472 | try: |
| ... | ... | @@ -433,12 +474,14 @@ class QuestionNumericInterval(Question): |
| 433 | 474 | except Exception as exc: |
| 434 | 475 | msg = (f'Numeric interval must be a list with two numbers, in ' |
| 435 | 476 | f'{self["ref"]}') |
| 477 | + logger.error(msg) | |
| 436 | 478 | raise QuestionException(msg) from exc |
| 437 | 479 | |
| 438 | 480 | # invalid |
| 439 | 481 | else: |
| 440 | 482 | msg = (f'Numeric interval must be a list with two numbers, in ' |
| 441 | 483 | f'{self["ref"]}') |
| 484 | + logger.error(msg) | |
| 442 | 485 | raise QuestionException(msg) |
| 443 | 486 | |
| 444 | 487 | # ------------------------------------------------------------------------ |
| ... | ... | @@ -629,11 +672,12 @@ class QFactory(): |
| 629 | 672 | # which will print a valid question in yaml format to stdout. This |
| 630 | 673 | # output is then yaml parsed into a dictionary `q`. |
| 631 | 674 | if question['type'] == 'generator': |
| 632 | - logger.debug(' \\_ Running "%s".', question["script"]) | |
| 675 | + logger.debug(' \\_ Running "%s".', question['script']) | |
| 633 | 676 | question.setdefault('args', []) |
| 634 | 677 | question.setdefault('stdin', '') |
| 635 | 678 | script = path.join(question['path'], question['script']) |
| 636 | - out = await run_script_async(script=script, args=question['args'], | |
| 679 | + out = await run_script_async(script=script, | |
| 680 | + args=question['args'], | |
| 637 | 681 | stdin=question['stdin']) |
| 638 | 682 | question.update(out) |
| 639 | 683 | |
| ... | ... | @@ -642,14 +686,17 @@ class QFactory(): |
| 642 | 686 | qclass = self._types[question['type']] |
| 643 | 687 | except KeyError: |
| 644 | 688 | logger.error('Invalid type "%s" in "%s"', |
| 645 | - question["type"], question["ref"]) | |
| 689 | + question['type'], question['ref']) | |
| 646 | 690 | raise |
| 647 | 691 | |
| 648 | 692 | # Finally create an instance of Question() |
| 649 | 693 | try: |
| 650 | 694 | qinstance = qclass(QDict(question)) |
| 651 | 695 | except QuestionException: |
| 652 | - logger.error('Error generating question %s', question['ref']) | |
| 696 | + logger.error('Error generating question "%s". See "%s/%s"', | |
| 697 | + question['ref'], | |
| 698 | + question['path'], | |
| 699 | + question['filename']) | |
| 653 | 700 | raise |
| 654 | 701 | |
| 655 | 702 | return qinstance | ... | ... |
perguntations/templates/admin.html
| ... | ... | @@ -21,9 +21,9 @@ |
| 21 | 21 | |
| 22 | 22 | <!-- Scripts --> |
| 23 | 23 | <script src="/static/jquery/jquery.min.js"></script> |
| 24 | - <script defer src="/static/popper.js/popper.min.js"></script> | |
| 24 | + <!-- <script defer src="/static/popper.js/popper.min.js"></script> --> | |
| 25 | 25 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 26 | - <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> | |
| 26 | + <script defer src="/static/bootstrap/js/bootstrap.bundle.min.js"></script> | |
| 27 | 27 | <script defer src="/static/datatables/js/jquery.dataTables.min.js"></script> |
| 28 | 28 | <script defer src="/static/underscore/underscore-min.js"></script> |
| 29 | 29 | <script defer src="/static/js/admin.js"></script> | ... | ... |
perguntations/templates/grade.html
| ... | ... | @@ -13,9 +13,9 @@ |
| 13 | 13 | |
| 14 | 14 | <!-- Scripts --> |
| 15 | 15 | <script src="/static/jquery/jquery.min.js"></script> |
| 16 | - <script defer src="/static/popper.js/popper.min.js"></script> | |
| 16 | + <!-- <script defer src="/static/popper.js/popper.min.js"></script> --> | |
| 17 | 17 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 18 | - <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> | |
| 18 | + <script defer src="/static/bootstrap/js/bootstrap.bundle.min.js"></script> | |
| 19 | 19 | </head> |
| 20 | 20 | <!-- ================================================================= --> |
| 21 | 21 | <body> | ... | ... |
perguntations/templates/login.html
| ... | ... | @@ -12,9 +12,9 @@ |
| 12 | 12 | |
| 13 | 13 | <!-- Scripts --> |
| 14 | 14 | <script src="/static/jquery/jquery.min.js"></script> |
| 15 | - <script defer src="/static/popper.js/popper.min.js"></script> | |
| 15 | + <!-- <script defer src="/static/popper.js/popper.min.js"></script> --> | |
| 16 | 16 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 17 | - <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> | |
| 17 | + <script defer src="/static/bootstrap/js/bootstrap.bundle.min.js"></script> | |
| 18 | 18 | |
| 19 | 19 | </head> |
| 20 | 20 | <!-- =================================================================== --> | ... | ... |
perguntations/templates/review.html
| ... | ... | @@ -28,9 +28,9 @@ |
| 28 | 28 | |
| 29 | 29 | <!-- Scripts --> |
| 30 | 30 | <script src="/static/jquery/jquery.min.js"></script> |
| 31 | - <script defer src="/static/popper.js/popper.min.js"></script> | |
| 31 | + <!-- <script defer src="/static/popper.js/popper.min.js"></script> --> | |
| 32 | 32 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 33 | - <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> | |
| 33 | + <script defer src="/static/bootstrap/js/bootstrap.bundle.min.js"></script> | |
| 34 | 34 | </head> |
| 35 | 35 | <!-- ===================================================================== --> |
| 36 | 36 | <body> | ... | ... |
perguntations/templates/test.html
| ... | ... | @@ -21,9 +21,9 @@ |
| 21 | 21 | |
| 22 | 22 | <!-- Scripts --> |
| 23 | 23 | <script src="/static/jquery/jquery.min.js"></script> |
| 24 | - <script defer src="/static/popper.js/popper.min.js"></script> | |
| 24 | + <!-- <script defer src="/static/popper.js/popper.min.js"></script> --> | |
| 25 | 25 | <script defer src="/static/fontawesome-free/js/all.min.js"></script> |
| 26 | - <script defer src="/static/bootstrap/js/bootstrap.min.js"></script> | |
| 26 | + <script defer src="/static/bootstrap/js/bootstrap.bundle.min.js"></script> | |
| 27 | 27 | <script defer src="/static/underscore/underscore-min.js"></script> |
| 28 | 28 | <script src="/static/codemirror/lib/codemirror.js"></script> |
| 29 | 29 | <script src="/static/codemirror/addon/selection/active-line.js"></script> | ... | ... |