706 lines
28 KiB
Python
706 lines
28 KiB
Python
"""
|
|
Test saved subsection grade functionality.
|
|
"""
|
|
# pylint: disable=protected-access
|
|
|
|
import datetime
|
|
|
|
import ddt
|
|
from django.conf import settings
|
|
import itertools
|
|
|
|
from mock import patch
|
|
import pytz
|
|
|
|
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
|
from courseware.access import has_access
|
|
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
|
|
from lms.djangoapps.course_blocks.api import get_course_blocks
|
|
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
|
|
from openedx.core.djangolib.testing.utils import get_mock_request
|
|
from student.models import CourseEnrollment
|
|
from student.tests.factories import UserFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
|
from xmodule.modulestore.tests.utils import TEST_DATA_DIR
|
|
from xmodule.modulestore.xml_importer import import_course_from_xml
|
|
|
|
from ..config.waffle import waffle, ASSUME_ZERO_GRADE_IF_ABSENT, WRITE_ONLY_IF_ENGAGED
|
|
from ..models import PersistentSubsectionGrade
|
|
from ..new.course_data import CourseData
|
|
from ..new.course_grade_factory import CourseGradeFactory
|
|
from ..new.course_grade import ZeroCourseGrade, CourseGrade
|
|
from ..new.subsection_grade_factory import SubsectionGradeFactory
|
|
from ..new.subsection_grade import ZeroSubsectionGrade, SubsectionGrade
|
|
from .utils import mock_get_score, mock_get_submissions_score
|
|
|
|
|
|
class GradeTestBase(SharedModuleStoreTestCase):
|
|
"""
|
|
Base class for Course- and SubsectionGradeFactory tests.
|
|
"""
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(GradeTestBase, cls).setUpClass()
|
|
cls.course = CourseFactory.create()
|
|
with cls.store.bulk_operations(cls.course.id):
|
|
cls.chapter = ItemFactory.create(
|
|
parent=cls.course,
|
|
category="chapter",
|
|
display_name="Test Chapter"
|
|
)
|
|
cls.sequence = ItemFactory.create(
|
|
parent=cls.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 1",
|
|
graded=True,
|
|
format="Homework"
|
|
)
|
|
cls.vertical = ItemFactory.create(
|
|
parent=cls.sequence,
|
|
category='vertical',
|
|
display_name='Test Vertical 1'
|
|
)
|
|
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
|
question_text='The correct answer is Choice 3',
|
|
choices=[False, False, True, False],
|
|
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
|
)
|
|
cls.problem = ItemFactory.create(
|
|
parent=cls.vertical,
|
|
category="problem",
|
|
display_name="Test Problem",
|
|
data=problem_xml
|
|
)
|
|
cls.sequence2 = ItemFactory.create(
|
|
parent=cls.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 2",
|
|
graded=True,
|
|
format="Homework"
|
|
)
|
|
cls.problem2 = ItemFactory.create(
|
|
parent=cls.sequence2,
|
|
category="problem",
|
|
display_name="Test Problem",
|
|
data=problem_xml
|
|
)
|
|
|
|
def setUp(self):
|
|
super(GradeTestBase, self).setUp()
|
|
self.request = get_mock_request(UserFactory())
|
|
self.client.login(username=self.request.user.username, password="test")
|
|
self._update_grading_policy()
|
|
self.course_structure = get_course_blocks(self.request.user, self.course.location)
|
|
self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure)
|
|
CourseEnrollment.enroll(self.request.user, self.course.id)
|
|
|
|
def _update_grading_policy(self, passing=0.5):
|
|
"""
|
|
Updates the course's grading policy.
|
|
"""
|
|
self.grading_policy = {
|
|
"GRADER": [
|
|
{
|
|
"type": "Homework",
|
|
"min_count": 1,
|
|
"drop_count": 0,
|
|
"short_label": "HW",
|
|
"weight": 1.0,
|
|
},
|
|
],
|
|
"GRADE_CUTOFFS": {
|
|
"Pass": passing,
|
|
},
|
|
}
|
|
self.course.set_grading_policy(self.grading_policy)
|
|
self.store.update_item(self.course, 0)
|
|
|
|
|
|
@ddt.ddt
|
|
class TestCourseGradeFactory(GradeTestBase):
|
|
"""
|
|
Test that CourseGrades are calculated properly
|
|
"""
|
|
def _assert_zero_grade(self, course_grade, expected_grade_class):
|
|
"""
|
|
Asserts whether the given course_grade is as expected with
|
|
zero values.
|
|
"""
|
|
self.assertIsInstance(course_grade, expected_grade_class)
|
|
self.assertIsNone(course_grade.letter_grade)
|
|
self.assertEqual(course_grade.percent, 0.0)
|
|
self.assertIsNotNone(course_grade.chapter_grades)
|
|
|
|
def test_course_grade_no_access(self):
|
|
"""
|
|
Test to ensure a grade can ba calculated for a student in a course, even if they themselves do not have access.
|
|
"""
|
|
invisible_course = CourseFactory.create(visible_to_staff_only=True)
|
|
access = has_access(self.request.user, 'load', invisible_course)
|
|
self.assertEqual(access.has_access, False)
|
|
self.assertEqual(access.error_code, 'not_visible_to_user')
|
|
|
|
# with self.assertNoExceptionRaised: <- this isn't a real method, it's an implicit assumption
|
|
grade = CourseGradeFactory().create(self.request.user, invisible_course)
|
|
self.assertEqual(grade.percent, 0)
|
|
|
|
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
|
|
@ddt.data(
|
|
(True, True),
|
|
(True, False),
|
|
(False, True),
|
|
(False, False),
|
|
)
|
|
@ddt.unpack
|
|
def test_course_grade_feature_gating(self, feature_flag, course_setting):
|
|
# Grades are only saved if the feature flag and the advanced setting are
|
|
# both set to True.
|
|
grade_factory = CourseGradeFactory()
|
|
with persistent_grades_feature_flags(
|
|
global_flag=feature_flag,
|
|
enabled_for_all_courses=False,
|
|
course_id=self.course.id,
|
|
enabled_for_course=course_setting
|
|
):
|
|
with patch('lms.djangoapps.grades.models.PersistentCourseGrade.read') as mock_read_grade:
|
|
grade_factory.create(self.request.user, self.course)
|
|
self.assertEqual(mock_read_grade.called, feature_flag and course_setting)
|
|
|
|
def test_create(self):
|
|
grade_factory = CourseGradeFactory()
|
|
|
|
def _assert_create(expected_pass):
|
|
"""
|
|
Creates the grade, ensuring it is as expected.
|
|
"""
|
|
course_grade = grade_factory.create(self.request.user, self.course)
|
|
self.assertEqual(course_grade.letter_grade, u'Pass' if expected_pass else None)
|
|
self.assertEqual(course_grade.percent, 0.5)
|
|
|
|
with self.assertNumQueries(11), mock_get_score(1, 2):
|
|
_assert_create(expected_pass=True)
|
|
|
|
with self.assertNumQueries(13), mock_get_score(1, 2):
|
|
grade_factory.update(self.request.user, self.course)
|
|
|
|
with self.assertNumQueries(1):
|
|
_assert_create(expected_pass=True)
|
|
|
|
self._update_grading_policy(passing=0.9)
|
|
|
|
with self.assertNumQueries(6):
|
|
_assert_create(expected_pass=False)
|
|
|
|
@ddt.data(True, False)
|
|
def test_create_zero(self, assume_zero_enabled):
|
|
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
|
|
grade_factory = CourseGradeFactory()
|
|
course_grade = grade_factory.create(self.request.user, self.course)
|
|
self._assert_zero_grade(course_grade, ZeroCourseGrade if assume_zero_enabled else CourseGrade)
|
|
|
|
def test_create_zero_subs_grade_for_nonzero_course_grade(self):
|
|
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT), waffle().override(WRITE_ONLY_IF_ENGAGED):
|
|
subsection = self.course_structure[self.sequence.location]
|
|
with mock_get_score(1, 2):
|
|
self.subsection_grade_factory.update(subsection)
|
|
course_grade = CourseGradeFactory().update(self.request.user, self.course)
|
|
subsection1_grade = course_grade.subsection_grades[self.sequence.location]
|
|
subsection2_grade = course_grade.subsection_grades[self.sequence2.location]
|
|
self.assertIsInstance(subsection1_grade, SubsectionGrade)
|
|
self.assertIsInstance(subsection2_grade, ZeroSubsectionGrade)
|
|
|
|
def test_read(self):
|
|
grade_factory = CourseGradeFactory()
|
|
with mock_get_score(1, 2):
|
|
grade_factory.update(self.request.user, self.course)
|
|
|
|
def _assert_read():
|
|
"""
|
|
Reads the grade, ensuring it is as expected and requires just one query
|
|
"""
|
|
with self.assertNumQueries(1):
|
|
course_grade = grade_factory.read(self.request.user, self.course)
|
|
self.assertEqual(course_grade.letter_grade, u'Pass')
|
|
self.assertEqual(course_grade.percent, 0.5)
|
|
|
|
_assert_read()
|
|
self._update_grading_policy(passing=0.9)
|
|
_assert_read()
|
|
|
|
@ddt.data(True, False)
|
|
def test_read_zero(self, assume_zero_enabled):
|
|
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
|
|
grade_factory = CourseGradeFactory()
|
|
course_grade = grade_factory.read(self.request.user, course_key=self.course.id)
|
|
if assume_zero_enabled:
|
|
self._assert_zero_grade(course_grade, ZeroCourseGrade)
|
|
else:
|
|
self.assertIsNone(course_grade)
|
|
|
|
|
|
@ddt.ddt
|
|
class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase):
|
|
"""
|
|
Tests for SubsectionGradeFactory functionality.
|
|
|
|
Ensures that SubsectionGrades are created and updated properly, that
|
|
persistent grades are functioning as expected, and that the flag to
|
|
enable saving subsection grades blocks/enables that feature as expected.
|
|
"""
|
|
|
|
def assert_grade(self, grade, expected_earned, expected_possible):
|
|
"""
|
|
Asserts that the given grade object has the expected score.
|
|
"""
|
|
self.assertEqual(
|
|
(grade.all_total.earned, grade.all_total.possible),
|
|
(expected_earned, expected_possible),
|
|
)
|
|
|
|
def test_create(self):
|
|
"""
|
|
Assuming the underlying score reporting methods work,
|
|
test that the score is calculated properly.
|
|
"""
|
|
with mock_get_score(1, 2):
|
|
grade = self.subsection_grade_factory.create(self.sequence)
|
|
self.assert_grade(grade, 1, 2)
|
|
|
|
def test_create_internals(self):
|
|
"""
|
|
Tests to ensure that a persistent subsection grade is
|
|
created, saved, then fetched on re-request.
|
|
"""
|
|
with patch(
|
|
'lms.djangoapps.grades.new.subsection_grade.PersistentSubsectionGrade.create_grade',
|
|
wraps=PersistentSubsectionGrade.create_grade
|
|
) as mock_create_grade:
|
|
with patch(
|
|
'lms.djangoapps.grades.new.subsection_grade_factory.SubsectionGradeFactory._get_bulk_cached_grade',
|
|
wraps=self.subsection_grade_factory._get_bulk_cached_grade
|
|
) as mock_get_bulk_cached_grade:
|
|
with self.assertNumQueries(14):
|
|
grade_a = self.subsection_grade_factory.create(self.sequence)
|
|
self.assertTrue(mock_get_bulk_cached_grade.called)
|
|
self.assertTrue(mock_create_grade.called)
|
|
|
|
mock_get_bulk_cached_grade.reset_mock()
|
|
mock_create_grade.reset_mock()
|
|
|
|
with self.assertNumQueries(0):
|
|
grade_b = self.subsection_grade_factory.create(self.sequence)
|
|
self.assertTrue(mock_get_bulk_cached_grade.called)
|
|
self.assertFalse(mock_create_grade.called)
|
|
|
|
self.assertEqual(grade_a.url_name, grade_b.url_name)
|
|
grade_b.all_total.first_attempted = None
|
|
self.assertEqual(grade_a.all_total, grade_b.all_total)
|
|
|
|
def test_update(self):
|
|
"""
|
|
Assuming the underlying score reporting methods work,
|
|
test that the score is calculated properly.
|
|
"""
|
|
with mock_get_score(1, 2):
|
|
grade = self.subsection_grade_factory.update(self.sequence)
|
|
self.assert_grade(grade, 1, 2)
|
|
|
|
def test_update_if_higher(self):
|
|
def verify_update_if_higher(mock_score, expected_grade):
|
|
"""
|
|
Updates the subsection grade and verifies the
|
|
resulting grade is as expected.
|
|
"""
|
|
with mock_get_score(*mock_score):
|
|
grade = self.subsection_grade_factory.update(self.sequence, only_if_higher=True)
|
|
self.assert_grade(grade, *expected_grade)
|
|
|
|
verify_update_if_higher((1, 2), (1, 2)) # previous value was non-existent
|
|
verify_update_if_higher((2, 4), (2, 4)) # previous value was equivalent
|
|
verify_update_if_higher((1, 4), (2, 4)) # previous value was greater
|
|
verify_update_if_higher((3, 4), (3, 4)) # previous value was less
|
|
|
|
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
|
|
@ddt.data(
|
|
(True, True),
|
|
(True, False),
|
|
(False, True),
|
|
(False, False),
|
|
)
|
|
@ddt.unpack
|
|
def test_subsection_grade_feature_gating(self, feature_flag, course_setting):
|
|
# Grades are only saved if the feature flag and the advanced setting are
|
|
# both set to True.
|
|
with patch(
|
|
'lms.djangoapps.grades.models.PersistentSubsectionGrade.bulk_read_grades'
|
|
) as mock_read_saved_grade:
|
|
with persistent_grades_feature_flags(
|
|
global_flag=feature_flag,
|
|
enabled_for_all_courses=False,
|
|
course_id=self.course.id,
|
|
enabled_for_course=course_setting
|
|
):
|
|
self.subsection_grade_factory.create(self.sequence)
|
|
self.assertEqual(mock_read_saved_grade.called, feature_flag and course_setting)
|
|
|
|
|
|
class ZeroGradeTest(GradeTestBase):
|
|
"""
|
|
Tests ZeroCourseGrade (and, implicitly, ZeroSubsectionGrade)
|
|
functionality.
|
|
"""
|
|
def test_zero(self):
|
|
"""
|
|
Creates a ZeroCourseGrade and ensures it's empty.
|
|
"""
|
|
course_data = CourseData(self.request.user, structure=self.course_structure)
|
|
chapter_grades = ZeroCourseGrade(self.request.user, course_data).chapter_grades
|
|
for chapter in chapter_grades:
|
|
for section in chapter_grades[chapter]['sections']:
|
|
for score in section.problem_scores.itervalues():
|
|
self.assertEqual(score.earned, 0)
|
|
self.assertEqual(score.first_attempted, None)
|
|
self.assertEqual(section.all_total.earned, 0)
|
|
|
|
|
|
class SubsectionGradeTest(GradeTestBase):
|
|
"""
|
|
Tests SubsectionGrade functionality.
|
|
"""
|
|
|
|
def test_save_and_load(self):
|
|
"""
|
|
Test that grades are persisted to the database properly,
|
|
and that loading saved grades returns the same data.
|
|
"""
|
|
# Create a grade that *isn't* saved to the database
|
|
input_grade = SubsectionGrade(self.sequence)
|
|
input_grade.init_from_structure(
|
|
self.request.user,
|
|
self.course_structure,
|
|
self.subsection_grade_factory._submissions_scores,
|
|
self.subsection_grade_factory._csm_scores,
|
|
)
|
|
self.assertEqual(PersistentSubsectionGrade.objects.count(), 0)
|
|
|
|
# save to db, and verify object is in database
|
|
input_grade.create_model(self.request.user)
|
|
self.assertEqual(PersistentSubsectionGrade.objects.count(), 1)
|
|
|
|
# load from db, and ensure output matches input
|
|
loaded_grade = SubsectionGrade(self.sequence)
|
|
saved_model = PersistentSubsectionGrade.read_grade(
|
|
user_id=self.request.user.id,
|
|
usage_key=self.sequence.location,
|
|
)
|
|
loaded_grade.init_from_model(
|
|
self.request.user,
|
|
saved_model,
|
|
self.course_structure,
|
|
self.subsection_grade_factory._submissions_scores,
|
|
self.subsection_grade_factory._csm_scores,
|
|
)
|
|
|
|
self.assertEqual(input_grade.url_name, loaded_grade.url_name)
|
|
loaded_grade.all_total.first_attempted = None
|
|
self.assertEqual(input_grade.all_total, loaded_grade.all_total)
|
|
|
|
|
|
@ddt.ddt
|
|
class TestMultipleProblemTypesSubsectionScores(SharedModuleStoreTestCase):
|
|
"""
|
|
Test grading of different problem types.
|
|
"""
|
|
|
|
SCORED_BLOCK_COUNT = 7
|
|
ACTUAL_TOTAL_POSSIBLE = 17.0
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestMultipleProblemTypesSubsectionScores, cls).setUpClass()
|
|
cls.load_scoreable_course()
|
|
chapter1 = cls.course.get_children()[0]
|
|
cls.seq1 = chapter1.get_children()[0]
|
|
|
|
def setUp(self):
|
|
super(TestMultipleProblemTypesSubsectionScores, self).setUp()
|
|
password = u'test'
|
|
self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password)
|
|
self.client.login(username=self.student.username, password=password)
|
|
self.request = get_mock_request(self.student)
|
|
self.course_structure = get_course_blocks(self.student, self.course.location)
|
|
|
|
@classmethod
|
|
def load_scoreable_course(cls):
|
|
"""
|
|
This test course lives at `common/test/data/scoreable`.
|
|
|
|
For details on the contents and structure of the file, see
|
|
`common/test/data/scoreable/README`.
|
|
"""
|
|
|
|
course_items = import_course_from_xml(
|
|
cls.store,
|
|
'test_user',
|
|
TEST_DATA_DIR,
|
|
source_dirs=['scoreable'],
|
|
static_content_store=None,
|
|
target_id=cls.store.make_course_key('edX', 'scoreable', '3000'),
|
|
raise_on_failure=True,
|
|
create_if_not_present=True,
|
|
)
|
|
|
|
cls.course = course_items[0]
|
|
|
|
def test_score_submission_for_all_problems(self):
|
|
subsection_factory = SubsectionGradeFactory(
|
|
self.student,
|
|
course_structure=self.course_structure,
|
|
course=self.course,
|
|
)
|
|
score = subsection_factory.create(self.seq1)
|
|
|
|
self.assertEqual(score.all_total.earned, 0.0)
|
|
self.assertEqual(score.all_total.possible, self.ACTUAL_TOTAL_POSSIBLE)
|
|
|
|
# Choose arbitrary, non-default values for earned and possible.
|
|
earned_per_block = 3.0
|
|
possible_per_block = 7.0
|
|
with mock_get_submissions_score(earned_per_block, possible_per_block) as mock_score:
|
|
# Configure one block to return no possible score, the rest to return 3.0 earned / 7.0 possible
|
|
block_count = self.SCORED_BLOCK_COUNT - 1
|
|
mock_score.side_effect = itertools.chain(
|
|
[(earned_per_block, None, earned_per_block, None, datetime.datetime(2000, 1, 1))],
|
|
itertools.repeat(mock_score.return_value)
|
|
)
|
|
score = subsection_factory.update(self.seq1)
|
|
self.assertEqual(score.all_total.earned, earned_per_block * block_count)
|
|
self.assertEqual(score.all_total.possible, possible_per_block * block_count)
|
|
|
|
|
|
@ddt.ddt
|
|
class TestVariedMetadata(ProblemSubmissionTestMixin, ModuleStoreTestCase):
|
|
"""
|
|
Test that changing the metadata on a block has the desired effect on the
|
|
persisted score.
|
|
"""
|
|
default_problem_metadata = {
|
|
u'graded': True,
|
|
u'weight': 2.5,
|
|
u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc),
|
|
}
|
|
|
|
def setUp(self):
|
|
super(TestVariedMetadata, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
with self.store.bulk_operations(self.course.id):
|
|
self.chapter = ItemFactory.create(
|
|
parent=self.course,
|
|
category="chapter",
|
|
display_name="Test Chapter"
|
|
)
|
|
self.sequence = ItemFactory.create(
|
|
parent=self.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 1",
|
|
graded=True
|
|
)
|
|
self.vertical = ItemFactory.create(
|
|
parent=self.sequence,
|
|
category='vertical',
|
|
display_name='Test Vertical 1'
|
|
)
|
|
self.problem_xml = u'''
|
|
<problem url_name="capa-optionresponse">
|
|
<optionresponse>
|
|
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
|
|
<optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput>
|
|
</optionresponse>
|
|
</problem>
|
|
'''
|
|
self.request = get_mock_request(UserFactory())
|
|
self.client.login(username=self.request.user.username, password="test")
|
|
CourseEnrollment.enroll(self.request.user, self.course.id)
|
|
|
|
def _get_altered_metadata(self, alterations):
|
|
"""
|
|
Returns a copy of the default_problem_metadata dict updated with the
|
|
specified alterations.
|
|
"""
|
|
metadata = self.default_problem_metadata.copy()
|
|
metadata.update(alterations)
|
|
return metadata
|
|
|
|
def _add_problem_with_alterations(self, alterations):
|
|
"""
|
|
Add a problem to the course with the specified metadata alterations.
|
|
"""
|
|
|
|
metadata = self._get_altered_metadata(alterations)
|
|
ItemFactory.create(
|
|
parent=self.vertical,
|
|
category="problem",
|
|
display_name="problem",
|
|
data=self.problem_xml,
|
|
metadata=metadata,
|
|
)
|
|
|
|
def _get_score(self):
|
|
"""
|
|
Return the score of the test problem when one correct problem (out of
|
|
two) is submitted.
|
|
"""
|
|
|
|
self.submit_question_answer(u'problem', {u'2_1': u'Correct'})
|
|
course_structure = get_course_blocks(self.request.user, self.course.location)
|
|
subsection_factory = SubsectionGradeFactory(
|
|
self.request.user,
|
|
course_structure=course_structure,
|
|
course=self.course,
|
|
)
|
|
return subsection_factory.create(self.sequence)
|
|
|
|
@ddt.data(
|
|
({}, 1.25, 2.5),
|
|
({u'weight': 27}, 13.5, 27),
|
|
({u'weight': 1.0}, 0.5, 1.0),
|
|
({u'weight': 0.0}, 0.0, 0.0),
|
|
({u'weight': None}, 1.0, 2.0),
|
|
)
|
|
@ddt.unpack
|
|
def test_weight_metadata_alterations(self, alterations, expected_earned, expected_possible):
|
|
self._add_problem_with_alterations(alterations)
|
|
score = self._get_score()
|
|
self.assertEqual(score.all_total.earned, expected_earned)
|
|
self.assertEqual(score.all_total.possible, expected_possible)
|
|
|
|
@ddt.data(
|
|
({u'graded': True}, 1.25, 2.5),
|
|
({u'graded': False}, 0.0, 0.0),
|
|
)
|
|
@ddt.unpack
|
|
def test_graded_metadata_alterations(self, alterations, expected_earned, expected_possible):
|
|
self._add_problem_with_alterations(alterations)
|
|
score = self._get_score()
|
|
self.assertEqual(score.graded_total.earned, expected_earned)
|
|
self.assertEqual(score.graded_total.possible, expected_possible)
|
|
|
|
|
|
class TestCourseGradeLogging(ProblemSubmissionTestMixin, SharedModuleStoreTestCase):
|
|
"""
|
|
Tests logging in the course grades module.
|
|
Uses a larger course structure than other
|
|
unit tests.
|
|
"""
|
|
def setUp(self):
|
|
super(TestCourseGradeLogging, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
with self.store.bulk_operations(self.course.id):
|
|
self.chapter = ItemFactory.create(
|
|
parent=self.course,
|
|
category="chapter",
|
|
display_name="Test Chapter"
|
|
)
|
|
self.sequence = ItemFactory.create(
|
|
parent=self.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 1",
|
|
graded=True
|
|
)
|
|
self.sequence_2 = ItemFactory.create(
|
|
parent=self.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 2",
|
|
graded=True
|
|
)
|
|
self.sequence_3 = ItemFactory.create(
|
|
parent=self.chapter,
|
|
category='sequential',
|
|
display_name="Test Sequential 3",
|
|
graded=False
|
|
)
|
|
self.vertical = ItemFactory.create(
|
|
parent=self.sequence,
|
|
category='vertical',
|
|
display_name='Test Vertical 1'
|
|
)
|
|
self.vertical_2 = ItemFactory.create(
|
|
parent=self.sequence_2,
|
|
category='vertical',
|
|
display_name='Test Vertical 2'
|
|
)
|
|
self.vertical_3 = ItemFactory.create(
|
|
parent=self.sequence_3,
|
|
category='vertical',
|
|
display_name='Test Vertical 3'
|
|
)
|
|
problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
|
|
question_text='The correct answer is Choice 2',
|
|
choices=[False, False, True, False],
|
|
choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
|
)
|
|
self.problem = ItemFactory.create(
|
|
parent=self.vertical,
|
|
category="problem",
|
|
display_name="test_problem_1",
|
|
data=problem_xml
|
|
)
|
|
self.problem_2 = ItemFactory.create(
|
|
parent=self.vertical_2,
|
|
category="problem",
|
|
display_name="test_problem_2",
|
|
data=problem_xml
|
|
)
|
|
self.problem_3 = ItemFactory.create(
|
|
parent=self.vertical_3,
|
|
category="problem",
|
|
display_name="test_problem_3",
|
|
data=problem_xml
|
|
)
|
|
self.request = get_mock_request(UserFactory())
|
|
self.client.login(username=self.request.user.username, password="test")
|
|
self.course_structure = get_course_blocks(self.request.user, self.course.location)
|
|
self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure)
|
|
CourseEnrollment.enroll(self.request.user, self.course.id)
|
|
|
|
def _create_course_grade_and_check_logging(
|
|
self,
|
|
factory_method,
|
|
log_mock,
|
|
log_statement,
|
|
):
|
|
"""
|
|
Creates a course grade and asserts that the associated logging
|
|
matches the expected totals passed in to the function.
|
|
"""
|
|
factory_method(self.request.user, self.course)
|
|
self.assertIn(log_statement, log_mock.info.call_args[0][0])
|
|
self.assertIn(unicode(self.course.id), log_mock.info.call_args[0][1])
|
|
self.assertEquals(self.request.user.id, log_mock.info.call_args[0][2])
|
|
|
|
def test_course_grade_logging(self):
|
|
grade_factory = CourseGradeFactory()
|
|
with persistent_grades_feature_flags(
|
|
global_flag=True,
|
|
enabled_for_all_courses=False,
|
|
course_id=self.course.id,
|
|
enabled_for_course=True
|
|
):
|
|
with patch('lms.djangoapps.grades.new.course_grade_factory.log') as log_mock:
|
|
# returns Zero when no grade, with ASSUME_ZERO_GRADE_IF_ABSENT
|
|
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=True):
|
|
self._create_course_grade_and_check_logging(grade_factory.create, log_mock, u'CreateZero')
|
|
|
|
# read, but not persisted
|
|
self._create_course_grade_and_check_logging(grade_factory.create, log_mock, u'Update')
|
|
|
|
# update and persist
|
|
self._create_course_grade_and_check_logging(grade_factory.update, log_mock, u'Update')
|
|
|
|
# read from persistence, using create
|
|
self._create_course_grade_and_check_logging(grade_factory.create, log_mock, u'Read')
|
|
|
|
# read from persistence, using read
|
|
self._create_course_grade_and_check_logging(grade_factory.read, log_mock, u'Read')
|