diff --git a/demo/demo.yaml b/demo/demo.yaml index 2f73919..73e044d 100644 --- a/demo/demo.yaml +++ b/demo/demo.yaml @@ -29,7 +29,7 @@ duration: 20 # Automatic test submission after the given 'duration' timeout # (default: false) -autosubmit: true +autosubmit: false # If true, the test will be corrected on submission, the grade calculated and # shown to the student. If false, the test is saved but not corrected. diff --git a/perguntations/app.py b/perguntations/app.py index fbc55ac..7d45d6d 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -22,7 +22,7 @@ from perguntations.models import Student, Test, Question from perguntations.tools import load_yaml from perguntations.testfactory import TestFactory, TestFactoryException import perguntations.test -from perguntations.questions import QuestionFrom +from perguntations.questions import question_from logger = logging.getLogger(__name__) @@ -129,32 +129,56 @@ class App(): # ------------------------------------------------------------------------ def _correct_tests(self): with self._db_session() as sess: - filenames = sess.query(Test.filename)\ + # Find which tests have to be corrected + dbtests = sess.query(Test)\ .filter(Test.ref == self.testfactory['ref'])\ .filter(Test.state == "SUBMITTED")\ .all() - # print([(x.filename, x.state, x.grade) for x in a]) - logger.info('Correcting %d tests...', len(filenames)) - - for filename, in filenames: - try: - with open(filename) as file: - testdict = json.load(file) - except FileNotFoundError: - logger.error('File not found: %s', filename) - continue - - test = perguntations.test.Test(testdict) - print(test['questions'][7]['correct']) - test['questions'] = [QuestionFrom(q) for q in test['questions']] - - print(test['questions'][7]['correct']) - test.correct() - logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) - - - # FIXME update JSON and database + logger.info('Correcting %d tests...', len(dbtests)) + for dbtest in dbtests: + try: + with open(dbtest.filename) as file: + testdict = json.load(file) + except FileNotFoundError: + logger.error('File not found: %s', dbtest.filename) + continue + + # creates a class Test with the methods to correct it + # the questions are still dictionaries, so we have to call + # question_from() to produce Question() instances that can be + # corrected. Finally the test can be corrected. + test = perguntations.test.Test(testdict) + test['questions'] = [question_from(q) for q in test['questions']] + test.correct() + logger.info('Student %s: grade = %f', test['student']['number'], test['grade']) + + # save JSON file (overwriting the old one) + uid = test['student']['number'] + ref = test['ref'] + finish_time = test['finish_time'] + answers_dir = test['answers_dir'] + fname = f'{uid}--{ref}--{finish_time}.json' + fpath = path.join(answers_dir, fname) + test.save_json(fpath) + logger.info('%s saved JSON file.', uid) + + # update database + dbtest.grade = test['grade'] + dbtest.state = test['state'] + dbtest.questions = [ + Question( + number=n, + ref=q['ref'], + grade=q['grade'], + comment=q.get('comment', ''), + starttime=str(test['start_time']), + finishtime=str(test['finish_time']), + test_id=test['ref'] + ) + for n, q in enumerate(test['questions']) + ] + logger.info('%s database updated.', uid) # ------------------------------------------------------------------------ async def login(self, uid, try_pw, headers=None): @@ -291,11 +315,9 @@ class App(): logger.info('"%s" grade = %g points.', uid, test['grade']) # --- save test in JSON format - fields = (uid, test['ref'], str(test['finish_time'])) - fname = '--'.join(fields) + '.json' + fname = f'{uid}--{test["ref"]}--{test["finish_time"]}.json' fpath = path.join(test['answers_dir'], fname) - with open(path.expanduser(fpath), 'w') as file: - json.dump(test, file, indent=2, default=str) # str for datetime + test.save_json(fpath) logger.info('"%s" saved JSON.', uid) # --- insert test and questions into the database @@ -311,19 +333,19 @@ class App(): filename=fpath, student_id=uid) - test_row.questions = [ - Question( - number=n, - ref=q['ref'], - grade=q['grade'], - comment=q.get('comment', ''), - starttime=str(test['start_time']), - finishtime=str(test['finish_time']), - test_id=test['ref'] - ) - for n, q in enumerate(test['questions']) - if 'grade' in q - ] + if test['state'] == 'CORRECTED': + test_row.questions = [ + Question( + number=n, + ref=q['ref'], + grade=q['grade'], + comment=q.get('comment', ''), + starttime=str(test['start_time']), + finishtime=str(test['finish_time']), + test_id=test['ref'] + ) + for n, q in enumerate(test['questions']) + ] with self._db_session() as sess: sess.add(test_row) diff --git a/perguntations/questions.py b/perguntations/questions.py index c406607..abdb5fb 100644 --- a/perguntations/questions.py +++ b/perguntations/questions.py @@ -14,9 +14,9 @@ from typing import Any, Dict, NewType import uuid -from urllib.error import HTTPError -import json -import http.client +# from urllib.error import HTTPError +# import json +# import http.client # this project @@ -29,6 +29,8 @@ logger = logging.getLogger(__name__) QDict = NewType('QDict', Dict[str, Any]) + + class QuestionException(Exception): '''Exceptions raised in this module''' @@ -43,8 +45,13 @@ class Question(dict): for each student. Instances can shuffle options or automatically generate questions. ''' - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + ''' + Sets defaults that are valid for any question type + ''' # add required keys if missing self.set_defaults(QDict({ @@ -89,9 +96,15 @@ class QuestionRadio(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + def gen(self) -> None: + ''' + Sets defaults, performs checks and generates the actual question + by modifying the options and correct values + ''' + super().gen() try: nopts = len(self['options']) except KeyError as exc: @@ -218,8 +231,11 @@ class QuestionCheckbox(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + super().gen() try: nopts = len(self['options']) @@ -340,9 +356,11 @@ class QuestionText(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + def gen(self) -> None: + super().gen() self.set_defaults(QDict({ 'text': '', 'correct': [], # no correct answers, always wrong @@ -409,8 +427,11 @@ class QuestionTextRegex(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + super().gen() self.set_defaults(QDict({ 'text': '', @@ -422,26 +443,34 @@ class QuestionTextRegex(Question): self['correct'] = [self['correct']] # converts patterns to compiled versions - try: - self['correct'] = [re.compile(a) for a in self['correct']] - except Exception as exc: - msg = f'Failed to compile regex in "{self["ref"]}"' - logger.error(msg) - raise QuestionException(msg) from exc + # try: + # self['correct'] = [re.compile(a) for a in self['correct']] + # except Exception as exc: + # msg = f'Failed to compile regex in "{self["ref"]}"' + # logger.error(msg) + # raise QuestionException(msg) from exc # ------------------------------------------------------------------------ def correct(self) -> None: super().correct() if self['answer'] is not None: - self['grade'] = 0.0 for regex in self['correct']: try: - if regex.match(self['answer']): + if re.fullmatch(regex, self['answer']): self['grade'] = 1.0 return except TypeError: - logger.error('While matching regex %s with answer "%s".', - regex.pattern, self["answer"]) + logger.error('While matching regex "%s" with answer "%s".', + regex, self['answer']) + self['grade'] = 0.0 + + # try: + # if regex.match(self['answer']): + # self['grade'] = 1.0 + # return + # except TypeError: + # logger.error('While matching regex %s with answer "%s".', + # regex.pattern, self["answer"]) # ============================================================================ @@ -455,8 +484,11 @@ class QuestionNumericInterval(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + super().gen() self.set_defaults(QDict({ 'text': '', @@ -516,8 +548,11 @@ class QuestionTextArea(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + super().gen() self.set_defaults(QDict({ 'text': '', @@ -720,8 +755,11 @@ class QuestionInformation(Question): ''' # ------------------------------------------------------------------------ - def __init__(self, q: QDict) -> None: - super().__init__(q) + # def __init__(self, q: QDict) -> None: + # super().__init__(q) + + def gen(self) -> None: + super().gen() self.set_defaults(QDict({ 'text': '', })) @@ -733,9 +771,8 @@ class QuestionInformation(Question): - # ============================================================================ -def question_from(qdict: dict): +def question_from(qdict: QDict) -> Question: ''' Converts a question specified in a dict into an instance of Question() ''' @@ -836,6 +873,7 @@ class QFactory(): qdict.update(out) question = question_from(qdict) # returns a Question instance + question.gen() return question # ------------------------------------------------------------------------ diff --git a/perguntations/templates/review-question.html b/perguntations/templates/review-question.html index 4ea3137..2d994d1 100644 --- a/perguntations/templates/review-question.html +++ b/perguntations/templates/review-question.html @@ -32,45 +32,46 @@

- + {% end %} {% else %} @@ -97,10 +98,10 @@

+