From 36f3895eafe7bfbb218d278c3ee2488b2e41d3a3 Mon Sep 17 00:00:00 2001 From: hajorg Date: Fri, 22 Mar 2024 10:32:54 +0100 Subject: [PATCH 1/3] feat: api function to delete learner's course grades --- lms/djangoapps/grades/models.py | 41 +++++++ lms/djangoapps/grades/models_api.py | 15 +++ lms/djangoapps/grades/tests/test_api.py | 134 ++++++++++++++++++++- lms/djangoapps/grades/tests/test_models.py | 48 ++++++++ lms/envs/devstack.py | 1 + 5 files changed, 236 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index b36d84794b3b..ca8e3957533e 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -534,6 +534,21 @@ def _emit_grade_calculated_event(grade): def _cache_key(cls, course_id): return f"subsection_grades_cache.{course_id}" + @classmethod + def clear_grade(cls, user_id, course_key): + """ + Clears Subsection grade override for a learner in a course + Arguments: + user_id: The user associated with the desired grade + course_id: The id of the course associated with the desired grade + """ + deleted_count, _ = cls.objects.filter( + user_id=user_id, + course_id=course_key, + ).delete() + cls.clear_prefetched_data(course_key) + return deleted_count + class PersistentCourseGrade(TimeStampedModel): """ @@ -681,6 +696,18 @@ def _cache_key(cls, course_id): def _emit_grade_calculated_event(grade): events.course_grade_calculated(grade) + @classmethod + def clear_grade(cls, course_id, user_id): + """ + Clears course grade for a learner in a course + Arguments: + course_id: The id of the course associated with the desired grade + user_id: The user associated with the desired grade + """ + deleted_count, _ = cls.objects.filter(user_id=user_id, course_id=course_id).delete() + cls.clear_prefetched_data(course_id) + return deleted_count + @staticmethod def _emit_openedx_persistent_grade_summary_changed_event(course_id, user_id, grade): """ @@ -828,3 +855,17 @@ def _prepare_override_params(subsection_grade_model, override_data): getattr(subsection_grade_model, field_name) ) return cleaned_data + + @classmethod + def clear_override(cls, user_id, course_key): + """ + Clears Subsection grade override for a learner in a course + Arguments: + user_id: The user associated with the desired grade + course_id: The id of the course associated with the desired grade + """ + total, _ = cls.objects.filter( + grade__user_id=user_id, + grade__course_id=course_key + ).delete() + return total diff --git a/lms/djangoapps/grades/models_api.py b/lms/djangoapps/grades/models_api.py index 5851c5439f63..a12a5347849b 100644 --- a/lms/djangoapps/grades/models_api.py +++ b/lms/djangoapps/grades/models_api.py @@ -2,6 +2,7 @@ Provides Python APIs exposed from Grades models. """ +from django.db import transaction from opaque_keys.edx.keys import CourseKey, UsageKey @@ -99,3 +100,17 @@ def get_subsection_grade_override(user_id, course_key_or_id, usage_key_or_id): _ = get_subsection_grade(user_id, course_key_or_id, usage_key_or_id) return _PersistentSubsectionGradeOverride.get_override(user_id, usage_key) + + +def clear_user_course_grades(user_id, course_key): + """ + Given a user_id and course_key, clears persistent grades for a learner in a course + """ + with transaction.atomic(): + try: + _PersistentSubsectionGradeOverride.clear_override(user_id, course_key) + _PersistentSubsectionGrade.clear_grade(user_id, course_key) + _PersistentCourseGrade.clear_grade(course_key, user_id) + return 'Grades deleted Successfully' + except Exception as e: # pylint: disable=broad-except + return f'Error deleting grades: {str(e)}' diff --git a/lms/djangoapps/grades/tests/test_api.py b/lms/djangoapps/grades/tests/test_api.py index 771a0637ef39..d89d301bb7ff 100644 --- a/lms/djangoapps/grades/tests/test_api.py +++ b/lms/djangoapps/grades/tests/test_api.py @@ -1,13 +1,17 @@ """ Tests calling the grades api directly """ -from unittest.mock import patch +from unittest.mock import patch, Mock import ddt from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.grades import api -from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride +from lms.djangoapps.grades.models import ( + PersistentSubsectionGrade, + PersistentSubsectionGradeOverride, + PersistentCourseGrade +) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order @@ -56,7 +60,7 @@ def setUp(self): def tearDown(self): super().tearDown() - PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides + PersistentSubsectionGradeOverride.objects.all().delete() @ddt.data(0.0, None, 3.0) def test_override_subsection_grade(self, earned_graded): @@ -108,3 +112,127 @@ def test_override_subsection_grade(self, earned_graded): else: assert history_entry.history_user is None assert history_entry.history_user_id is None + + +class ClearGradeTests(ModuleStoreTestCase): + """ + Tests for the clearing grades api call + """ + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.user = UserFactory() + cls.overriding_user = UserFactory() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019') + self.subsection = BlockFactory.create(parent=self.course, category="sequential", display_name="Subsection") + self.grade = PersistentSubsectionGrade.update_or_create_grade( + user_id=self.user.id, + course_id=self.course.id, + usage_key=self.subsection.location, + first_attempted=None, + visible_blocks=[], + earned_all=6.0, + possible_all=6.0, + earned_graded=5.0, + possible_graded=5.0 + ) + self.params = { + "user_id": self.user.id, + "course_id": self.course.id, + "course_version": "JoeMcEwing", + "percent_grade": 77.7, + "letter_grade": "Great job", + "passed": True, + } + PersistentCourseGrade.update_or_create(**self.params) + + def tearDown(self): + super().tearDown() + PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides + + def test_clear_user_course_grades(self): + api.override_subsection_grade( + self.user.id, + self.course.id, + self.subsection.location, + overrider=self.overriding_user, + earned_graded=0.0, + comment='Test Override Comment', + ) + override_obj = api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + self.assertIsNotNone(course_grade) + self.assertIsNotNone(override_obj) + + api.clear_user_course_grades(self.user.id, self.course.id) + with self.assertRaises(PersistentCourseGrade.DoesNotExist): + PersistentCourseGrade.read(self.user.id, self.course.id) + + with self.assertRaises(PersistentSubsectionGrade.DoesNotExist): + api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + + def test_clear_wrong_user_course_grades(self): + wrong_user = UserFactory() + api.override_subsection_grade( + self.user.id, + self.course.id, + self.subsection.location, + overrider=self.overriding_user, + earned_graded=0.0, + comment='Test Override Comment', + ) + override_obj = api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + self.assertIsNotNone(course_grade) + self.assertIsNotNone(override_obj) + + api.clear_user_course_grades(wrong_user.id, self.course.id) + + after_clear_override_obj = api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + after_clear_course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + self.assertIsNotNone(after_clear_override_obj) + self.assertIsNotNone(after_clear_course_grade) + + @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') + @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') + @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGradeOverride') + def test_assert_clear_grade_methods_called(self, mock_override, mock_course_grade, mock_subsection_grade): + api.clear_user_course_grades(self.user.id, self.course.id) + mock_override.clear_override.assert_called_with(self.user.id, self.course.id) + mock_course_grade.clear_grade.assert_called_with(self.course.id, self.user.id) + mock_subsection_grade.clear_grade.assert_called_with(self.user.id, self.course.id) + + @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') + @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') + def test_assert_clear_grade_exception(self, mock_course_grade, mock_subsection_grade): + with patch( + 'lms.djangoapps.grades.models_api._PersistentSubsectionGradeOverride', + Mock(side_effect=Exception) + ) as mock_override: + api.clear_user_course_grades(self.user.id, self.course.id) + self.assertRaises(Exception, mock_override) + self.assertFalse(mock_course_grade.called) + self.assertFalse(mock_subsection_grade.called) diff --git a/lms/djangoapps/grades/tests/test_models.py b/lms/djangoapps/grades/tests/test_models.py index 5405b03e9479..a10d4e68f3b3 100644 --- a/lms/djangoapps/grades/tests/test_models.py +++ b/lms/djangoapps/grades/tests/test_models.py @@ -346,6 +346,23 @@ def _assert_tracker_emitted_event(self, tracker_mock, grade): } ) + def test_clear_subsection_grade(self): + PersistentSubsectionGrade.update_or_create_grade(**self.params) + deleted = PersistentSubsectionGrade.clear_grade(self.user.id, self.course_key) + self.assertEqual(deleted, 1) + + def test_clear_subsection_grade_override(self): + grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) + PersistentSubsectionGradeOverride.update_or_create_override( + requesting_user=self.user, + subsection_grade_model=grade, + earned_all_override=0.0, + earned_graded_override=0.0, + feature=GradeOverrideFeatureEnum.gradebook, + ) + deleted = PersistentSubsectionGradeOverride.clear_override(self.user.id, self.course_key) + self.assertEqual(deleted, 1) + @ddt.ddt class PersistentCourseGradesTest(GradesModelTestCase): @@ -490,3 +507,34 @@ def _assert_tracker_emitted_event(self, tracker_mock, grade): 'grading_policy_hash': str(grade.grading_policy_hash), } ) + + def test_clear_grade(self): + another_params = { + "user_id": 123456, + "course_id": self.course_key, + "course_version": "JoeMcEwing", + "course_edited_timestamp": datetime( + year=2016, + month=8, + day=1, + hour=18, + minute=53, + second=24, + microsecond=354741, + tzinfo=pytz.UTC, + ), + "percent_grade": 77.8, + "letter_grade": "Great job", + "passed": True, + } + + UserFactory(id=another_params['user_id']) + + PersistentCourseGrade.update_or_create(**self.params) + PersistentCourseGrade.update_or_create(**another_params) + + deleted_user_grades = PersistentCourseGrade.clear_grade(self.course_key, self.params['user_id']) + another_user_grade = PersistentCourseGrade.read(another_params['user_id'], self.course_key) + + self.assertEqual(deleted_user_grades, 1) + self.assertIsNotNone(another_user_grade) diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index a1edf2019813..11726120f86b 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -560,6 +560,7 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing 'http://localhost:2002', # frontend-app-discussions 'http://localhost:1991', # frontend-app-admin-portal 'http://localhost:1999', # frontend-app-authn + 'http://localhost:18450', # frontend-app-support-tools ] From 3bdd1e0a8a19700e3a5327c77f0536c8f72f15da Mon Sep 17 00:00:00 2001 From: hajorg Date: Mon, 25 Mar 2024 15:22:25 +0100 Subject: [PATCH 2/3] feat: update grade delete methods --- lms/djangoapps/grades/models.py | 42 +++++++++++----------- lms/djangoapps/grades/models_api.py | 5 ++- lms/djangoapps/grades/tests/test_api.py | 9 +++-- lms/djangoapps/grades/tests/test_models.py | 17 +++++---- 4 files changed, 37 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index ca8e3957533e..1fface394da3 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -535,18 +535,24 @@ def _cache_key(cls, course_id): return f"subsection_grades_cache.{course_id}" @classmethod - def clear_grade(cls, user_id, course_key): + def delete_subsection_grades_for_learner(cls, user_id, course_key): """ Clears Subsection grade override for a learner in a course Arguments: user_id: The user associated with the desired grade course_id: The id of the course associated with the desired grade """ - deleted_count, _ = cls.objects.filter( - user_id=user_id, - course_id=course_key, - ).delete() - cls.clear_prefetched_data(course_key) + try: + deleted_count, deleted_obj = cls.objects.filter( + user_id=user_id, + course_id=course_key, + ).delete() + get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_key)].pop(user_id) + if deleted_obj['grades.PersistentSubsectionGradeOverride'] is not None: + PersistentSubsectionGradeOverride.clear_prefetched_overrides_for_learner(user_id, course_key) + except KeyError: + pass + return deleted_count @@ -697,16 +703,18 @@ def _emit_grade_calculated_event(grade): events.course_grade_calculated(grade) @classmethod - def clear_grade(cls, course_id, user_id): + def delete_course_grade_for_learner(cls, course_id, user_id): """ Clears course grade for a learner in a course Arguments: course_id: The id of the course associated with the desired grade user_id: The user associated with the desired grade """ - deleted_count, _ = cls.objects.filter(user_id=user_id, course_id=course_id).delete() - cls.clear_prefetched_data(course_id) - return deleted_count + try: + cls.objects.get(user_id=user_id, course_id=course_id).delete() + get_cache(cls._CACHE_NAMESPACE)[cls._cache_key(course_id)].pop(user_id) + except (PersistentCourseGrade.DoesNotExist, KeyError): + pass @staticmethod def _emit_openedx_persistent_grade_summary_changed_event(course_id, user_id, grade): @@ -857,15 +865,5 @@ def _prepare_override_params(subsection_grade_model, override_data): return cleaned_data @classmethod - def clear_override(cls, user_id, course_key): - """ - Clears Subsection grade override for a learner in a course - Arguments: - user_id: The user associated with the desired grade - course_id: The id of the course associated with the desired grade - """ - total, _ = cls.objects.filter( - grade__user_id=user_id, - grade__course_id=course_key - ).delete() - return total + def clear_prefetched_overrides_for_learner(cls, user_id, course_key): + get_cache(cls._CACHE_NAMESPACE).pop((user_id, str(course_key)), None) diff --git a/lms/djangoapps/grades/models_api.py b/lms/djangoapps/grades/models_api.py index a12a5347849b..5eef8d236952 100644 --- a/lms/djangoapps/grades/models_api.py +++ b/lms/djangoapps/grades/models_api.py @@ -108,9 +108,8 @@ def clear_user_course_grades(user_id, course_key): """ with transaction.atomic(): try: - _PersistentSubsectionGradeOverride.clear_override(user_id, course_key) - _PersistentSubsectionGrade.clear_grade(user_id, course_key) - _PersistentCourseGrade.clear_grade(course_key, user_id) + _PersistentSubsectionGrade.delete_subsection_grades_for_learner(user_id, course_key) + _PersistentCourseGrade.delete_course_grade_for_learner(course_key, user_id) return 'Grades deleted Successfully' except Exception as e: # pylint: disable=broad-except return f'Error deleting grades: {str(e)}' diff --git a/lms/djangoapps/grades/tests/test_api.py b/lms/djangoapps/grades/tests/test_api.py index d89d301bb7ff..8848bb63c9c8 100644 --- a/lms/djangoapps/grades/tests/test_api.py +++ b/lms/djangoapps/grades/tests/test_api.py @@ -176,6 +176,7 @@ def test_clear_user_course_grades(self): self.assertIsNotNone(override_obj) api.clear_user_course_grades(self.user.id, self.course.id) + with self.assertRaises(PersistentCourseGrade.DoesNotExist): PersistentCourseGrade.read(self.user.id, self.course.id) @@ -218,12 +219,10 @@ def test_clear_wrong_user_course_grades(self): @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') - @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGradeOverride') - def test_assert_clear_grade_methods_called(self, mock_override, mock_course_grade, mock_subsection_grade): + def test_assert_clear_grade_methods_called(self, mock_course_grade, mock_subsection_grade): api.clear_user_course_grades(self.user.id, self.course.id) - mock_override.clear_override.assert_called_with(self.user.id, self.course.id) - mock_course_grade.clear_grade.assert_called_with(self.course.id, self.user.id) - mock_subsection_grade.clear_grade.assert_called_with(self.user.id, self.course.id) + mock_course_grade.delete_course_grade_for_learner.assert_called_with(self.course.id, self.user.id) + mock_subsection_grade.delete_subsection_grades_for_learner.assert_called_with(self.user.id, self.course.id) @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') diff --git a/lms/djangoapps/grades/tests/test_models.py b/lms/djangoapps/grades/tests/test_models.py index a10d4e68f3b3..51278706b918 100644 --- a/lms/djangoapps/grades/tests/test_models.py +++ b/lms/djangoapps/grades/tests/test_models.py @@ -348,7 +348,9 @@ def _assert_tracker_emitted_event(self, tracker_mock, grade): def test_clear_subsection_grade(self): PersistentSubsectionGrade.update_or_create_grade(**self.params) - deleted = PersistentSubsectionGrade.clear_grade(self.user.id, self.course_key) + deleted = PersistentSubsectionGrade.delete_subsection_grades_for_learner( + self.user.id, self.course_key + ) self.assertEqual(deleted, 1) def test_clear_subsection_grade_override(self): @@ -360,8 +362,8 @@ def test_clear_subsection_grade_override(self): earned_graded_override=0.0, feature=GradeOverrideFeatureEnum.gradebook, ) - deleted = PersistentSubsectionGradeOverride.clear_override(self.user.id, self.course_key) - self.assertEqual(deleted, 1) + deleted = PersistentSubsectionGrade.delete_subsection_grades_for_learner(self.user.id, self.course_key) + self.assertEqual(deleted, 2) @ddt.ddt @@ -533,8 +535,11 @@ def test_clear_grade(self): PersistentCourseGrade.update_or_create(**self.params) PersistentCourseGrade.update_or_create(**another_params) - deleted_user_grades = PersistentCourseGrade.clear_grade(self.course_key, self.params['user_id']) - another_user_grade = PersistentCourseGrade.read(another_params['user_id'], self.course_key) + PersistentCourseGrade.delete_course_grade_for_learner( + self.course_key, self.params['user_id'] + ) + with self.assertRaises(PersistentCourseGrade.DoesNotExist): + PersistentCourseGrade.read(self.params['user_id'], self.course_key) - self.assertEqual(deleted_user_grades, 1) + another_user_grade = PersistentCourseGrade.read(another_params['user_id'], self.course_key) self.assertIsNotNone(another_user_grade) From 1c3f403f38e59836a44f04868426243682a2cddd Mon Sep 17 00:00:00 2001 From: hajorg Date: Tue, 26 Mar 2024 11:11:13 +0100 Subject: [PATCH 3/3] fix: update and cleanup tests --- lms/djangoapps/grades/models.py | 2 +- lms/djangoapps/grades/models_api.py | 8 +-- lms/djangoapps/grades/tests/test_api.py | 61 ++++++++++++---------- lms/djangoapps/grades/tests/test_models.py | 49 +++++++++-------- 4 files changed, 64 insertions(+), 56 deletions(-) diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index 1fface394da3..a5608eb39afd 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -537,7 +537,7 @@ def _cache_key(cls, course_id): @classmethod def delete_subsection_grades_for_learner(cls, user_id, course_key): """ - Clears Subsection grade override for a learner in a course + Clears Subsection grades and overrides for a learner in a course Arguments: user_id: The user associated with the desired grade course_id: The id of the course associated with the desired grade diff --git a/lms/djangoapps/grades/models_api.py b/lms/djangoapps/grades/models_api.py index 5eef8d236952..0466fb2a56ac 100644 --- a/lms/djangoapps/grades/models_api.py +++ b/lms/djangoapps/grades/models_api.py @@ -107,9 +107,5 @@ def clear_user_course_grades(user_id, course_key): Given a user_id and course_key, clears persistent grades for a learner in a course """ with transaction.atomic(): - try: - _PersistentSubsectionGrade.delete_subsection_grades_for_learner(user_id, course_key) - _PersistentCourseGrade.delete_course_grade_for_learner(course_key, user_id) - return 'Grades deleted Successfully' - except Exception as e: # pylint: disable=broad-except - return f'Error deleting grades: {str(e)}' + _PersistentSubsectionGrade.delete_subsection_grades_for_learner(user_id, course_key) + _PersistentCourseGrade.delete_course_grade_for_learner(course_key, user_id) diff --git a/lms/djangoapps/grades/tests/test_api.py b/lms/djangoapps/grades/tests/test_api.py index 8848bb63c9c8..e42e9ea56f6d 100644 --- a/lms/djangoapps/grades/tests/test_api.py +++ b/lms/djangoapps/grades/tests/test_api.py @@ -1,7 +1,7 @@ """ Tests calling the grades api directly """ -from unittest.mock import patch, Mock +from unittest.mock import patch import ddt @@ -44,7 +44,7 @@ def tearDownClass(cls): def setUp(self): super().setUp() - self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019') + self.course = CourseFactory.create() self.subsection = BlockFactory.create(parent=self.course, category="sequential", display_name="Subsection") self.grade = PersistentSubsectionGrade.update_or_create_grade( user_id=self.user.id, @@ -130,8 +130,8 @@ def tearDownClass(cls): def setUp(self): super().setUp() - self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019') - self.subsection = BlockFactory.create(parent=self.course, category="sequential", display_name="Subsection") + self.course = CourseFactory.create() + self.subsection = BlockFactory.create(parent=self.course) self.grade = PersistentSubsectionGrade.update_or_create_grade( user_id=self.user.id, course_id=self.course.id, @@ -187,35 +187,52 @@ def test_clear_user_course_grades(self): self.subsection.location ) - def test_clear_wrong_user_course_grades(self): - wrong_user = UserFactory() + def _create_and_get_user_grades(self, user_id): + """ Creates grades for a user and override object """ api.override_subsection_grade( - self.user.id, + user_id, self.course.id, self.subsection.location, overrider=self.overriding_user, earned_graded=0.0, comment='Test Override Comment', ) - override_obj = api.get_subsection_grade_override( - self.user.id, + return api.get_subsection_grade_override( + user_id, self.course.id, self.subsection.location ) - course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) - self.assertIsNotNone(course_grade) - self.assertIsNotNone(override_obj) - - api.clear_user_course_grades(wrong_user.id, self.course.id) + def test_clear_other_user_course_grades(self): + """ + Make sure it deletes grades for other_user and not self.user + """ + # Create grades for 2 users + other_user = UserFactory() + user_override_obj = self._create_and_get_user_grades(self.user.id) + other_user_override_obj = self._create_and_get_user_grades(other_user.id) + + # fetch and assert grades are available for both users + user_course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + other_user_course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + self.assertIsNotNone(user_course_grade) + self.assertIsNotNone(user_override_obj) + self.assertIsNotNone(other_user_override_obj) + self.assertIsNotNone(other_user_course_grade) + + api.clear_user_course_grades(other_user.id, self.course.id) + + # assert grades after deletion for other_user after_clear_override_obj = api.get_subsection_grade_override( self.user.id, self.course.id, self.subsection.location ) - after_clear_course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + after_clear_user_course_grade = PersistentCourseGrade.read(self.user.id, self.course.id) + with self.assertRaises(PersistentCourseGrade.DoesNotExist): + PersistentCourseGrade.read(other_user.id, self.course.id) self.assertIsNotNone(after_clear_override_obj) - self.assertIsNotNone(after_clear_course_grade) + self.assertIsNotNone(after_clear_user_course_grade) @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') @@ -223,15 +240,3 @@ def test_assert_clear_grade_methods_called(self, mock_course_grade, mock_subsect api.clear_user_course_grades(self.user.id, self.course.id) mock_course_grade.delete_course_grade_for_learner.assert_called_with(self.course.id, self.user.id) mock_subsection_grade.delete_subsection_grades_for_learner.assert_called_with(self.user.id, self.course.id) - - @patch('lms.djangoapps.grades.models_api._PersistentSubsectionGrade') - @patch('lms.djangoapps.grades.models_api._PersistentCourseGrade') - def test_assert_clear_grade_exception(self, mock_course_grade, mock_subsection_grade): - with patch( - 'lms.djangoapps.grades.models_api._PersistentSubsectionGradeOverride', - Mock(side_effect=Exception) - ) as mock_override: - api.clear_user_course_grades(self.user.id, self.course.id) - self.assertRaises(Exception, mock_override) - self.assertFalse(mock_course_grade.called) - self.assertFalse(mock_subsection_grade.called) diff --git a/lms/djangoapps/grades/tests/test_models.py b/lms/djangoapps/grades/tests/test_models.py index 51278706b918..acdb20c5f1a7 100644 --- a/lms/djangoapps/grades/tests/test_models.py +++ b/lms/djangoapps/grades/tests/test_models.py @@ -352,6 +352,9 @@ def test_clear_subsection_grade(self): self.user.id, self.course_key ) self.assertEqual(deleted, 1) + self.assertFalse(PersistentSubsectionGrade.objects.filter( + user_id=self.user.id, course_id=self.course_key).exists() + ) def test_clear_subsection_grade_override(self): grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) @@ -510,36 +513,40 @@ def _assert_tracker_emitted_event(self, tracker_mock, grade): } ) - def test_clear_grade(self): - another_params = { - "user_id": 123456, - "course_id": self.course_key, - "course_version": "JoeMcEwing", - "course_edited_timestamp": datetime( - year=2016, - month=8, - day=1, - hour=18, - minute=53, - second=24, - microsecond=354741, - tzinfo=pytz.UTC, - ), - "percent_grade": 77.8, - "letter_grade": "Great job", - "passed": True, + def test_clear_course_grade(self): + # create params for another user and another course + other_user = UserFactory.create() + other_user_params = { + **self.params, + 'user_id': other_user.id } - UserFactory(id=another_params['user_id']) + other_course_key = CourseLocator( + org='some_org', + course='some_other_course', + run='some_run' + ) + user_other_course_params = { + **self.params, + 'course_id': other_course_key + } + # create course grades based on different params PersistentCourseGrade.update_or_create(**self.params) - PersistentCourseGrade.update_or_create(**another_params) + PersistentCourseGrade.update_or_create(**other_user_params) + PersistentCourseGrade.update_or_create(**user_other_course_params) PersistentCourseGrade.delete_course_grade_for_learner( self.course_key, self.params['user_id'] ) + + # assert after deleteing grade for a single user and course with self.assertRaises(PersistentCourseGrade.DoesNotExist): PersistentCourseGrade.read(self.params['user_id'], self.course_key) - another_user_grade = PersistentCourseGrade.read(another_params['user_id'], self.course_key) + another_user_grade = PersistentCourseGrade.read(other_user_params['user_id'], self.course_key) self.assertIsNotNone(another_user_grade) + + self.assertTrue(PersistentCourseGrade.objects.filter( + user_id=self.params['user_id'], course_id=other_course_key).exists() + )