Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
0de55de2
Commit
0de55de2
authored
Oct 02, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Grades: Clean up tests
EDUCATOR-1404
parent
02410818
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1054 additions
and
1034 deletions
+1054
-1034
lms/djangoapps/grades/tests/base.py
+100
-0
lms/djangoapps/grades/tests/test_course_grade.py
+145
-0
lms/djangoapps/grades/tests/test_course_grade_factory.py
+362
-0
lms/djangoapps/grades/tests/test_grades.py
+0
-335
lms/djangoapps/grades/tests/test_new.py
+0
-699
lms/djangoapps/grades/tests/test_problems.py
+306
-0
lms/djangoapps/grades/tests/test_subsection_grade.py
+40
-0
lms/djangoapps/grades/tests/test_subsection_grade_factory.py
+101
-0
No files found.
lms/djangoapps/grades/tests/base.py
0 → 100644
View file @
0de55de2
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
)
lms/djangoapps/grades/tests/test_course_grade.py
0 → 100644
View file @
0de55de2
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
)
lms/djangoapps/grades/tests/test_course_grade_factory.py
0 → 100644
View file @
0de55de2
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
)
lms/djangoapps/grades/tests/test_grades.py
deleted
100644 → 0
View file @
02410818
"""
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
)
lms/djangoapps/grades/tests/test_new.py
deleted
100644 → 0
View file @
02410818
"""
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
)
lms/djangoapps/grades/tests/test_problems.py
0 → 100644
View file @
0de55de2
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
)
lms/djangoapps/grades/tests/test_subsection_grade.py
0 → 100644
View file @
0de55de2
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
)
lms/djangoapps/grades/tests/test_subsection_grade_factory.py
0 → 100644
View file @
0de55de2
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
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment