diff --git a/openassessment/xblock/openassessmentblock.py b/openassessment/xblock/openassessmentblock.py
index 1302d92b3f..367eb13432 100644
--- a/openassessment/xblock/openassessmentblock.py
+++ b/openassessment/xblock/openassessmentblock.py
@@ -1369,10 +1369,10 @@ def reset_submission(self, data, suffix=""): # pylint: disable=unused-argument
"""
try:
block_user = self.runtime.service(self, "user").get_current_user()
- user = get_user_by_username_or_email(block_user.opt_attrs["edx-platform.username"])
+ user = get_user_by_username_or_email(block_user.opt_attrs.get("edx-platform.username"))
reset_student_attempts(self.course_id, user, self.location, user, True) # pylint: disable=no-member
- except Exception:
- logger.exception("An error occurred while resetting the submission.")
+ except Exception as error:
+ logger.exception(f"An error occurred while resetting the submission: {error}")
return {"success": False, "msg": self._("Error resetting submission.")}
return {"success": True, "msg": self._("Submission reset successfully.")}
diff --git a/openassessment/xblock/test/data/update_from_xml.json b/openassessment/xblock/test/data/update_from_xml.json
index 3ea91aa06b..dba2a87c16 100644
--- a/openassessment/xblock/test/data/update_from_xml.json
+++ b/openassessment/xblock/test/data/update_from_xml.json
@@ -654,5 +654,27 @@
""
],
"show_rubric_during_response": true
+ },
+
+ "allow_learner_resubmissions": {
+ "xml": [
+ "",
+ "Foo",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "Test prompt",
+ "",
+ "Test criterion",
+ "Test criterion prompt",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "allow_learner_resubmissions": true
}
}
diff --git a/openassessment/xblock/test/data/update_from_xml_error.json b/openassessment/xblock/test/data/update_from_xml_error.json
index 329f9fa501..e144af3590 100644
--- a/openassessment/xblock/test/data/update_from_xml_error.json
+++ b/openassessment/xblock/test/data/update_from_xml_error.json
@@ -455,5 +455,47 @@
"",
""
]
+ },
+
+ "resubmissions_grace_period_hours_not_integer": {
+ "xml": [
+ "",
+ "Foo",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "Test prompt",
+ "",
+ "Test criterion",
+ "Test criterion prompt",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
+ },
+
+ "resubmissions_grace_period_minutes_not_integer": {
+ "xml": [
+ "",
+ "Foo",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "Test prompt",
+ "",
+ "Test criterion",
+ "Test criterion prompt",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
}
}
diff --git a/openassessment/xblock/test/test_allow_resubmission.py b/openassessment/xblock/test/test_allow_resubmission.py
new file mode 100644
index 0000000000..4c0f2c5196
--- /dev/null
+++ b/openassessment/xblock/test/test_allow_resubmission.py
@@ -0,0 +1,159 @@
+"""This module contains the tests for the allow_resubmission module."""
+
+import datetime
+import unittest
+
+import ddt
+
+from openassessment.xblock.utils.allow_resubmission import allow_resubmission
+
+
+class ConfigDataMock:
+ """Mock class for the ORAConfigAPI object."""
+
+ def __init__(self):
+ self.allow_learner_resubmissions = True
+ self.submission_due = "2029-01-01T00:00:00+00:00"
+ self.resubmissions_grace_period_hours = 0
+ self.resubmissions_grace_period_minutes = 0
+
+
+class WorkflowDataMock:
+ """Mock class for the WorkflowAPI object."""
+
+ def __init__(self):
+ self.status = "waiting"
+ self.status_details = {
+ "staff": {"complete": True, "graded": False, "skipped": False},
+ "peer": {
+ "complete": False,
+ "graded": False,
+ "skipped": True,
+ "peers_graded_count": 0,
+ "graded_by_count": 0,
+ },
+ }
+
+
+@ddt.ddt
+class TestAllowResubmission(unittest.TestCase):
+ """Tests for the allow_resubmission module."""
+
+ def setUp(self):
+ self.config_data = ConfigDataMock()
+ self.workflow_data = WorkflowDataMock()
+ self.submission_data = {
+ "created_at": datetime.datetime.now(tz=datetime.timezone.utc)
+ }
+
+ def test_allow_resubmission_all_conditions_met(self):
+ """
+ Test case for the allow_resubmission function when all conditions are met.
+
+ This test checks if the function returns True when:
+ - Learner resubmissions are allowed
+ - The submission date has not been exceeded
+ - The workflow has not been graded
+ """
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertTrue(result)
+
+ @ddt.data(
+ (1, 0),
+ (0, 30),
+ (10, 59),
+ (0, 0),
+ )
+ @ddt.unpack
+ def test_allow_resubmission_resubmissions_with_grace_period(
+ self, hours: int, minutes: int
+ ):
+ """
+ Test case for the allow_resubmission function when the resubmissions grace period is set.
+ """
+ self.config_data.resubmissions_grace_period_hours = hours
+ self.config_data.resubmissions_grace_period_minutes = minutes
+
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertTrue(result)
+
+ def test_allow_resubmission_resubmissions_with_grace_period_exceeded(self):
+ """
+ Test case for the allow_resubmission function when the resubmissions grace period is exceeded.
+ """
+ self.submission_data["created_at"] = datetime.datetime(
+ 2020, 1, 1, tzinfo=datetime.timezone.utc
+ )
+ self.config_data.resubmissions_grace_period_hours = 1
+ self.config_data.resubmissions_grace_period_minutes = 30
+
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertFalse(result)
+
+ def test_allow_resubmission_resubmissions_not_allowed(self):
+ """
+ Test case for the allow_resubmission function when learner resubmissions are not allowed.
+ """
+ self.config_data.allow_learner_resubmissions = False
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertFalse(result)
+
+ def test_allow_resubmission_submission_date_exceeded(self):
+ """
+ Test case for the allow_resubmission function when the submission date has been exceeded.
+ """
+ self.config_data.submission_due = "2020-01-01T00:00:00+00:00"
+
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertFalse(result)
+
+ @ddt.data("done", "cancelled")
+ def test_allow_resubmission_has_been_graded_or_cancelled(self, status: str):
+ """
+ Test case for the allow_resubmission function when the
+ learner's response has been graded or cancelled.
+ """
+ self.workflow_data.status = status
+
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertFalse(result)
+
+ @ddt.data(
+ (0, True),
+ (1, False),
+ (2, False),
+ )
+ @ddt.unpack
+ def test_allow_resubmission_has_been_graded_by_peers(
+ self, count: int, expected_result: bool
+ ):
+ """
+ Test case for the allow_resubmission function when the
+ learner's response has been graded by peers.
+ """
+ self.workflow_data.status = "self"
+ self.workflow_data.status_details["peer"]["graded_by_count"] = count
+
+ result = allow_resubmission(
+ self.config_data, self.workflow_data, self.submission_data
+ )
+
+ self.assertEqual(result, expected_result)
diff --git a/openassessment/xblock/test/test_submission.py b/openassessment/xblock/test/test_submission.py
index 6ef243cca2..376cda2bff 100644
--- a/openassessment/xblock/test/test_submission.py
+++ b/openassessment/xblock/test/test_submission.py
@@ -153,6 +153,24 @@ def test_cannot_submit_in_preview_mode(self, xblock):
self.assertEqual(resp[1], "ENOPREVIEW")
self.assertIsNot(resp[2], None)
+ @patch("openassessment.xblock.openassessmentblock.reset_student_attempts")
+ @patch("openassessment.xblock.openassessmentblock.get_user_by_username_or_email")
+ @scenario("data/basic_scenario.xml", user_id="Bob")
+ def test_reset_submission(self, xblock, mock_user: Mock, mock_reset: Mock):
+ xblock.xmodule_runtime = Mock(course_id=COURSE_ID)
+ mock_user.return_value = "test-user"
+ mock_reset.return_value = True
+
+ resp = self.request(xblock, "reset_submission", json.dumps({}), response_format="json")
+ self.assertTrue(resp["success"])
+ self.assertEqual(resp["msg"], "Submission reset successfully.")
+
+ @scenario("data/basic_scenario.xml", user_id="Bob")
+ def test_reset_submission_error(self, xblock):
+ resp = self.request(xblock, "reset_submission", json.dumps({}), response_format="json")
+ self.assertFalse(resp["success"])
+ self.assertEqual(resp["msg"], "Error resetting submission.")
+
@scenario('data/over_grade_scenario.xml', user_id='Alice')
def test_closed_submissions(self, xblock):
resp = self.request(xblock, 'render_submission', json.dumps({}))
diff --git a/openassessment/xblock/test/test_xml.py b/openassessment/xblock/test/test_xml.py
index 6ca90302b4..8ffdcf0369 100644
--- a/openassessment/xblock/test/test_xml.py
+++ b/openassessment/xblock/test/test_xml.py
@@ -568,6 +568,9 @@ def test_parse_from_xml(self, data):
'white_listed_file_types',
'allow_multiple_files',
'allow_latex',
+ 'allow_learner_resubmissions',
+ 'resubmissions_grace_period_hours',
+ 'resubmissions_grace_period_minutes',
'leaderboard_show'
]
for field_name in expected_fields:
diff --git a/openassessment/xblock/utils/allow_resubmission.py b/openassessment/xblock/utils/allow_resubmission.py
index 058a24a6c4..a9d63ab265 100644
--- a/openassessment/xblock/utils/allow_resubmission.py
+++ b/openassessment/xblock/utils/allow_resubmission.py
@@ -95,8 +95,4 @@ def has_been_graded(workflow_data) -> bool:
if graded_by_count is not None and graded_by_count > 0:
return True
- # If the learner has been graded by staff
- if status_details.get("staff", {}).get("graded"):
- return True
-
return False