Files
edx-platform/lms/djangoapps/grades/tests/test_services.py
Michael Terry cb1bb7fa64 test: switch default test store to the split store
It's long past time that the default test modulestore was Split,
instead of Old Mongo. This commit switches the default store and
fixes some tests that now fail:
- Tests that didn't expect MFE to be enabled (because we don't
  enable MFE for Old Mongo) - opt out of MFE for those
- Tests that hardcoded old key string formats
- Lots of other random little differences

In many places, I didn't spend much time trying to figure out how to
properly fix the test, and instead just set the modulestore to Old
Mongo.

For those tests that I didn't spend time investigating, I've set
the modulestore to TEST_DATA_MONGO_AMNESTY_MODULESTORE - search for
that string to find further work.
2022-02-04 14:32:50 -05:00

311 lines
11 KiB
Python

"""
Grades Service Tests
"""
from datetime import datetime
from unittest.mock import call, patch
import ddt
import pytz
from freezegun import freeze_time
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from lms.djangoapps.grades.services import GradesService
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory # lint-amnesty, pylint: disable=wrong-import-order
from ..config.waffle import REJECTED_EXAM_OVERRIDES_GRADE
from ..constants import ScoreDatabaseTableEnum
class MockWaffleFlag:
"""
A Mock WaffleFlag object.
"""
def __init__(self, state):
self.state = state
# pylint: disable=unused-argument
def is_enabled(self, course_key):
return self.state
@ddt.ddt
class GradesServiceTests(ModuleStoreTestCase):
"""
Tests for the Grades service
"""
def setUp(self):
super().setUp()
self.service = GradesService()
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019')
self.subsection = ItemFactory.create(parent=self.course, category="sequential", display_name="Subsection")
self.subsection_without_grade = ItemFactory.create(
parent=self.course,
category="sequential",
display_name="Subsection without grade"
)
self.user = UserFactory()
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.signal_patcher = patch('lms.djangoapps.grades.signals.signals.SUBSECTION_OVERRIDE_CHANGED.send')
self.mock_signal = self.signal_patcher.start()
self.id_patcher = patch('lms.djangoapps.grades.api.create_new_event_transaction_id')
self.mock_create_id = self.id_patcher.start()
self.mock_create_id.return_value = 1
self.type_patcher = patch('lms.djangoapps.grades.api.set_event_transaction_type')
self.mock_set_type = self.type_patcher.start()
self.flag_patcher = patch('lms.djangoapps.grades.config.waffle.waffle_flags')
self.mock_waffle_flags = self.flag_patcher.start()
self.mock_waffle_flags.return_value = {
REJECTED_EXAM_OVERRIDES_GRADE: MockWaffleFlag(True)
}
def tearDown(self):
super().tearDown()
PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides
self.signal_patcher.stop()
self.id_patcher.stop()
self.type_patcher.stop()
self.flag_patcher.stop()
def subsection_grade_to_dict(self, grade):
return {
'earned_all': grade.earned_all,
'earned_graded': grade.earned_graded
}
def subsection_grade_override_to_dict(self, grade):
return {
'earned_all_override': grade.earned_all_override,
'earned_graded_override': grade.earned_graded_override
}
def test_get_subsection_grade(self):
self.assertDictEqual(self.subsection_grade_to_dict(self.service.get_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location
)), {
'earned_all': 6.0,
'earned_graded': 5.0
})
# test with id strings as parameters instead
self.assertDictEqual(self.subsection_grade_to_dict(self.service.get_subsection_grade(
user_id=self.user.id,
course_key_or_id=str(self.course.id),
usage_key_or_id=str(self.subsection.location)
)), {
'earned_all': 6.0,
'earned_graded': 5.0
})
def test_get_subsection_grade_override(self):
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(grade=self.grade)
self.assertDictEqual(self.subsection_grade_override_to_dict(self.service.get_subsection_grade_override(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location
)), {
'earned_all_override': override.earned_all_override,
'earned_graded_override': override.earned_graded_override
})
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
grade=self.grade,
defaults={
'earned_all_override': 9.0
}
)
# test with course key parameter as string instead
self.assertDictEqual(self.subsection_grade_override_to_dict(self.service.get_subsection_grade_override(
user_id=self.user.id,
course_key_or_id=str(self.course.id),
usage_key_or_id=self.subsection.location
)), {
'earned_all_override': override.earned_all_override,
'earned_graded_override': override.earned_graded_override
})
@ddt.data(
{
'earned_all': 0.0,
'earned_graded': 0.0
},
{
'earned_all': 0.0,
'earned_graded': None
},
{
'earned_all': None,
'earned_graded': None
},
{
'earned_all': 3.0,
'earned_graded': 2.0
},
)
def test_override_subsection_grade(self, override):
self.service.override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location,
earned_all=override['earned_all'],
earned_graded=override['earned_graded']
)
override_obj = self.service.get_subsection_grade_override(
self.user.id,
self.course.id,
self.subsection.location
)
assert override_obj is not None
assert override_obj.earned_all_override == override['earned_all']
assert override_obj.earned_graded_override == override['earned_graded']
assert self.mock_signal.call_args == call(
sender=None,
user_id=self.user.id,
course_id=str(self.course.id),
usage_id=str(self.subsection.location),
only_if_higher=False,
modified=override_obj.modified,
score_deleted=False,
score_db_table=ScoreDatabaseTableEnum.overrides
)
def test_override_subsection_grade_no_psg(self):
"""
When there is no PersistentSubsectionGrade associated with the learner
and subsection to override, one should be created.
"""
earned_all_override = 2
earned_graded_override = 0
self.service.override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection_without_grade.location,
earned_all=earned_all_override,
earned_graded=earned_graded_override
)
# Assert that a new PersistentSubsectionGrade was created
subsection_grade = self.service.get_subsection_grade(
self.user.id,
self.course.id,
self.subsection_without_grade.location
)
assert subsection_grade is not None
assert 0 == subsection_grade.earned_all
assert 0 == subsection_grade.earned_graded
# Now assert things about the grade override
override_obj = self.service.get_subsection_grade_override(
self.user.id,
self.course.id,
self.subsection_without_grade.location
)
assert override_obj is not None
assert override_obj.earned_all_override == earned_all_override
assert override_obj.earned_graded_override == earned_graded_override
assert self.mock_signal.call_args == call(
sender=None,
user_id=self.user.id,
course_id=str(self.course.id),
usage_id=str(self.subsection_without_grade.location),
only_if_higher=False,
modified=override_obj.modified,
score_deleted=False,
score_db_table=ScoreDatabaseTableEnum.overrides
)
@freeze_time('2017-01-01')
def test_undo_override_subsection_grade(self):
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
grade=self.grade,
system=GradeOverrideFeatureEnum.proctoring
)
override_id = override.id # lint-amnesty, pylint: disable=unused-variable
self.service.undo_override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location,
)
override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location)
assert override is None
assert self.mock_signal.call_args == call(
sender=None,
user_id=self.user.id,
course_id=str(self.course.id),
usage_id=str(self.subsection.location),
only_if_higher=False,
modified=datetime.now().replace(tzinfo=pytz.UTC),
score_deleted=True,
score_db_table=ScoreDatabaseTableEnum.overrides
)
def test_undo_override_subsection_grade_across_features(self):
"""
Test that deletion of subsection grade overrides requested by
one feature doesn't delete overrides created by another
feature.
"""
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
grade=self.grade,
system=GradeOverrideFeatureEnum.gradebook
)
self.service.undo_override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location,
feature=GradeOverrideFeatureEnum.proctoring,
)
override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location)
assert override is not None
@freeze_time('2018-01-01')
def test_undo_override_subsection_grade_without_grade(self):
"""
Test exception handling of `undo_override_subsection_grade` when PersistentSubsectionGrade
does not exist.
"""
self.grade.delete()
try:
self.service.undo_override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
usage_key_or_id=self.subsection.location,
)
except PersistentSubsectionGrade.DoesNotExist:
assert False, 'Exception raised unexpectedly'
assert not self.mock_signal.called
def test_should_override_grade_on_rejected_exam(self):
assert self.service.should_override_grade_on_rejected_exam('course-v1:edX+DemoX+Demo_Course')
self.mock_waffle_flags.return_value = {
REJECTED_EXAM_OVERRIDES_GRADE: MockWaffleFlag(False)
}
assert not self.service.should_override_grade_on_rejected_exam('course-v1:edX+DemoX+Demo_Course')