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' |