Commit 96ae3635ade8084928617a596601435ccfbe1776
1 parent
9f9c3faf
Exists in
master
and in
1 other branch
option --correction is now working (needs more testing)
required changes in all questions classes where a new method gen() was added to generate a question instead of __init__()
Showing
5 changed files
with
178 additions
and
109 deletions
Show diff stats
demo/demo.yaml
| @@ -29,7 +29,7 @@ duration: 20 | @@ -29,7 +29,7 @@ duration: 20 | ||
| 29 | 29 | ||
| 30 | # Automatic test submission after the given 'duration' timeout | 30 | # Automatic test submission after the given 'duration' timeout |
| 31 | # (default: false) | 31 | # (default: false) |
| 32 | -autosubmit: true | 32 | +autosubmit: false |
| 33 | 33 | ||
| 34 | # If true, the test will be corrected on submission, the grade calculated and | 34 | # If true, the test will be corrected on submission, the grade calculated and |
| 35 | # shown to the student. If false, the test is saved but not corrected. | 35 | # shown to the student. If false, the test is saved but not corrected. |
perguntations/app.py
| @@ -22,7 +22,7 @@ from perguntations.models import Student, Test, Question | @@ -22,7 +22,7 @@ from perguntations.models import Student, Test, Question | ||
| 22 | from perguntations.tools import load_yaml | 22 | from perguntations.tools import load_yaml |
| 23 | from perguntations.testfactory import TestFactory, TestFactoryException | 23 | from perguntations.testfactory import TestFactory, TestFactoryException |
| 24 | import perguntations.test | 24 | import perguntations.test |
| 25 | -from perguntations.questions import QuestionFrom | 25 | +from perguntations.questions import question_from |
| 26 | 26 | ||
| 27 | logger = logging.getLogger(__name__) | 27 | logger = logging.getLogger(__name__) |
| 28 | 28 | ||
| @@ -129,32 +129,56 @@ class App(): | @@ -129,32 +129,56 @@ class App(): | ||
| 129 | # ------------------------------------------------------------------------ | 129 | # ------------------------------------------------------------------------ |
| 130 | def _correct_tests(self): | 130 | def _correct_tests(self): |
| 131 | with self._db_session() as sess: | 131 | with self._db_session() as sess: |
| 132 | - filenames = sess.query(Test.filename)\ | 132 | + # Find which tests have to be corrected |
| 133 | + dbtests = sess.query(Test)\ | ||
| 133 | .filter(Test.ref == self.testfactory['ref'])\ | 134 | .filter(Test.ref == self.testfactory['ref'])\ |
| 134 | .filter(Test.state == "SUBMITTED")\ | 135 | .filter(Test.state == "SUBMITTED")\ |
| 135 | .all() | 136 | .all() |
| 136 | - # print([(x.filename, x.state, x.grade) for x in a]) | ||
| 137 | - logger.info('Correcting %d tests...', len(filenames)) | ||
| 138 | - | ||
| 139 | - for filename, in filenames: | ||
| 140 | - try: | ||
| 141 | - with open(filename) as file: | ||
| 142 | - testdict = json.load(file) | ||
| 143 | - except FileNotFoundError: | ||
| 144 | - logger.error('File not found: %s', filename) | ||
| 145 | - continue | ||
| 146 | - | ||
| 147 | - test = perguntations.test.Test(testdict) | ||
| 148 | - print(test['questions'][7]['correct']) | ||
| 149 | - test['questions'] = [QuestionFrom(q) for q in test['questions']] | ||
| 150 | - | ||
| 151 | - print(test['questions'][7]['correct']) | ||
| 152 | - test.correct() | ||
| 153 | - logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) | ||
| 154 | - | ||
| 155 | - | ||
| 156 | - # FIXME update JSON and database | ||
| 157 | 137 | ||
| 138 | + logger.info('Correcting %d tests...', len(dbtests)) | ||
| 139 | + for dbtest in dbtests: | ||
| 140 | + try: | ||
| 141 | + with open(dbtest.filename) as file: | ||
| 142 | + testdict = json.load(file) | ||
| 143 | + except FileNotFoundError: | ||
| 144 | + logger.error('File not found: %s', dbtest.filename) | ||
| 145 | + continue | ||
| 146 | + | ||
| 147 | + # creates a class Test with the methods to correct it | ||
| 148 | + # the questions are still dictionaries, so we have to call | ||
| 149 | + # question_from() to produce Question() instances that can be | ||
| 150 | + # corrected. Finally the test can be corrected. | ||
| 151 | + test = perguntations.test.Test(testdict) | ||
| 152 | + test['questions'] = [question_from(q) for q in test['questions']] | ||
| 153 | + test.correct() | ||
| 154 | + logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) | ||
| 155 | + | ||
| 156 | + # save JSON file (overwriting the old one) | ||
| 157 | + uid = test['student']['number'] | ||
| 158 | + ref = test['ref'] | ||
| 159 | + finish_time = test['finish_time'] | ||
| 160 | + answers_dir = test['answers_dir'] | ||
| 161 | + fname = f'{uid}--{ref}--{finish_time}.json' | ||
| 162 | + fpath = path.join(answers_dir, fname) | ||
| 163 | + test.save_json(fpath) | ||
| 164 | + logger.info('%s saved JSON file.', uid) | ||
| 165 | + | ||
| 166 | + # update database | ||
| 167 | + dbtest.grade = test['grade'] | ||
| 168 | + dbtest.state = test['state'] | ||
| 169 | + dbtest.questions = [ | ||
| 170 | + Question( | ||
| 171 | + number=n, | ||
| 172 | + ref=q['ref'], | ||
| 173 | + grade=q['grade'], | ||
| 174 | + comment=q.get('comment', ''), | ||
| 175 | + starttime=str(test['start_time']), | ||
| 176 | + finishtime=str(test['finish_time']), | ||
| 177 | + test_id=test['ref'] | ||
| 178 | + ) | ||
| 179 | + for n, q in enumerate(test['questions']) | ||
| 180 | + ] | ||
| 181 | + logger.info('%s database updated.', uid) | ||
| 158 | 182 | ||
| 159 | # ------------------------------------------------------------------------ | 183 | # ------------------------------------------------------------------------ |
| 160 | async def login(self, uid, try_pw, headers=None): | 184 | async def login(self, uid, try_pw, headers=None): |
| @@ -291,11 +315,9 @@ class App(): | @@ -291,11 +315,9 @@ class App(): | ||
| 291 | logger.info('"%s" grade = %g points.', uid, test['grade']) | 315 | logger.info('"%s" grade = %g points.', uid, test['grade']) |
| 292 | 316 | ||
| 293 | # --- save test in JSON format | 317 | # --- save test in JSON format |
| 294 | - fields = (uid, test['ref'], str(test['finish_time'])) | ||
| 295 | - fname = '--'.join(fields) + '.json' | 318 | + fname = f'{uid}--{test["ref"]}--{test["finish_time"]}.json' |
| 296 | fpath = path.join(test['answers_dir'], fname) | 319 | fpath = path.join(test['answers_dir'], fname) |
| 297 | - with open(path.expanduser(fpath), 'w') as file: | ||
| 298 | - json.dump(test, file, indent=2, default=str) # str for datetime | 320 | + test.save_json(fpath) |
| 299 | logger.info('"%s" saved JSON.', uid) | 321 | logger.info('"%s" saved JSON.', uid) |
| 300 | 322 | ||
| 301 | # --- insert test and questions into the database | 323 | # --- insert test and questions into the database |
| @@ -311,19 +333,19 @@ class App(): | @@ -311,19 +333,19 @@ class App(): | ||
| 311 | filename=fpath, | 333 | filename=fpath, |
| 312 | student_id=uid) | 334 | student_id=uid) |
| 313 | 335 | ||
| 314 | - test_row.questions = [ | ||
| 315 | - Question( | ||
| 316 | - number=n, | ||
| 317 | - ref=q['ref'], | ||
| 318 | - grade=q['grade'], | ||
| 319 | - comment=q.get('comment', ''), | ||
| 320 | - starttime=str(test['start_time']), | ||
| 321 | - finishtime=str(test['finish_time']), | ||
| 322 | - test_id=test['ref'] | ||
| 323 | - ) | ||
| 324 | - for n, q in enumerate(test['questions']) | ||
| 325 | - if 'grade' in q | ||
| 326 | - ] | 336 | + if test['state'] == 'CORRECTED': |
| 337 | + test_row.questions = [ | ||
| 338 | + Question( | ||
| 339 | + number=n, | ||
| 340 | + ref=q['ref'], | ||
| 341 | + grade=q['grade'], | ||
| 342 | + comment=q.get('comment', ''), | ||
| 343 | + starttime=str(test['start_time']), | ||
| 344 | + finishtime=str(test['finish_time']), | ||
| 345 | + test_id=test['ref'] | ||
| 346 | + ) | ||
| 347 | + for n, q in enumerate(test['questions']) | ||
| 348 | + ] | ||
| 327 | 349 | ||
| 328 | with self._db_session() as sess: | 350 | with self._db_session() as sess: |
| 329 | sess.add(test_row) | 351 | sess.add(test_row) |
perguntations/questions.py
| @@ -14,9 +14,9 @@ from typing import Any, Dict, NewType | @@ -14,9 +14,9 @@ from typing import Any, Dict, NewType | ||
| 14 | import uuid | 14 | import uuid |
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | -from urllib.error import HTTPError | ||
| 18 | -import json | ||
| 19 | -import http.client | 17 | +# from urllib.error import HTTPError |
| 18 | +# import json | ||
| 19 | +# import http.client | ||
| 20 | 20 | ||
| 21 | 21 | ||
| 22 | # this project | 22 | # this project |
| @@ -29,6 +29,8 @@ logger = logging.getLogger(__name__) | @@ -29,6 +29,8 @@ logger = logging.getLogger(__name__) | ||
| 29 | QDict = NewType('QDict', Dict[str, Any]) | 29 | QDict = NewType('QDict', Dict[str, Any]) |
| 30 | 30 | ||
| 31 | 31 | ||
| 32 | + | ||
| 33 | + | ||
| 32 | class QuestionException(Exception): | 34 | class QuestionException(Exception): |
| 33 | '''Exceptions raised in this module''' | 35 | '''Exceptions raised in this module''' |
| 34 | 36 | ||
| @@ -43,8 +45,13 @@ class Question(dict): | @@ -43,8 +45,13 @@ class Question(dict): | ||
| 43 | for each student. | 45 | for each student. |
| 44 | Instances can shuffle options or automatically generate questions. | 46 | Instances can shuffle options or automatically generate questions. |
| 45 | ''' | 47 | ''' |
| 46 | - def __init__(self, q: QDict) -> None: | ||
| 47 | - super().__init__(q) | 48 | + # def __init__(self, q: QDict) -> None: |
| 49 | + # super().__init__(q) | ||
| 50 | + | ||
| 51 | + def gen(self) -> None: | ||
| 52 | + ''' | ||
| 53 | + Sets defaults that are valid for any question type | ||
| 54 | + ''' | ||
| 48 | 55 | ||
| 49 | # add required keys if missing | 56 | # add required keys if missing |
| 50 | self.set_defaults(QDict({ | 57 | self.set_defaults(QDict({ |
| @@ -89,9 +96,15 @@ class QuestionRadio(Question): | @@ -89,9 +96,15 @@ class QuestionRadio(Question): | ||
| 89 | ''' | 96 | ''' |
| 90 | 97 | ||
| 91 | # ------------------------------------------------------------------------ | 98 | # ------------------------------------------------------------------------ |
| 92 | - def __init__(self, q: QDict) -> None: | ||
| 93 | - super().__init__(q) | 99 | + # def __init__(self, q: QDict) -> None: |
| 100 | + # super().__init__(q) | ||
| 94 | 101 | ||
| 102 | + def gen(self) -> None: | ||
| 103 | + ''' | ||
| 104 | + Sets defaults, performs checks and generates the actual question | ||
| 105 | + by modifying the options and correct values | ||
| 106 | + ''' | ||
| 107 | + super().gen() | ||
| 95 | try: | 108 | try: |
| 96 | nopts = len(self['options']) | 109 | nopts = len(self['options']) |
| 97 | except KeyError as exc: | 110 | except KeyError as exc: |
| @@ -218,8 +231,11 @@ class QuestionCheckbox(Question): | @@ -218,8 +231,11 @@ class QuestionCheckbox(Question): | ||
| 218 | ''' | 231 | ''' |
| 219 | 232 | ||
| 220 | # ------------------------------------------------------------------------ | 233 | # ------------------------------------------------------------------------ |
| 221 | - def __init__(self, q: QDict) -> None: | ||
| 222 | - super().__init__(q) | 234 | + # def __init__(self, q: QDict) -> None: |
| 235 | + # super().__init__(q) | ||
| 236 | + | ||
| 237 | + def gen(self) -> None: | ||
| 238 | + super().gen() | ||
| 223 | 239 | ||
| 224 | try: | 240 | try: |
| 225 | nopts = len(self['options']) | 241 | nopts = len(self['options']) |
| @@ -340,9 +356,11 @@ class QuestionText(Question): | @@ -340,9 +356,11 @@ class QuestionText(Question): | ||
| 340 | ''' | 356 | ''' |
| 341 | 357 | ||
| 342 | # ------------------------------------------------------------------------ | 358 | # ------------------------------------------------------------------------ |
| 343 | - def __init__(self, q: QDict) -> None: | ||
| 344 | - super().__init__(q) | 359 | + # def __init__(self, q: QDict) -> None: |
| 360 | + # super().__init__(q) | ||
| 345 | 361 | ||
| 362 | + def gen(self) -> None: | ||
| 363 | + super().gen() | ||
| 346 | self.set_defaults(QDict({ | 364 | self.set_defaults(QDict({ |
| 347 | 'text': '', | 365 | 'text': '', |
| 348 | 'correct': [], # no correct answers, always wrong | 366 | 'correct': [], # no correct answers, always wrong |
| @@ -409,8 +427,11 @@ class QuestionTextRegex(Question): | @@ -409,8 +427,11 @@ class QuestionTextRegex(Question): | ||
| 409 | ''' | 427 | ''' |
| 410 | 428 | ||
| 411 | # ------------------------------------------------------------------------ | 429 | # ------------------------------------------------------------------------ |
| 412 | - def __init__(self, q: QDict) -> None: | ||
| 413 | - super().__init__(q) | 430 | + # def __init__(self, q: QDict) -> None: |
| 431 | + # super().__init__(q) | ||
| 432 | + | ||
| 433 | + def gen(self) -> None: | ||
| 434 | + super().gen() | ||
| 414 | 435 | ||
| 415 | self.set_defaults(QDict({ | 436 | self.set_defaults(QDict({ |
| 416 | 'text': '', | 437 | 'text': '', |
| @@ -422,26 +443,34 @@ class QuestionTextRegex(Question): | @@ -422,26 +443,34 @@ class QuestionTextRegex(Question): | ||
| 422 | self['correct'] = [self['correct']] | 443 | self['correct'] = [self['correct']] |
| 423 | 444 | ||
| 424 | # converts patterns to compiled versions | 445 | # converts patterns to compiled versions |
| 425 | - try: | ||
| 426 | - self['correct'] = [re.compile(a) for a in self['correct']] | ||
| 427 | - except Exception as exc: | ||
| 428 | - msg = f'Failed to compile regex in "{self["ref"]}"' | ||
| 429 | - logger.error(msg) | ||
| 430 | - raise QuestionException(msg) from exc | 446 | + # try: |
| 447 | + # self['correct'] = [re.compile(a) for a in self['correct']] | ||
| 448 | + # except Exception as exc: | ||
| 449 | + # msg = f'Failed to compile regex in "{self["ref"]}"' | ||
| 450 | + # logger.error(msg) | ||
| 451 | + # raise QuestionException(msg) from exc | ||
| 431 | 452 | ||
| 432 | # ------------------------------------------------------------------------ | 453 | # ------------------------------------------------------------------------ |
| 433 | def correct(self) -> None: | 454 | def correct(self) -> None: |
| 434 | super().correct() | 455 | super().correct() |
| 435 | if self['answer'] is not None: | 456 | if self['answer'] is not None: |
| 436 | - self['grade'] = 0.0 | ||
| 437 | for regex in self['correct']: | 457 | for regex in self['correct']: |
| 438 | try: | 458 | try: |
| 439 | - if regex.match(self['answer']): | 459 | + if re.fullmatch(regex, self['answer']): |
| 440 | self['grade'] = 1.0 | 460 | self['grade'] = 1.0 |
| 441 | return | 461 | return |
| 442 | except TypeError: | 462 | except TypeError: |
| 443 | - logger.error('While matching regex %s with answer "%s".', | ||
| 444 | - regex.pattern, self["answer"]) | 463 | + logger.error('While matching regex "%s" with answer "%s".', |
| 464 | + regex, self['answer']) | ||
| 465 | + self['grade'] = 0.0 | ||
| 466 | + | ||
| 467 | + # try: | ||
| 468 | + # if regex.match(self['answer']): | ||
| 469 | + # self['grade'] = 1.0 | ||
| 470 | + # return | ||
| 471 | + # except TypeError: | ||
| 472 | + # logger.error('While matching regex %s with answer "%s".', | ||
| 473 | + # regex.pattern, self["answer"]) | ||
| 445 | 474 | ||
| 446 | 475 | ||
| 447 | # ============================================================================ | 476 | # ============================================================================ |
| @@ -455,8 +484,11 @@ class QuestionNumericInterval(Question): | @@ -455,8 +484,11 @@ class QuestionNumericInterval(Question): | ||
| 455 | ''' | 484 | ''' |
| 456 | 485 | ||
| 457 | # ------------------------------------------------------------------------ | 486 | # ------------------------------------------------------------------------ |
| 458 | - def __init__(self, q: QDict) -> None: | ||
| 459 | - super().__init__(q) | 487 | + # def __init__(self, q: QDict) -> None: |
| 488 | + # super().__init__(q) | ||
| 489 | + | ||
| 490 | + def gen(self) -> None: | ||
| 491 | + super().gen() | ||
| 460 | 492 | ||
| 461 | self.set_defaults(QDict({ | 493 | self.set_defaults(QDict({ |
| 462 | 'text': '', | 494 | 'text': '', |
| @@ -516,8 +548,11 @@ class QuestionTextArea(Question): | @@ -516,8 +548,11 @@ class QuestionTextArea(Question): | ||
| 516 | ''' | 548 | ''' |
| 517 | 549 | ||
| 518 | # ------------------------------------------------------------------------ | 550 | # ------------------------------------------------------------------------ |
| 519 | - def __init__(self, q: QDict) -> None: | ||
| 520 | - super().__init__(q) | 551 | + # def __init__(self, q: QDict) -> None: |
| 552 | + # super().__init__(q) | ||
| 553 | + | ||
| 554 | + def gen(self) -> None: | ||
| 555 | + super().gen() | ||
| 521 | 556 | ||
| 522 | self.set_defaults(QDict({ | 557 | self.set_defaults(QDict({ |
| 523 | 'text': '', | 558 | 'text': '', |
| @@ -720,8 +755,11 @@ class QuestionInformation(Question): | @@ -720,8 +755,11 @@ class QuestionInformation(Question): | ||
| 720 | ''' | 755 | ''' |
| 721 | 756 | ||
| 722 | # ------------------------------------------------------------------------ | 757 | # ------------------------------------------------------------------------ |
| 723 | - def __init__(self, q: QDict) -> None: | ||
| 724 | - super().__init__(q) | 758 | + # def __init__(self, q: QDict) -> None: |
| 759 | + # super().__init__(q) | ||
| 760 | + | ||
| 761 | + def gen(self) -> None: | ||
| 762 | + super().gen() | ||
| 725 | self.set_defaults(QDict({ | 763 | self.set_defaults(QDict({ |
| 726 | 'text': '', | 764 | 'text': '', |
| 727 | })) | 765 | })) |
| @@ -733,9 +771,8 @@ class QuestionInformation(Question): | @@ -733,9 +771,8 @@ class QuestionInformation(Question): | ||
| 733 | 771 | ||
| 734 | 772 | ||
| 735 | 773 | ||
| 736 | - | ||
| 737 | # ============================================================================ | 774 | # ============================================================================ |
| 738 | -def question_from(qdict: dict): | 775 | +def question_from(qdict: QDict) -> Question: |
| 739 | ''' | 776 | ''' |
| 740 | Converts a question specified in a dict into an instance of Question() | 777 | Converts a question specified in a dict into an instance of Question() |
| 741 | ''' | 778 | ''' |
| @@ -836,6 +873,7 @@ class QFactory(): | @@ -836,6 +873,7 @@ class QFactory(): | ||
| 836 | qdict.update(out) | 873 | qdict.update(out) |
| 837 | 874 | ||
| 838 | question = question_from(qdict) # returns a Question instance | 875 | question = question_from(qdict) # returns a Question instance |
| 876 | + question.gen() | ||
| 839 | return question | 877 | return question |
| 840 | 878 | ||
| 841 | # ------------------------------------------------------------------------ | 879 | # ------------------------------------------------------------------------ |
perguntations/templates/review-question.html
| @@ -32,45 +32,46 @@ | @@ -32,45 +32,46 @@ | ||
| 32 | </p> | 32 | </p> |
| 33 | </div> <!-- card-body --> | 33 | </div> <!-- card-body --> |
| 34 | 34 | ||
| 35 | - <div class="card-footer"> | ||
| 36 | - {% if q['grade'] > 0.99 %} | ||
| 37 | - <p class="text-success"> | ||
| 38 | - <i class="far fa-thumbs-up fa-3x" aria-hidden="true"></i> | ||
| 39 | - {{ round(q['grade'] * q['points'], 2) }} | ||
| 40 | - pontos | ||
| 41 | - </p> | ||
| 42 | - <p class="text-success">{{ md(q['comments']) }}</p> | ||
| 43 | - {% elif q['grade'] > 0.49 %} | ||
| 44 | - <p class="text-warning"> | ||
| 45 | - <i class="fas fa-exclamation-triangle fa-3x" aria-hidden="true"></i> | ||
| 46 | - {{ round(q['grade'] * q['points'], 2) }} | ||
| 47 | - pontos | ||
| 48 | - </p> | ||
| 49 | - <p class="text-warning">{{ md(q['comments']) }}</p> | ||
| 50 | - {% if q['solution'] %} | ||
| 51 | - <hr> | ||
| 52 | - {{ md('**Solução:** \n\n' + q['solution']) }} | 35 | + {% if 'grade' in q %} |
| 36 | + <div class="card-footer"> | ||
| 37 | + {% if q['grade'] > 0.999 %} | ||
| 38 | + <p class="text-success"> | ||
| 39 | + <i class="far fa-thumbs-up fa-3x" aria-hidden="true"></i> | ||
| 40 | + {{ round(q['grade'] * q['points'], 2) }} | ||
| 41 | + pontos | ||
| 42 | + </p> | ||
| 43 | + <p class="text-success">{{ md(q['comments']) }}</p> | ||
| 44 | + {% elif q['grade'] >= 0.5 %} | ||
| 45 | + <p class="text-warning"> | ||
| 46 | + <i class="fas fa-exclamation-triangle fa-3x" aria-hidden="true"></i> | ||
| 47 | + {{ round(q['grade'] * q['points'], 2) }} | ||
| 48 | + pontos | ||
| 49 | + </p> | ||
| 50 | + <p class="text-warning">{{ md(q['comments']) }}</p> | ||
| 51 | + {% if q['solution'] %} | ||
| 52 | + <hr> | ||
| 53 | + {{ md('**Solução:** \n\n' + q['solution']) }} | ||
| 54 | + {% end %} | ||
| 55 | + {% else %} | ||
| 56 | + <p class="text-danger"> | ||
| 57 | + <i class="far fa-thumbs-down fa-3x" aria-hidden="true"></i> | ||
| 58 | + {{ round(q['grade'] * q['points'], 2) }} | ||
| 59 | + pontos | ||
| 60 | + </p> | ||
| 61 | + <p class="text-danger">{{ md(q['comments']) }}</p> | ||
| 62 | + {% if q['solution'] %} | ||
| 63 | + <hr> | ||
| 64 | + {{ md('**Solução:** \n\n' + q['solution']) }} | ||
| 65 | + {% end %} | ||
| 53 | {% end %} | 66 | {% end %} |
| 54 | - {% else %} | ||
| 55 | - <p class="text-danger"> | ||
| 56 | - <i class="far fa-thumbs-down fa-3x" aria-hidden="true"></i> | ||
| 57 | - {{ round(q['grade'] * q['points'], 2) }} | ||
| 58 | - pontos | ||
| 59 | - </p> | ||
| 60 | - <p class="text-danger">{{ md(q['comments']) }}</p> | ||
| 61 | - {% if q['solution'] %} | 67 | + |
| 68 | + {% if t['show_ref'] %} | ||
| 62 | <hr> | 69 | <hr> |
| 63 | - {{ md('**Solução:** \n\n' + q['solution']) }} | 70 | + file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br> |
| 71 | + ref: <code>{{ q['ref'] }}</code> | ||
| 64 | {% end %} | 72 | {% end %} |
| 65 | - {% end %} | ||
| 66 | - | ||
| 67 | - {% if t['show_ref'] %} | ||
| 68 | - <hr> | ||
| 69 | - file: <code>{{ q['path'] }}/{{ q['filename'] }}</code><br> | ||
| 70 | - ref: <code>{{ q['ref'] }}</code> | ||
| 71 | - {% end %} | ||
| 72 | - | ||
| 73 | - </div> <!-- card-footer --> | 73 | + </div> <!-- card-footer --> |
| 74 | + {% end %} | ||
| 74 | </div> <!-- card --> | 75 | </div> <!-- card --> |
| 75 | 76 | ||
| 76 | {% else %} | 77 | {% else %} |
| @@ -97,10 +98,10 @@ | @@ -97,10 +98,10 @@ | ||
| 97 | </small> | 98 | </small> |
| 98 | </p> | 99 | </p> |
| 99 | </div> <!-- card-body --> | 100 | </div> <!-- card-body --> |
| 101 | + | ||
| 100 | <div class="card-footer"> | 102 | <div class="card-footer"> |
| 101 | <p class="text-secondary"> | 103 | <p class="text-secondary"> |
| 102 | <i class="fas fa-ban fa-3x" aria-hidden="true"></i> | 104 | <i class="fas fa-ban fa-3x" aria-hidden="true"></i> |
| 103 | - {{ round(q['grade'] * q['points'], 2) }} pontos<br> | ||
| 104 | {{ md(q['comments']) }} | 105 | {{ md(q['comments']) }} |
| 105 | {% if q['solution'] %} | 106 | {% if q['solution'] %} |
| 106 | <hr> | 107 | <hr> |
perguntations/test.py
| @@ -4,8 +4,10 @@ Test - instances of this class are individual tests | @@ -4,8 +4,10 @@ Test - instances of this class are individual tests | ||
| 4 | 4 | ||
| 5 | # python standard library | 5 | # python standard library |
| 6 | from datetime import datetime | 6 | from datetime import datetime |
| 7 | +import json | ||
| 7 | import logging | 8 | import logging |
| 8 | from math import nan | 9 | from math import nan |
| 10 | +from os import path | ||
| 9 | 11 | ||
| 10 | # Logger configuration | 12 | # Logger configuration |
| 11 | logger = logging.getLogger(__name__) | 13 | logger = logging.getLogger(__name__) |
| @@ -92,6 +94,12 @@ class Test(dict): | @@ -92,6 +94,12 @@ class Test(dict): | ||
| 92 | self['grade'] = 0.0 | 94 | self['grade'] = 0.0 |
| 93 | 95 | ||
| 94 | # ------------------------------------------------------------------------ | 96 | # ------------------------------------------------------------------------ |
| 97 | + def save_json(self, pathfile) -> None: | ||
| 98 | + '''save test in JSON format''' | ||
| 99 | + with open(pathfile, 'w') as file: | ||
| 100 | + json.dump(self, file, indent=2, default=str) # str for datetime | ||
| 101 | + | ||
| 102 | + # ------------------------------------------------------------------------ | ||
| 95 | def __str__(self) -> str: | 103 | def __str__(self) -> str: |
| 96 | return '\n'.join([f'{k}: {v}' for k,v in self.items()]) | 104 | return '\n'.join([f'{k}: {v}' for k,v in self.items()]) |
| 97 | # return ('Test:\n' | 105 | # return ('Test:\n' |