diff --git a/common/test/data/blocks/capa.xml b/common/test/data/blocks/capa.xml new file mode 100644 index 0000000000..a3460b1ef8 --- /dev/null +++ b/common/test/data/blocks/capa.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/common/test/data/blocks/coderesponse.xml b/common/test/data/blocks/coderesponse.xml new file mode 100644 index 0000000000..fde73aa327 --- /dev/null +++ b/common/test/data/blocks/coderesponse.xml @@ -0,0 +1,25 @@ + + +

+ ESTIMATED TIME TO COMPLETE: 4 minutes +

+
+      >>> print testList
+      [1, 16, 64, 81]
+    
+
+ + + + +# Your Code Here + + +def square(a): + return a * a +applyToEach(testList, square) + + {"grader": "finger_exercises/L6/applyToEach3/grade_ate3.py"} + + +
diff --git a/common/test/data/blocks/library_content.xml b/common/test/data/blocks/library_content.xml new file mode 100644 index 0000000000..6c8fc8105b --- /dev/null +++ b/common/test/data/blocks/library_content.xml @@ -0,0 +1,4 @@ + + + + diff --git a/common/test/data/blocks/lti.xml b/common/test/data/blocks/lti.xml new file mode 100644 index 0000000000..ae8bed35c1 --- /dev/null +++ b/common/test/data/blocks/lti.xml @@ -0,0 +1 @@ + diff --git a/common/test/data/blocks/openassessment.xml b/common/test/data/blocks/openassessment.xml new file mode 100644 index 0000000000..12b0919ba6 --- /dev/null +++ b/common/test/data/blocks/openassessment.xml @@ -0,0 +1,87 @@ + + + + + + + + + Censorship in the Libraries + + 'All of us can think of a book that we hope none of our children or any + other children have taken off the shelf. But if I have the right to remove + that book from the shelf -- that work I abhor -- then you also have exactly + the same right and so does everyone else. And then we have no books left on + the shelf for any of us.' --Katherine Paterson, Author + + Write a persuasive essay to a newspaper reflecting your views on censorship + in libraries. Do you believe that certain materials, such as books, music, + movies, magazines, etc., should be removed from the shelves if they are + found offensive? Support your position with convincing arguments from your + own experience, observations, and/or reading. + + Read for conciseness, clarity of thought, and form. + + + Ideas + Determine if there is a unifying theme or main idea. + + + + + + Content + Assess the content of the submission + + + + + + + diff --git a/lms/djangoapps/grades/tests/test_grades.py b/lms/djangoapps/grades/tests/test_grades.py index 0b493cf812..038fff9147 100644 --- a/lms/djangoapps/grades/tests/test_grades.py +++ b/lms/djangoapps/grades/tests/test_grades.py @@ -15,10 +15,10 @@ from lms.djangoapps.course_blocks.api import get_course_blocks from student.tests.factories import UserFactory from student.models import CourseEnrollment from xmodule.block_metadata_utils import display_name_with_default_escaped +from xmodule.graders import ProblemScore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.graders import ProblemScore from .utils import answer_problem from .. import course_grades diff --git a/lms/djangoapps/grades/tests/test_new.py b/lms/djangoapps/grades/tests/test_new.py index 25402508c6..4cecb8ee73 100644 --- a/lms/djangoapps/grades/tests/test_new.py +++ b/lms/djangoapps/grades/tests/test_new.py @@ -2,24 +2,30 @@ Test saved subsection grade functionality. """ # pylint: disable=protected-access + +import datetime + import ddt from django.conf import settings from django.db.utils import DatabaseError from mock import patch +import pytz from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from courseware.tests.helpers import get_request_for_user +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.lib.xblock_utils.test_utils import add_xml_block_from_file from student.models import CourseEnrollment from student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from ..models import PersistentSubsectionGrade from ..new.course_grade import CourseGradeFactory from ..new.subsection_grade import SubsectionGrade, SubsectionGradeFactory -from lms.djangoapps.grades.tests.utils import mock_get_score +from .utils import mock_get_score class GradeTestBase(SharedModuleStoreTestCase): @@ -230,3 +236,148 @@ class SubsectionGradeTest(GradeTestBase): self.assertEqual(input_grade.url_name, loaded_grade.url_name) self.assertEqual(input_grade.all_total, loaded_grade.all_total) + + +@ddt.ddt +class TestMultipleProblemTypesSubsectionScores(ModuleStoreTestCase, ProblemSubmissionTestMixin): + """ + Test grading of different problem types. + """ + + default_problem_metadata = { + u'graded': True, + u'weight': 2.5, + u'max_score': 7.0, + u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc), + } + + COURSE_NAME = u'Problem Type Test Course' + COURSE_NUM = u'probtype' + + 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_request_for_user(self.student) + self.course = CourseFactory.create( + display_name=self.COURSE_NAME, + number=self.COURSE_NUM + ) + self.chapter = ItemFactory.create( + parent=self.course, + category=u'chapter', + display_name=u'Test Chapter' + ) + self.seq1 = ItemFactory.create( + parent=self.chapter, + category=u'sequential', + display_name=u'Test Sequential 1', + graded=True + ) + self.vert1 = ItemFactory.create( + parent=self.seq1, + category=u'vertical', + display_name=u'Test Vertical 1' + ) + + def _get_fresh_subsection_score(self, course_structure, subsection): + """ + Return a Score object for the specified subsection. + + Ensures that a stale cached value is not returned. + """ + subsection_factory = SubsectionGradeFactory( + self.student, + course_structure=course_structure, + course=self.course, + ) + return subsection_factory.update(subsection) + + 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 _get_score_with_alterations(self, alterations): + """ + Given a dict of alterations to the default_problem_metadata, return + the score when one correct problem (out of two) is submitted. + """ + metadata = self._get_altered_metadata(alterations) + + add_xml_block_from_file(u'problem', u'capa.xml', parent=self.vert1, metadata=metadata) + course_structure = get_course_blocks(self.student, self.course.location) + + self.submit_question_answer(u'problem', {u'2_1': u'Correct'}) + return self._get_fresh_subsection_score(course_structure, self.seq1) + + def test_score_submission_for_capa_problems(self): + add_xml_block_from_file(u'problem', u'capa.xml', parent=self.vert1, metadata=self.default_problem_metadata) + course_structure = get_course_blocks(self.student, self.course.location) + + score = self._get_fresh_subsection_score(course_structure, self.seq1) + self.assertEqual(score.all_total.earned, 0.0) + self.assertEqual(score.all_total.possible, 2.5) + + self.submit_question_answer(u'problem', {u'2_1': u'Correct'}) + score = self._get_fresh_subsection_score(course_structure, self.seq1) + self.assertEqual(score.all_total.earned, 1.25) + self.assertEqual(score.all_total.possible, 2.5) + + @ddt.data( + (u'openassessment', u'openassessment.xml'), + (u'coderesponse', u'coderesponse.xml'), + (u'lti', u'lti.xml'), + (u'library_content', u'library_content.xml'), + ) + @ddt.unpack + def test_loading_different_problem_types(self, block_type, filename): + """ + Test that transformation works for various block types + """ + metadata = self.default_problem_metadata.copy() + if block_type == u'library_content': + # Library content does not have a weight + del metadata[u'weight'] + add_xml_block_from_file(block_type, filename, parent=self.vert1, metadata=metadata) + + @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): + score = self._get_score_with_alterations(alterations) + 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): + score = self._get_score_with_alterations(alterations) + self.assertEqual(score.graded_total.earned, expected_earned) + self.assertEqual(score.graded_total.possible, expected_possible) + + @ddt.data( + {u'max_score': 99.3}, + {u'max_score': 1.0}, + {u'max_score': 0.0}, + {u'max_score': None}, + ) + def test_max_score_does_not_change_results(self, alterations): + expected_earned = 1.25 + expected_possible = 2.5 + score = self._get_score_with_alterations(alterations) + self.assertEqual(score.all_total.earned, expected_earned) + self.assertEqual(score.all_total.possible, expected_possible) diff --git a/openedx/core/lib/xblock_utils.py b/openedx/core/lib/xblock_utils/__init__.py similarity index 100% rename from openedx/core/lib/xblock_utils.py rename to openedx/core/lib/xblock_utils/__init__.py diff --git a/openedx/core/lib/xblock_utils/test_utils.py b/openedx/core/lib/xblock_utils/test_utils.py new file mode 100644 index 0000000000..ac1b145dc0 --- /dev/null +++ b/openedx/core/lib/xblock_utils/test_utils.py @@ -0,0 +1,26 @@ +""" +Utilities for testing xblocks +""" + +from django.conf import settings + +from xmodule.modulestore.tests.factories import ItemFactory + +TEST_DATA_DIR = settings.COMMON_ROOT / u'test/data' + + +def add_xml_block_from_file(block_type, filename, parent, metadata): + """ + Create a block of the specified type with content included from the + specified XML file. + + XML filenames are relative to common/test/data/blocks. + """ + with open(TEST_DATA_DIR / u'blocks' / filename) as datafile: + return ItemFactory.create( + parent=parent, + category=block_type, + data=datafile.read().decode('utf-8'), + metadata=metadata, + display_name=u'problem' + )