From 96ae3635ade8084928617a596601435ccfbe1776 Mon Sep 17 00:00:00 2001
From: Miguel Barão
Date: Wed, 9 Dec 2020 21:12:45 +0000
Subject: [PATCH] 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__()
---
demo/demo.yaml | 2 +-
perguntations/app.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------
perguntations/questions.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
perguntations/templates/review-question.html | 75 ++++++++++++++++++++++++++++++++++++++-------------------------------------
perguntations/test.py | 8 ++++++++
5 files changed, 178 insertions(+), 109 deletions(-)
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 @@
+