feat: api function to delete learner's course grades
This commit is contained in:
@@ -534,6 +534,21 @@ class PersistentSubsectionGrade(TimeStampedModel):
|
||||
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 @@ class PersistentCourseGrade(TimeStampedModel):
|
||||
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 @@ class PersistentSubsectionGradeOverride(models.Model):
|
||||
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
|
||||
|
||||
@@ -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)}'
|
||||
|
||||
@@ -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 @@ class OverrideSubsectionGradeTests(ModuleStoreTestCase):
|
||||
|
||||
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 @@ class OverrideSubsectionGradeTests(ModuleStoreTestCase):
|
||||
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)
|
||||
|
||||
@@ -346,6 +346,23 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
|
||||
}
|
||||
)
|
||||
|
||||
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 @@ class PersistentCourseGradesTest(GradesModelTestCase):
|
||||
'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)
|
||||
|
||||
@@ -560,6 +560,7 @@ CSRF_TRUSTED_ORIGINS = [
|
||||
'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
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user