Commit 0de55de2 by Nimisha Asthagiri

Grades: Clean up tests

EDUCATOR-1404
parent 02410818
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
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 SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..subsection_grade_factory import SubsectionGradeFactory
class GradeTestBase(SharedModuleStoreTestCase):
"""
Base class for some Grades 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
)
# AED 2017-06-19: make cls.sequence belong to multiple parents,
# so we can test that DAGs with this shape are handled correctly.
cls.chapter_2 = ItemFactory.create(
parent=cls.course,
category='chapter',
display_name='Test Chapter 2'
)
cls.chapter_2.children.append(cls.sequence.location)
cls.store.update_item(cls.chapter_2, UserFactory().id)
def setUp(self):
super(GradeTestBase, self).setUp()
self.request = get_mock_request(UserFactory())
self.client.login(username=self.request.user.username, password="test")
self._set_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 _set_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)
import ddt
from django.conf import settings
from mock import patch
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 SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT, waffle
from ..course_data import CourseData
from ..course_grade import ZeroCourseGrade
from ..course_grade_factory import CourseGradeFactory
from .base import GradeTestBase
from .utils import answer_problem
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.ddt
class ZeroGradeTest(GradeTestBase):
"""
Tests ZeroCourseGrade (and, implicitly, ZeroSubsectionGrade)
functionality.
"""
@ddt.data(True, False)
def test_zero(self, assume_zero_enabled):
"""
Creates a ZeroCourseGrade and ensures it's empty.
"""
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
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)
@ddt.data(True, False)
def test_zero_null_scores(self, assume_zero_enabled):
"""
Creates a zero course grade and ensures that null scores aren't included in the section problem scores.
"""
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
with patch('lms.djangoapps.grades.subsection_grade.get_score', return_value=None):
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:
self.assertNotEqual({}, chapter_grades[chapter]['sections'])
for section in chapter_grades[chapter]['sections']:
self.assertEqual({}, section.problem_scores)
class TestScoreForModule(SharedModuleStoreTestCase):
"""
Test the method that calculates the score for a given block based on the
cumulative scores of its children. This test class uses a hard-coded block
hierarchy with scores as follows:
a
+--------+--------+
b c
+--------------+-----------+ |
d e f g
+-----+ +-----+-----+ | |
h i j k l m n
(2/5) (3/5) (0/1) - (1/3) - (3/10)
"""
@classmethod
def setUpClass(cls):
super(TestScoreForModule, cls).setUpClass()
cls.course = CourseFactory.create()
with cls.store.bulk_operations(cls.course.id):
cls.a = ItemFactory.create(parent=cls.course, category="chapter", display_name="a")
cls.b = ItemFactory.create(parent=cls.a, category="sequential", display_name="b")
cls.c = ItemFactory.create(parent=cls.a, category="sequential", display_name="c")
cls.d = ItemFactory.create(parent=cls.b, category="vertical", display_name="d")
cls.e = ItemFactory.create(parent=cls.b, category="vertical", display_name="e")
cls.f = ItemFactory.create(parent=cls.b, category="vertical", display_name="f")
cls.g = ItemFactory.create(parent=cls.c, category="vertical", display_name="g")
cls.h = ItemFactory.create(parent=cls.d, category="problem", display_name="h")
cls.i = ItemFactory.create(parent=cls.d, category="problem", display_name="i")
cls.j = ItemFactory.create(parent=cls.e, category="problem", display_name="j")
cls.k = ItemFactory.create(parent=cls.e, category="html", display_name="k")
cls.l = ItemFactory.create(parent=cls.e, category="problem", display_name="l")
cls.m = ItemFactory.create(parent=cls.f, category="html", display_name="m")
cls.n = ItemFactory.create(parent=cls.g, category="problem", display_name="n")
cls.request = get_mock_request(UserFactory())
CourseEnrollment.enroll(cls.request.user, cls.course.id)
answer_problem(cls.course, cls.request, cls.h, score=2, max_value=5)
answer_problem(cls.course, cls.request, cls.i, score=3, max_value=5)
answer_problem(cls.course, cls.request, cls.j, score=0, max_value=1)
answer_problem(cls.course, cls.request, cls.l, score=1, max_value=3)
answer_problem(cls.course, cls.request, cls.n, score=3, max_value=10)
cls.course_grade = CourseGradeFactory().read(cls.request.user, cls.course)
def test_score_chapter(self):
earned, possible = self.course_grade.score_for_module(self.a.location)
self.assertEqual(earned, 9)
self.assertEqual(possible, 24)
def test_score_section_many_leaves(self):
earned, possible = self.course_grade.score_for_module(self.b.location)
self.assertEqual(earned, 6)
self.assertEqual(possible, 14)
def test_score_section_one_leaf(self):
earned, possible = self.course_grade.score_for_module(self.c.location)
self.assertEqual(earned, 3)
self.assertEqual(possible, 10)
def test_score_vertical_two_leaves(self):
earned, possible = self.course_grade.score_for_module(self.d.location)
self.assertEqual(earned, 5)
self.assertEqual(possible, 10)
def test_score_vertical_two_leaves_one_unscored(self):
earned, possible = self.course_grade.score_for_module(self.e.location)
self.assertEqual(earned, 1)
self.assertEqual(possible, 4)
def test_score_vertical_no_score(self):
earned, possible = self.course_grade.score_for_module(self.f.location)
self.assertEqual(earned, 0)
self.assertEqual(possible, 0)
def test_score_vertical_one_leaf(self):
earned, possible = self.course_grade.score_for_module(self.g.location)
self.assertEqual(earned, 3)
self.assertEqual(possible, 10)
def test_score_leaf(self):
earned, possible = self.course_grade.score_for_module(self.h.location)
self.assertEqual(earned, 2)
self.assertEqual(possible, 5)
def test_score_leaf_no_score(self):
earned, possible = self.course_grade.score_for_module(self.m.location)
self.assertEqual(earned, 0)
self.assertEqual(possible, 0)
import itertools
from nose.plugins.attrib import attr
import ddt
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from courseware.access import has_access
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from django.conf import settings
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
from mock import patch
from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT, waffle
from ..course_grade import CourseGrade, ZeroCourseGrade
from ..course_grade_factory import CourseGradeFactory
from ..subsection_grade import SubsectionGrade, ZeroSubsectionGrade
from ..subsection_grade_factory import SubsectionGradeFactory
from .base import GradeTestBase
from .utils import mock_get_score
@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().read(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.read(self.request.user, self.course)
self.assertEqual(mock_read_grade.called, feature_flag and course_setting)
def test_read(self):
grade_factory = CourseGradeFactory()
def _assert_read(expected_pass, expected_percent):
"""
Creates the grade, ensuring it is as expected.
"""
course_grade = grade_factory.read(self.request.user, self.course)
self.assertEqual(course_grade.letter_grade, u'Pass' if expected_pass else None)
self.assertEqual(course_grade.percent, expected_percent)
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT):
with self.assertNumQueries(1), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0)
with self.assertNumQueries(10), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course)
with self.assertNumQueries(1):
_assert_read(expected_pass=True, expected_percent=0.5)
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(*itertools.product((True, False), (True, False)))
@ddt.unpack
def test_read_zero(self, assume_zero_enabled, create_if_needed):
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
grade_factory = CourseGradeFactory()
course_grade = grade_factory.read(self.request.user, self.course, create_if_needed=create_if_needed)
if create_if_needed or assume_zero_enabled:
self._assert_zero_grade(course_grade, ZeroCourseGrade if assume_zero_enabled else CourseGrade)
else:
self.assertIsNone(course_grade)
def test_create_zero_subs_grade_for_nonzero_course_grade(self):
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT):
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)
@ddt.data(True, False)
def test_iter_force_update(self, force_update):
with patch('lms.djangoapps.grades.subsection_grade_factory.SubsectionGradeFactory.update') as mock_update:
set(CourseGradeFactory().iter(
users = [self.request.user], course = self.course, force_update = force_update
))
self.assertEqual(mock_update.called, force_update)
def test_course_grade_summary(self):
with mock_get_score(1, 2):
self.subsection_grade_factory.update(self.course_structure[self.sequence.location])
course_grade = CourseGradeFactory().update(self.request.user, self.course)
actual_summary = course_grade.summary
# We should have had a zero subsection grade for sequential 2, since we never
# gave it a mock score above.
expected_summary = {
'grade': None,
'grade_breakdown': {
'Homework': {
'category': 'Homework',
'percent': 0.25,
'detail': 'Homework = 25.00% of a possible 100.00%',
}
},
'percent': 0.25,
'section_breakdown': [
{
'category': 'Homework',
'detail': u'Homework 1 - Test Sequential 1 - 50% (1/2)',
'label': u'HW 01',
'percent': 0.5
},
{
'category': 'Homework',
'detail': u'Homework 2 - Test Sequential 2 - 0% (0/1)',
'label': u'HW 02',
'percent': 0.0
},
{
'category': 'Homework',
'detail': u'Homework Average = 25%',
'label': u'HW Avg',
'percent': 0.25,
'prominent': True
},
]
}
self.assertEqual(expected_summary, actual_summary)
@attr(shard=1)
class TestGradeIteration(SharedModuleStoreTestCase):
"""
Test iteration through student course grades.
"""
COURSE_NUM = "1000"
COURSE_NAME = "grading_test_course"
@classmethod
def setUpClass(cls):
super(TestGradeIteration, cls).setUpClass()
cls.course = CourseFactory.create(
display_name=cls.COURSE_NAME,
number=cls.COURSE_NUM
)
def setUp(self):
"""
Create a course and a handful of users to assign grades
"""
super(TestGradeIteration, self).setUp()
self.students = [
UserFactory.create(username='student1'),
UserFactory.create(username='student2'),
UserFactory.create(username='student3'),
UserFactory.create(username='student4'),
UserFactory.create(username='student5'),
]
def test_empty_student_list(self):
"""
If we don't pass in any students, it should return a zero-length
iterator, but it shouldn't error.
"""
grade_results = list(CourseGradeFactory().iter([], self.course))
self.assertEqual(grade_results, [])
def test_all_empty_grades(self):
"""
No students have grade entries.
"""
with patch.object(
BlockStructureFactory,
'create_from_store',
wraps=BlockStructureFactory.create_from_store
) as mock_create_from_store:
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
self.assertEquals(mock_create_from_store.call_count, 1)
self.assertEqual(len(all_errors), 0)
for course_grade in all_course_grades.values():
self.assertIsNone(course_grade.letter_grade)
self.assertEqual(course_grade.percent, 0.0)
@patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read')
def test_grading_exception(self, mock_course_grade):
"""Test that we correctly capture exception messages that bubble up from
grading. Note that we only see errors at this level if the grading
process for this student fails entirely due to an unexpected event --
having errors in the problem sets will not trigger this.
We patch the grade() method with our own, which will generate the errors
for student3 and student4.
"""
student1, student2, student3, student4, student5 = self.students
mock_course_grade.side_effect = [
Exception("Error for {}.".format(student.username))
if student.username in ['student3', 'student4']
else mock_course_grade.return_value
for student in self.students
]
with self.assertNumQueries(4):
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
self.assertEqual(
{student: all_errors[student].message for student in all_errors},
{
student3: "Error for student3.",
student4: "Error for student4.",
}
)
# But we should still have five gradesets
self.assertEqual(len(all_course_grades), 5)
# Even though two will simply be empty
self.assertIsNone(all_course_grades[student3])
self.assertIsNone(all_course_grades[student4])
# The rest will have grade information in them
self.assertIsNotNone(all_course_grades[student1])
self.assertIsNotNone(all_course_grades[student2])
self.assertIsNotNone(all_course_grades[student5])
def _course_grades_and_errors_for(self, course, students):
"""
Simple helper method to iterate through student grades and give us
two dictionaries -- one that has all students and their respective
course grades, and one that has only students that could not be graded
and their respective error messages.
"""
students_to_course_grades = {}
students_to_errors = {}
for student, course_grade, error in CourseGradeFactory().iter(students, course):
students_to_course_grades[student] = course_grade
if error:
students_to_errors[student] = error
return students_to_course_grades, students_to_errors
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)
"""
Test grade calculation.
"""
import datetime
import itertools
import ddt
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
from mock import patch
from nose.plugins.attrib import attr
from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory
from openedx.core.djangolib.testing.utils import get_mock_request
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.graders import ProblemScore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..course_grade_factory import CourseGradeFactory
from ..subsection_grade_factory import SubsectionGradeFactory
from .utils import answer_problem
@attr(shard=1)
class TestGradeIteration(SharedModuleStoreTestCase):
"""
Test iteration through student course grades.
"""
COURSE_NUM = "1000"
COURSE_NAME = "grading_test_course"
@classmethod
def setUpClass(cls):
super(TestGradeIteration, cls).setUpClass()
cls.course = CourseFactory.create(
display_name=cls.COURSE_NAME,
number=cls.COURSE_NUM
)
def setUp(self):
"""
Create a course and a handful of users to assign grades
"""
super(TestGradeIteration, self).setUp()
self.students = [
UserFactory.create(username='student1'),
UserFactory.create(username='student2'),
UserFactory.create(username='student3'),
UserFactory.create(username='student4'),
UserFactory.create(username='student5'),
]
def test_empty_student_list(self):
"""
If we don't pass in any students, it should return a zero-length
iterator, but it shouldn't error.
"""
grade_results = list(CourseGradeFactory().iter([], self.course))
self.assertEqual(grade_results, [])
def test_all_empty_grades(self):
"""
No students have grade entries.
"""
with patch.object(
BlockStructureFactory,
'create_from_store',
wraps=BlockStructureFactory.create_from_store
) as mock_create_from_store:
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
self.assertEquals(mock_create_from_store.call_count, 1)
self.assertEqual(len(all_errors), 0)
for course_grade in all_course_grades.values():
self.assertIsNone(course_grade.letter_grade)
self.assertEqual(course_grade.percent, 0.0)
@patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read')
def test_grading_exception(self, mock_course_grade):
"""Test that we correctly capture exception messages that bubble up from
grading. Note that we only see errors at this level if the grading
process for this student fails entirely due to an unexpected event --
having errors in the problem sets will not trigger this.
We patch the grade() method with our own, which will generate the errors
for student3 and student4.
"""
student1, student2, student3, student4, student5 = self.students
mock_course_grade.side_effect = [
Exception("Error for {}.".format(student.username))
if student.username in ['student3', 'student4']
else mock_course_grade.return_value
for student in self.students
]
with self.assertNumQueries(4):
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
self.assertEqual(
{student: all_errors[student].message for student in all_errors},
{
student3: "Error for student3.",
student4: "Error for student4.",
}
)
# But we should still have five gradesets
self.assertEqual(len(all_course_grades), 5)
# Even though two will simply be empty
self.assertIsNone(all_course_grades[student3])
self.assertIsNone(all_course_grades[student4])
# The rest will have grade information in them
self.assertIsNotNone(all_course_grades[student1])
self.assertIsNotNone(all_course_grades[student2])
self.assertIsNotNone(all_course_grades[student5])
def _course_grades_and_errors_for(self, course, students):
"""
Simple helper method to iterate through student grades and give us
two dictionaries -- one that has all students and their respective
course grades, and one that has only students that could not be graded
and their respective error messages.
"""
students_to_course_grades = {}
students_to_errors = {}
for student, course_grade, error in CourseGradeFactory().iter(students, course):
students_to_course_grades[student] = course_grade
if error:
students_to_errors[student] = error
return students_to_course_grades, students_to_errors
@ddt.ddt
class TestWeightedProblems(SharedModuleStoreTestCase):
"""
Test scores and grades with various problem weight values.
"""
@classmethod
def setUpClass(cls):
super(TestWeightedProblems, 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="chapter")
cls.sequential = ItemFactory.create(parent=cls.chapter, category="sequential", display_name="sequential")
cls.vertical = ItemFactory.create(parent=cls.sequential, category="vertical", display_name="vertical1")
problem_xml = cls._create_problem_xml()
cls.problems = []
for i in range(2):
cls.problems.append(
ItemFactory.create(
parent=cls.vertical,
category="problem",
display_name="problem_{}".format(i),
data=problem_xml,
)
)
def setUp(self):
super(TestWeightedProblems, self).setUp()
self.user = UserFactory()
self.request = get_mock_request(self.user)
@classmethod
def _create_problem_xml(cls):
"""
Creates and returns XML for a multiple choice response problem
"""
return 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']
)
def _verify_grades(self, raw_earned, raw_possible, weight, expected_score):
"""
Verifies the computed grades are as expected.
"""
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
# pylint: disable=no-member
for problem in self.problems:
problem.weight = weight
self.store.update_item(problem, self.user.id)
self.store.publish(self.course.location, self.user.id)
course_structure = get_course_blocks(self.request.user, self.course.location)
# answer all problems
for problem in self.problems:
answer_problem(self.course, self.request, problem, score=raw_earned, max_value=raw_possible)
# get grade
subsection_grade = SubsectionGradeFactory(
self.request.user, self.course, course_structure
).update(self.sequential)
# verify all problem grades
for problem in self.problems:
problem_score = subsection_grade.problem_scores[problem.location]
self.assertEqual(type(expected_score.first_attempted), type(problem_score.first_attempted))
expected_score.first_attempted = problem_score.first_attempted
self.assertEquals(problem_score, expected_score)
# verify subsection grades
self.assertEquals(subsection_grade.all_total.earned, expected_score.earned * len(self.problems))
self.assertEquals(subsection_grade.all_total.possible, expected_score.possible * len(self.problems))
@ddt.data(
*itertools.product(
(0.0, 0.5, 1.0, 2.0), # raw_earned
(-2.0, -1.0, 0.0, 0.5, 1.0, 2.0), # raw_possible
(-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 50.0, None), # weight
)
)
@ddt.unpack
def test_problem_weight(self, raw_earned, raw_possible, weight):
use_weight = weight is not None and raw_possible != 0
if use_weight:
expected_w_earned = raw_earned / raw_possible * weight
expected_w_possible = weight
else:
expected_w_earned = raw_earned
expected_w_possible = raw_possible
expected_graded = expected_w_possible > 0
expected_score = ProblemScore(
raw_earned=raw_earned,
raw_possible=raw_possible,
weighted_earned=expected_w_earned,
weighted_possible=expected_w_possible,
weight=weight,
graded=expected_graded,
first_attempted=datetime.datetime(2010, 1, 1),
)
self._verify_grades(raw_earned, raw_possible, weight, expected_score)
class TestScoreForModule(SharedModuleStoreTestCase):
"""
Test the method that calculates the score for a given block based on the
cumulative scores of its children. This test class uses a hard-coded block
hierarchy with scores as follows:
a
+--------+--------+
b c
+--------------+-----------+ |
d e f g
+-----+ +-----+-----+ | |
h i j k l m n
(2/5) (3/5) (0/1) - (1/3) - (3/10)
"""
@classmethod
def setUpClass(cls):
super(TestScoreForModule, cls).setUpClass()
cls.course = CourseFactory.create()
with cls.store.bulk_operations(cls.course.id):
cls.a = ItemFactory.create(parent=cls.course, category="chapter", display_name="a")
cls.b = ItemFactory.create(parent=cls.a, category="sequential", display_name="b")
cls.c = ItemFactory.create(parent=cls.a, category="sequential", display_name="c")
cls.d = ItemFactory.create(parent=cls.b, category="vertical", display_name="d")
cls.e = ItemFactory.create(parent=cls.b, category="vertical", display_name="e")
cls.f = ItemFactory.create(parent=cls.b, category="vertical", display_name="f")
cls.g = ItemFactory.create(parent=cls.c, category="vertical", display_name="g")
cls.h = ItemFactory.create(parent=cls.d, category="problem", display_name="h")
cls.i = ItemFactory.create(parent=cls.d, category="problem", display_name="i")
cls.j = ItemFactory.create(parent=cls.e, category="problem", display_name="j")
cls.k = ItemFactory.create(parent=cls.e, category="html", display_name="k")
cls.l = ItemFactory.create(parent=cls.e, category="problem", display_name="l")
cls.m = ItemFactory.create(parent=cls.f, category="html", display_name="m")
cls.n = ItemFactory.create(parent=cls.g, category="problem", display_name="n")
cls.request = get_mock_request(UserFactory())
CourseEnrollment.enroll(cls.request.user, cls.course.id)
answer_problem(cls.course, cls.request, cls.h, score=2, max_value=5)
answer_problem(cls.course, cls.request, cls.i, score=3, max_value=5)
answer_problem(cls.course, cls.request, cls.j, score=0, max_value=1)
answer_problem(cls.course, cls.request, cls.l, score=1, max_value=3)
answer_problem(cls.course, cls.request, cls.n, score=3, max_value=10)
cls.course_grade = CourseGradeFactory().read(cls.request.user, cls.course)
def test_score_chapter(self):
earned, possible = self.course_grade.score_for_module(self.a.location)
self.assertEqual(earned, 9)
self.assertEqual(possible, 24)
def test_score_section_many_leaves(self):
earned, possible = self.course_grade.score_for_module(self.b.location)
self.assertEqual(earned, 6)
self.assertEqual(possible, 14)
def test_score_section_one_leaf(self):
earned, possible = self.course_grade.score_for_module(self.c.location)
self.assertEqual(earned, 3)
self.assertEqual(possible, 10)
def test_score_vertical_two_leaves(self):
earned, possible = self.course_grade.score_for_module(self.d.location)
self.assertEqual(earned, 5)
self.assertEqual(possible, 10)
def test_score_vertical_two_leaves_one_unscored(self):
earned, possible = self.course_grade.score_for_module(self.e.location)
self.assertEqual(earned, 1)
self.assertEqual(possible, 4)
def test_score_vertical_no_score(self):
earned, possible = self.course_grade.score_for_module(self.f.location)
self.assertEqual(earned, 0)
self.assertEqual(possible, 0)
def test_score_vertical_one_leaf(self):
earned, possible = self.course_grade.score_for_module(self.g.location)
self.assertEqual(earned, 3)
self.assertEqual(possible, 10)
def test_score_leaf(self):
earned, possible = self.course_grade.score_for_module(self.h.location)
self.assertEqual(earned, 2)
self.assertEqual(possible, 5)
def test_score_leaf_no_score(self):
earned, possible = self.course_grade.score_for_module(self.m.location)
self.assertEqual(earned, 0)
self.assertEqual(possible, 0)
"""
Test saved subsection grade functionality.
"""
# pylint: disable=protected-access
import datetime
import itertools
import ddt
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 django.conf import settings
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
from mock import patch
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 ASSUME_ZERO_GRADE_IF_ABSENT, waffle
from ..course_data import CourseData
from ..course_grade import CourseGrade, ZeroCourseGrade
from ..course_grade_factory import CourseGradeFactory
from ..models import PersistentSubsectionGrade
from ..subsection_grade import SubsectionGrade, ZeroSubsectionGrade
from ..subsection_grade_factory import SubsectionGradeFactory
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
)
# AED 2017-06-19: make cls.sequence belong to multiple parents,
# so we can test that DAGs with this shape are handled correctly.
cls.chapter_2 = ItemFactory.create(
parent=cls.course,
category='chapter',
display_name='Test Chapter 2'
)
cls.chapter_2.children.append(cls.sequence.location)
cls.store.update_item(cls.chapter_2, UserFactory().id)
def setUp(self):
super(GradeTestBase, self).setUp()
self.request = get_mock_request(UserFactory())
self.client.login(username=self.request.user.username, password="test")
self._set_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 _set_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().read(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.read(self.request.user, self.course)
self.assertEqual(mock_read_grade.called, feature_flag and course_setting)
def test_read(self):
grade_factory = CourseGradeFactory()
def _assert_read(expected_pass, expected_percent):
"""
Creates the grade, ensuring it is as expected.
"""
course_grade = grade_factory.read(self.request.user, self.course)
self.assertEqual(course_grade.letter_grade, u'Pass' if expected_pass else None)
self.assertEqual(course_grade.percent, expected_percent)
with self.assertNumQueries(1), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0)
with self.assertNumQueries(37), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)
with self.assertNumQueries(1):
_assert_read(expected_pass=True, expected_percent=0.5)
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(*itertools.product((True, False), (True, False)))
@ddt.unpack
def test_read_zero(self, assume_zero_enabled, create_if_needed):
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
grade_factory = CourseGradeFactory()
course_grade = grade_factory.read(self.request.user, self.course, create_if_needed=create_if_needed)
if create_if_needed or assume_zero_enabled:
self._assert_zero_grade(course_grade, ZeroCourseGrade if assume_zero_enabled else CourseGrade)
else:
self.assertIsNone(course_grade)
def test_create_zero_subs_grade_for_nonzero_course_grade(self):
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)
@ddt.data(True, False)
def test_iter_force_update(self, force_update):
with patch('lms.djangoapps.grades.subsection_grade_factory.SubsectionGradeFactory.update') as mock_update:
set(CourseGradeFactory().iter(
users=[self.request.user], course=self.course, force_update=force_update
))
self.assertEqual(mock_update.called, force_update)
def test_course_grade_summary(self):
with mock_get_score(1, 2):
self.subsection_grade_factory.update(self.course_structure[self.sequence.location])
course_grade = CourseGradeFactory().update(self.request.user, self.course)
actual_summary = course_grade.summary
# We should have had a zero subsection grade for sequential 2, since we never
# gave it a mock score above.
expected_summary = {
'grade': None,
'grade_breakdown': {
'Homework': {
'category': 'Homework',
'percent': 0.25,
'detail': 'Homework = 25.00% of a possible 100.00%',
}
},
'percent': 0.25,
'section_breakdown': [
{
'category': 'Homework',
'detail': u'Homework 1 - Test Sequential 1 - 50% (1/2)',
'label': u'HW 01',
'percent': 0.5
},
{
'category': 'Homework',
'detail': u'Homework 2 - Test Sequential 2 - 0% (0/1)',
'label': u'HW 02',
'percent': 0.0
},
{
'category': 'Homework',
'detail': u'Homework Average = 25%',
'label': u'HW Avg',
'percent': 0.25,
'prominent': True
},
]
}
self.assertEqual(expected_summary, actual_summary)
@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_zero(self):
"""
Test that a zero grade is returned.
"""
grade = self.subsection_grade_factory.create(self.sequence)
self.assertIsInstance(grade, ZeroSubsectionGrade)
self.assert_grade(grade, 0.0, 1.0)
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_write_only_if_engaged(self):
"""
Test that scores are not persisted when a learner has
never attempted a problem, but are persisted if the
learner's state has been deleted.
"""
with mock_get_score(0, 0, None):
self.subsection_grade_factory.update(self.sequence)
# ensure no grades have been persisted
self.assertEqual(0, len(PersistentSubsectionGrade.objects.all()))
with mock_get_score(0, 0, None):
self.subsection_grade_factory.update(self.sequence, score_deleted=True)
# ensure a grade has been persisted
self.assertEqual(1, len(PersistentSubsectionGrade.objects.all()))
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)
@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.ddt
class ZeroGradeTest(GradeTestBase):
"""
Tests ZeroCourseGrade (and, implicitly, ZeroSubsectionGrade)
functionality.
"""
@ddt.data(True, False)
def test_zero(self, assume_zero_enabled):
"""
Creates a ZeroCourseGrade and ensures it's empty.
"""
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
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)
@ddt.data(True, False)
def test_zero_null_scores(self, assume_zero_enabled):
"""
Creates a zero course grade and ensures that null scores aren't included in the section problem scores.
"""
with waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=assume_zero_enabled):
with patch('lms.djangoapps.grades.subsection_grade.get_score', return_value=None):
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:
self.assertNotEqual({}, chapter_grades[chapter]['sections'])
for section in chapter_grades[chapter]['sections']:
self.assertEqual({}, section.problem_scores)
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.
"""
with mock_get_score(1, 2):
# 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 = input_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)
import datetime
import itertools
import ddt
import pytz
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from lms.djangoapps.course_blocks.api import get_course_blocks
from openedx.core.djangolib.testing.utils import get_mock_request
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from xmodule.graders import ProblemScore
from xmodule.modulestore import ModuleStoreEnum
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 ..subsection_grade_factory import SubsectionGradeFactory
from .utils import answer_problem, mock_get_submissions_score
@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)
@ddt.ddt
class TestWeightedProblems(SharedModuleStoreTestCase):
"""
Test scores and grades with various problem weight values.
"""
@classmethod
def setUpClass(cls):
super(TestWeightedProblems, 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="chapter")
cls.sequential = ItemFactory.create(parent=cls.chapter, category="sequential", display_name="sequential")
cls.vertical = ItemFactory.create(parent=cls.sequential, category="vertical", display_name="vertical1")
problem_xml = cls._create_problem_xml()
cls.problems = []
for i in range(2):
cls.problems.append(
ItemFactory.create(
parent=cls.vertical,
category="problem",
display_name="problem_{}".format(i),
data=problem_xml,
)
)
def setUp(self):
super(TestWeightedProblems, self).setUp()
self.user = UserFactory()
self.request = get_mock_request(self.user)
@classmethod
def _create_problem_xml(cls):
"""
Creates and returns XML for a multiple choice response problem
"""
return 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']
)
def _verify_grades(self, raw_earned, raw_possible, weight, expected_score):
"""
Verifies the computed grades are as expected.
"""
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
# pylint: disable=no-member
for problem in self.problems:
problem.weight = weight
self.store.update_item(problem, self.user.id)
self.store.publish(self.course.location, self.user.id)
course_structure = get_course_blocks(self.request.user, self.course.location)
# answer all problems
for problem in self.problems:
answer_problem(self.course, self.request, problem, score=raw_earned, max_value=raw_possible)
# get grade
subsection_grade = SubsectionGradeFactory(
self.request.user, self.course, course_structure
).update(self.sequential)
# verify all problem grades
for problem in self.problems:
problem_score = subsection_grade.problem_scores[problem.location]
self.assertEqual(type(expected_score.first_attempted), type(problem_score.first_attempted))
expected_score.first_attempted = problem_score.first_attempted
self.assertEquals(problem_score, expected_score)
# verify subsection grades
self.assertEquals(subsection_grade.all_total.earned, expected_score.earned * len(self.problems))
self.assertEquals(subsection_grade.all_total.possible, expected_score.possible * len(self.problems))
@ddt.data(
*itertools.product(
(0.0, 0.5, 1.0, 2.0), # raw_earned
(-2.0, -1.0, 0.0, 0.5, 1.0, 2.0), # raw_possible
(-2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 50.0, None), # weight
)
)
@ddt.unpack
def test_problem_weight(self, raw_earned, raw_possible, weight):
use_weight = weight is not None and raw_possible != 0
if use_weight:
expected_w_earned = raw_earned / raw_possible * weight
expected_w_possible = weight
else:
expected_w_earned = raw_earned
expected_w_possible = raw_possible
expected_graded = expected_w_possible > 0
expected_score = ProblemScore(
raw_earned=raw_earned,
raw_possible=raw_possible,
weighted_earned=expected_w_earned,
weighted_possible=expected_w_possible,
weight=weight,
graded=expected_graded,
first_attempted=datetime.datetime(2010, 1, 1),
)
self._verify_grades(raw_earned, raw_possible, weight, expected_score)
from ..models import PersistentSubsectionGrade
from ..subsection_grade import SubsectionGrade
from .utils import mock_get_score
from .base import GradeTestBase
class SubsectionGradeTest(GradeTestBase):
def test_save_and_load(self):
with mock_get_score(1, 2):
# 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 = input_grade.all_total.first_attempted = None
self.assertEqual(input_grade.all_total, loaded_grade.all_total)
import ddt
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
from django.conf import settings
from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags
from mock import patch
from ..models import PersistentSubsectionGrade
from ..subsection_grade_factory import ZeroSubsectionGrade
from .base import GradeTestBase
from .utils import mock_get_score
@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_zero(self):
"""
Test that a zero grade is returned.
"""
grade = self.subsection_grade_factory.create(self.sequence)
self.assertIsInstance(grade, ZeroSubsectionGrade)
self.assert_grade(grade, 0.0, 1.0)
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_write_only_if_engaged(self):
"""
Test that scores are not persisted when a learner has
never attempted a problem, but are persisted if the
learner's state has been deleted.
"""
with mock_get_score(0, 0, None):
self.subsection_grade_factory.update(self.sequence)
# ensure no grades have been persisted
self.assertEqual(0, len(PersistentSubsectionGrade.objects.all()))
with mock_get_score(0, 0, None):
self.subsection_grade_factory.update(self.sequence, score_deleted=True)
# ensure a grade has been persisted
self.assertEqual(1, len(PersistentSubsectionGrade.objects.all()))
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment