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