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
b5435e6f
Commit
b5435e6f
authored
Oct 04, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Course Grade reports: eliminate CSM calls, user-transformed structures
EDUCATOR-558
parent
09b235bc
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
214 additions
and
119 deletions
+214
-119
common/lib/xmodule/xmodule/graders.py
+22
-23
lms/djangoapps/grades/context.py
+9
-7
lms/djangoapps/grades/course_data.py
+7
-7
lms/djangoapps/grades/course_grade.py
+27
-19
lms/djangoapps/grades/course_grade_factory.py
+1
-1
lms/djangoapps/grades/subsection_grade.py
+22
-5
lms/djangoapps/grades/subsection_grade_factory.py
+2
-6
lms/djangoapps/grades/tests/base.py
+9
-0
lms/djangoapps/grades/tests/test_course_grade_factory.py
+45
-4
lms/djangoapps/grades/tests/test_subsection_grade.py
+3
-3
lms/djangoapps/grades/tests/test_tasks.py
+8
-8
lms/djangoapps/instructor_analytics/basic.py
+2
-2
lms/djangoapps/instructor_task/tasks_helper/grades.py
+49
-21
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+8
-13
No files found.
common/lib/xmodule/xmodule/graders.py
View file @
b5435e6f
...
@@ -170,7 +170,6 @@ def grader_from_conf(conf):
...
@@ -170,7 +170,6 @@ def grader_from_conf(conf):
weight
=
subgraderconf
.
pop
(
"weight"
,
0
)
weight
=
subgraderconf
.
pop
(
"weight"
,
0
)
try
:
try
:
if
'min_count'
in
subgraderconf
:
if
'min_count'
in
subgraderconf
:
#This is an AssignmentFormatGrader
subgrader_class
=
AssignmentFormatGrader
subgrader_class
=
AssignmentFormatGrader
else
:
else
:
raise
ValueError
(
"Configuration has no appropriate grader class."
)
raise
ValueError
(
"Configuration has no appropriate grader class."
)
...
@@ -344,28 +343,28 @@ class AssignmentFormatGrader(CourseGrader):
...
@@ -344,28 +343,28 @@ class AssignmentFormatGrader(CourseGrader):
self
.
starting_index
=
starting_index
self
.
starting_index
=
starting_index
self
.
hide_average
=
hide_average
self
.
hide_average
=
hide_average
def
grade
(
self
,
grade_sheet
,
generate_random_scores
=
False
):
def
total_with_drops
(
self
,
breakdown
):
def
total_with_drops
(
breakdown
,
drop_count
):
"""
"""
Calculates total score for a section while dropping lowest scores
Calculates total score for a section while dropping lowest scores
"""
"""
# Create an array of tuples with (index, mark), sorted by mark['percent'] descending
# Create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown
=
sorted
(
enumerate
(
breakdown
),
key
=
lambda
x
:
-
x
[
1
][
'percent'
])
sorted_breakdown
=
sorted
(
enumerate
(
breakdown
),
key
=
lambda
x
:
-
x
[
1
][
'percent'
])
# A list of the indices of the dropped scores
# A list of the indices of the dropped scores
dropped_indices
=
[]
dropped_indices
=
[]
if
self
.
drop_count
>
0
:
if
drop_count
>
0
:
dropped_indices
=
[
x
[
0
]
for
x
in
sorted_breakdown
[
-
self
.
drop_count
:]]
dropped_indices
=
[
x
[
0
]
for
x
in
sorted_breakdown
[
-
drop_count
:]]
aggregate_score
=
0
aggregate_score
=
0
for
index
,
mark
in
enumerate
(
breakdown
):
for
index
,
mark
in
enumerate
(
breakdown
):
if
index
not
in
dropped_indices
:
if
index
not
in
dropped_indices
:
aggregate_score
+=
mark
[
'percent'
]
aggregate_score
+=
mark
[
'percent'
]
if
len
(
breakdown
)
-
drop_count
>
0
:
aggregate_score
/=
len
(
breakdown
)
-
drop_count
return
aggregate_score
,
dropped_indices
if
len
(
breakdown
)
-
self
.
drop_count
>
0
:
aggregate_score
/=
len
(
breakdown
)
-
self
.
drop_count
return
aggregate_score
,
dropped_indices
def
grade
(
self
,
grade_sheet
,
generate_random_scores
=
False
):
scores
=
grade_sheet
.
get
(
self
.
type
,
{})
.
values
()
scores
=
grade_sheet
.
get
(
self
.
type
,
{})
.
values
()
breakdown
=
[]
breakdown
=
[]
for
i
in
range
(
max
(
self
.
min_count
,
len
(
scores
))):
for
i
in
range
(
max
(
self
.
min_count
,
len
(
scores
))):
...
@@ -405,7 +404,7 @@ class AssignmentFormatGrader(CourseGrader):
...
@@ -405,7 +404,7 @@ class AssignmentFormatGrader(CourseGrader):
breakdown
.
append
({
'percent'
:
percentage
,
'label'
:
short_label
,
breakdown
.
append
({
'percent'
:
percentage
,
'label'
:
short_label
,
'detail'
:
summary
,
'category'
:
self
.
category
})
'detail'
:
summary
,
'category'
:
self
.
category
})
total_percent
,
dropped_indices
=
total_with_drops
(
breakdown
,
self
.
drop_count
)
total_percent
,
dropped_indices
=
self
.
total_with_drops
(
breakdown
)
for
dropped_index
in
dropped_indices
:
for
dropped_index
in
dropped_indices
:
breakdown
[
dropped_index
][
'mark'
]
=
{
breakdown
[
dropped_index
][
'mark'
]
=
{
...
...
lms/djangoapps/grades/context.py
View file @
b5435e6f
...
@@ -5,18 +5,19 @@ from collections import OrderedDict
...
@@ -5,18 +5,19 @@ from collections import OrderedDict
from
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
.course_grade
import
CourseGrade
from
.scores
import
possibly_scored
from
.scores
import
possibly_scored
def
grading_context_for_course
(
course
_key
):
def
grading_context_for_course
(
course
):
"""
"""
Same as grading_context, but takes in a course key.
Same as grading_context, but takes in a course key.
"""
"""
course_structure
=
get_course_in_cache
(
course
_key
)
course_structure
=
get_course_in_cache
(
course
.
id
)
return
grading_context
(
course_structure
)
return
grading_context
(
course
,
course
_structure
)
def
grading_context
(
course_structure
):
def
grading_context
(
course
,
course
_structure
):
"""
"""
This returns a dictionary with keys necessary for quickly grading
This returns a dictionary with keys necessary for quickly grading
a student.
a student.
...
@@ -36,7 +37,7 @@ def grading_context(course_structure):
...
@@ -36,7 +37,7 @@ def grading_context(course_structure):
the descriptor tree again.
the descriptor tree again.
"""
"""
all_graded_blocks
=
[]
count_all_graded_blocks
=
0
all_graded_subsections_by_type
=
OrderedDict
()
all_graded_subsections_by_type
=
OrderedDict
()
for
chapter_key
in
course_structure
.
get_children
(
course_structure
.
root_block_usage_key
):
for
chapter_key
in
course_structure
.
get_children
(
course_structure
.
root_block_usage_key
):
...
@@ -64,9 +65,10 @@ def grading_context(course_structure):
...
@@ -64,9 +65,10 @@ def grading_context(course_structure):
if
subsection_format
not
in
all_graded_subsections_by_type
:
if
subsection_format
not
in
all_graded_subsections_by_type
:
all_graded_subsections_by_type
[
subsection_format
]
=
[]
all_graded_subsections_by_type
[
subsection_format
]
=
[]
all_graded_subsections_by_type
[
subsection_format
]
.
append
(
subsection_info
)
all_graded_subsections_by_type
[
subsection_format
]
.
append
(
subsection_info
)
all_graded_blocks
.
extend
(
scored_descendants_of_subsection
)
count_all_graded_blocks
+=
len
(
scored_descendants_of_subsection
)
return
{
return
{
'all_graded_subsections_by_type'
:
all_graded_subsections_by_type
,
'all_graded_subsections_by_type'
:
all_graded_subsections_by_type
,
'all_graded_blocks'
:
all_graded_blocks
,
'count_all_graded_blocks'
:
count_all_graded_blocks
,
'subsection_type_graders'
:
CourseGrade
.
get_subsection_type_graders
(
course
)
}
}
lms/djangoapps/grades/course_data.py
View file @
b5435e6f
...
@@ -32,14 +32,14 @@ class CourseData(object):
...
@@ -32,14 +32,14 @@ class CourseData(object):
if
self
.
_course
:
if
self
.
_course
:
self
.
_course_key
=
self
.
_course
.
id
self
.
_course_key
=
self
.
_course
.
id
else
:
else
:
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
self
.
_course_key
=
structure
.
root_block_usage_key
.
course_key
self
.
_course_key
=
structure
.
root_block_usage_key
.
course_key
return
self
.
_course_key
return
self
.
_course_key
@property
@property
def
location
(
self
):
def
location
(
self
):
if
not
self
.
_location
:
if
not
self
.
_location
:
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
if
structure
:
if
structure
:
self
.
_location
=
structure
.
root_block_usage_key
self
.
_location
=
structure
.
root_block_usage_key
elif
self
.
_course
:
elif
self
.
_course
:
...
@@ -72,7 +72,7 @@ class CourseData(object):
...
@@ -72,7 +72,7 @@ class CourseData(object):
@property
@property
def
grading_policy_hash
(
self
):
def
grading_policy_hash
(
self
):
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
if
structure
:
if
structure
:
return
structure
.
get_transformer_block_field
(
return
structure
.
get_transformer_block_field
(
structure
.
root_block_usage_key
,
structure
.
root_block_usage_key
,
...
@@ -84,14 +84,14 @@ class CourseData(object):
...
@@ -84,14 +84,14 @@ class CourseData(object):
@property
@property
def
version
(
self
):
def
version
(
self
):
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
course_block
=
structure
[
self
.
location
]
if
structure
else
self
.
course
course_block
=
structure
[
self
.
location
]
if
structure
else
self
.
course
return
getattr
(
course_block
,
'course_version'
,
None
)
return
getattr
(
course_block
,
'course_version'
,
None
)
@property
@property
def
edited_on
(
self
):
def
edited_on
(
self
):
# get course block from structure only; subtree_edited_on field on modulestore's course block isn't optimized.
# get course block from structure only; subtree_edited_on field on modulestore's course block isn't optimized.
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
if
structure
:
if
structure
:
course_block
=
structure
[
self
.
location
]
course_block
=
structure
[
self
.
location
]
return
getattr
(
course_block
,
'subtree_edited_on'
,
None
)
return
getattr
(
course_block
,
'subtree_edited_on'
,
None
)
...
@@ -100,7 +100,7 @@ class CourseData(object):
...
@@ -100,7 +100,7 @@ class CourseData(object):
return
u'Course: course_key: {}'
.
format
(
self
.
course_key
)
return
u'Course: course_key: {}'
.
format
(
self
.
course_key
)
def
full_string
(
self
):
def
full_string
(
self
):
if
self
.
_
effective_structure
:
if
self
.
effective_structure
:
return
u'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'
.
format
(
return
u'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'
.
format
(
self
.
course_key
,
self
.
version
,
self
.
edited_on
,
self
.
grading_policy_hash
,
self
.
course_key
,
self
.
version
,
self
.
edited_on
,
self
.
grading_policy_hash
,
)
)
...
@@ -108,5 +108,5 @@ class CourseData(object):
...
@@ -108,5 +108,5 @@ class CourseData(object):
return
u'Course: course_key: {}, empty course structure'
.
format
(
self
.
course_key
)
return
u'Course: course_key: {}, empty course structure'
.
format
(
self
.
course_key
)
@property
@property
def
_
effective_structure
(
self
):
def
effective_structure
(
self
):
return
self
.
_structure
or
self
.
_collected_block_structure
return
self
.
_structure
or
self
.
_collected_block_structure
lms/djangoapps/grades/course_grade.py
View file @
b5435e6f
...
@@ -10,6 +10,7 @@ from lazy import lazy
...
@@ -10,6 +10,7 @@ from lazy import lazy
from
ccx_keys.locator
import
CCXLocator
from
ccx_keys.locator
import
CCXLocator
from
xmodule
import
block_metadata_utils
from
xmodule
import
block_metadata_utils
from
.config
import
assume_zero_if_absent
from
.subsection_grade
import
ZeroSubsectionGrade
from
.subsection_grade
import
ZeroSubsectionGrade
from
.subsection_grade_factory
import
SubsectionGradeFactory
from
.subsection_grade_factory
import
SubsectionGradeFactory
...
@@ -46,17 +47,14 @@ class CourseGradeBase(object):
...
@@ -46,17 +47,14 @@ class CourseGradeBase(object):
def
subsection_grade
(
self
,
subsection_key
):
def
subsection_grade
(
self
,
subsection_key
):
"""
"""
Returns the subsection grade for given subsection usage key.
Returns the subsection grade for the given subsection usage key.
Raises KeyError if the user doesn't have access to that subsection.
"""
return
self
.
_get_subsection_grade
(
self
.
course_data
.
structure
[
subsection_key
])
@abstractmethod
Note: does NOT check whether the user has access to the subsection.
def
assignment_average
(
self
,
assignment_type
):
Assumes that if a grade exists, the user has access to it. If the
"""
grade doesn't exist then either the user does not have access to
Returns the average of all assignments of the given assignment type
.
it or hasn't attempted any problems in the subsection
.
"""
"""
r
aise
NotImplementedError
r
eturn
self
.
_get_subsection_grade
(
self
.
course_data
.
effective_structure
[
subsection_key
])
@lazy
@lazy
def
graded_subsections_by_format
(
self
):
def
graded_subsections_by_format
(
self
):
...
@@ -148,7 +146,7 @@ class CourseGradeBase(object):
...
@@ -148,7 +146,7 @@ class CourseGradeBase(object):
"""
"""
Returns the result from the course grader.
Returns the result from the course grader.
"""
"""
course
=
self
.
_prep_course_for_grading
()
course
=
self
.
_prep_course_for_grading
(
self
.
course_data
.
course
)
return
course
.
grader
.
grade
(
return
course
.
grader
.
grade
(
self
.
graded_subsections_by_format
,
self
.
graded_subsections_by_format
,
generate_random_scores
=
settings
.
GENERATE_PROFILE_SCORES
,
generate_random_scores
=
settings
.
GENERATE_PROFILE_SCORES
,
...
@@ -166,7 +164,21 @@ class CourseGradeBase(object):
...
@@ -166,7 +164,21 @@ class CourseGradeBase(object):
grade_summary
[
'grade'
]
=
self
.
letter_grade
grade_summary
[
'grade'
]
=
self
.
letter_grade
return
grade_summary
return
grade_summary
def
_prep_course_for_grading
(
self
):
@classmethod
def
get_subsection_type_graders
(
cls
,
course
):
"""
Returns a dictionary mapping subsection types to their
corresponding configured graders, per grading policy.
"""
course
=
cls
.
_prep_course_for_grading
(
course
)
return
{
subsection_type
:
subsection_type_grader
for
(
subsection_type_grader
,
subsection_type
,
_
)
in
course
.
grader
.
subgraders
}
@classmethod
def
_prep_course_for_grading
(
cls
,
course
):
"""
"""
Make sure any overrides to the grading policy are used.
Make sure any overrides to the grading policy are used.
This is most relevant for CCX courses.
This is most relevant for CCX courses.
...
@@ -176,8 +188,7 @@ class CourseGradeBase(object):
...
@@ -176,8 +188,7 @@ class CourseGradeBase(object):
this will no longer be needed - since BlockStructure correctly
this will no longer be needed - since BlockStructure correctly
retrieves/uses all field overrides.
retrieves/uses all field overrides.
"""
"""
course
=
self
.
course_data
.
course
if
isinstance
(
course
.
id
,
CCXLocator
):
if
isinstance
(
self
.
course_data
.
course_key
,
CCXLocator
):
# clean out any field values that may have been set from the
# clean out any field values that may have been set from the
# parent course of the CCX course.
# parent course of the CCX course.
course
.
_field_data_cache
=
{}
# pylint: disable=protected-access
course
.
_field_data_cache
=
{}
# pylint: disable=protected-access
...
@@ -221,9 +232,6 @@ class ZeroCourseGrade(CourseGradeBase):
...
@@ -221,9 +232,6 @@ class ZeroCourseGrade(CourseGradeBase):
Course Grade class for Zero-value grades when no problems were
Course Grade class for Zero-value grades when no problems were
attempted in the course.
attempted in the course.
"""
"""
def
assignment_average
(
self
,
assignment_type
):
return
0.0
def
_get_subsection_grade
(
self
,
subsection
):
def
_get_subsection_grade
(
self
,
subsection
):
return
ZeroSubsectionGrade
(
subsection
,
self
.
course_data
)
return
ZeroSubsectionGrade
(
subsection
,
self
.
course_data
)
...
@@ -259,15 +267,15 @@ class CourseGrade(CourseGradeBase):
...
@@ -259,15 +267,15 @@ class CourseGrade(CourseGradeBase):
Returns whether any of the subsections in this course
Returns whether any of the subsections in this course
have been attempted by the student.
have been attempted by the student.
"""
"""
if
assume_zero_if_absent
(
self
.
course_data
.
course_key
):
return
True
for
chapter
in
self
.
chapter_grades
.
itervalues
():
for
chapter
in
self
.
chapter_grades
.
itervalues
():
for
subsection_grade
in
chapter
[
'sections'
]:
for
subsection_grade
in
chapter
[
'sections'
]:
if
subsection_grade
.
all_total
.
first_attempted
:
if
subsection_grade
.
all_total
.
first_attempted
:
return
True
return
True
return
False
return
False
def
assignment_average
(
self
,
assignment_type
):
return
self
.
grader_result
[
'grade_breakdown'
]
.
get
(
assignment_type
,
{})
.
get
(
'percent'
)
def
_get_subsection_grade
(
self
,
subsection
):
def
_get_subsection_grade
(
self
,
subsection
):
if
self
.
force_update_subsections
:
if
self
.
force_update_subsections
:
return
self
.
_subsection_grade_factory
.
update
(
subsection
)
return
self
.
_subsection_grade_factory
.
update
(
subsection
)
...
...
lms/djangoapps/grades/course_grade_factory.py
View file @
b5435e6f
...
@@ -85,7 +85,7 @@ class CourseGradeFactory(object):
...
@@ -85,7 +85,7 @@ class CourseGradeFactory(object):
If an error occurred, course_grade will be None and err_msg will be an
If an error occurred, course_grade will be None and err_msg will be an
exception message. If there was no error, err_msg is an empty string.
exception message. If there was no error, err_msg is an empty string.
"""
"""
# Pre-fetch the collected course_structure so:
# Pre-fetch the collected course_structure
(in _iter_grade_result)
so:
# 1. Correctness: the same version of the course is used to
# 1. Correctness: the same version of the course is used to
# compute the grade for all students.
# compute the grade for all students.
# 2. Optimization: the collected course_structure is not
# 2. Optimization: the collected course_structure is not
...
...
lms/djangoapps/grades/subsection_grade.py
View file @
b5435e6f
...
@@ -63,6 +63,13 @@ class SubsectionGradeBase(object):
...
@@ -63,6 +63,13 @@ class SubsectionGradeBase(object):
"""
"""
raise
NotImplementedError
raise
NotImplementedError
@property
def
percent_graded
(
self
):
"""
Returns the percent score of the graded problems in this subsection.
"""
raise
NotImplementedError
class
ZeroSubsectionGrade
(
SubsectionGradeBase
):
class
ZeroSubsectionGrade
(
SubsectionGradeBase
):
"""
"""
...
@@ -78,6 +85,10 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
...
@@ -78,6 +85,10 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
return
False
return
False
@property
@property
def
percent_graded
(
self
):
return
0.0
@property
def
all_total
(
self
):
def
all_total
(
self
):
return
self
.
_aggregate_scores
[
0
]
return
self
.
_aggregate_scores
[
0
]
...
@@ -128,6 +139,10 @@ class NonZeroSubsectionGrade(SubsectionGradeBase):
...
@@ -128,6 +139,10 @@ class NonZeroSubsectionGrade(SubsectionGradeBase):
def
attempted_graded
(
self
):
def
attempted_graded
(
self
):
return
self
.
graded_total
.
first_attempted
is
not
None
return
self
.
graded_total
.
first_attempted
is
not
None
@property
def
percent_graded
(
self
):
return
self
.
graded_total
.
earned
/
self
.
graded_total
.
possible
@staticmethod
@staticmethod
def
_compute_block_score
(
def
_compute_block_score
(
block_key
,
block_key
,
...
@@ -157,7 +172,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
...
@@ -157,7 +172,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
"""
"""
Class for Subsection grades that are read from the database.
Class for Subsection grades that are read from the database.
"""
"""
def
__init__
(
self
,
subsection
,
model
,
course_structure
,
submissions_scores
,
csm_scores
):
def
__init__
(
self
,
subsection
,
model
,
factory
):
all_total
=
AggregatedScore
(
all_total
=
AggregatedScore
(
tw_earned
=
model
.
earned_all
,
tw_earned
=
model
.
earned_all
,
tw_possible
=
model
.
possible_all
,
tw_possible
=
model
.
possible_all
,
...
@@ -174,9 +189,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
...
@@ -174,9 +189,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
# save these for later since we compute problem_scores lazily
# save these for later since we compute problem_scores lazily
self
.
model
=
model
self
.
model
=
model
self
.
course_structure
=
course_structure
self
.
factory
=
factory
self
.
submissions_scores
=
submissions_scores
self
.
csm_scores
=
csm_scores
super
(
ReadSubsectionGrade
,
self
)
.
__init__
(
subsection
,
all_total
,
graded_total
,
override
)
super
(
ReadSubsectionGrade
,
self
)
.
__init__
(
subsection
,
all_total
,
graded_total
,
override
)
...
@@ -185,7 +198,11 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
...
@@ -185,7 +198,11 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
problem_scores
=
OrderedDict
()
problem_scores
=
OrderedDict
()
for
block
in
self
.
model
.
visible_blocks
.
blocks
:
for
block
in
self
.
model
.
visible_blocks
.
blocks
:
problem_score
=
self
.
_compute_block_score
(
problem_score
=
self
.
_compute_block_score
(
block
.
locator
,
self
.
course_structure
,
self
.
submissions_scores
,
self
.
csm_scores
,
block
,
block
.
locator
,
self
.
factory
.
course_data
.
structure
,
self
.
factory
.
_submissions_scores
,
self
.
factory
.
_csm_scores
,
block
,
)
)
if
problem_score
:
if
problem_score
:
problem_scores
[
block
.
locator
]
=
problem_score
problem_scores
[
block
.
locator
]
=
problem_score
...
...
lms/djangoapps/grades/subsection_grade_factory.py
View file @
b5435e6f
...
@@ -80,9 +80,7 @@ class SubsectionGradeFactory(object):
...
@@ -80,9 +80,7 @@ class SubsectionGradeFactory(object):
except
PersistentSubsectionGrade
.
DoesNotExist
:
except
PersistentSubsectionGrade
.
DoesNotExist
:
pass
pass
else
:
else
:
orig_subsection_grade
=
ReadSubsectionGrade
(
orig_subsection_grade
=
ReadSubsectionGrade
(
subsection
,
grade_model
,
self
)
subsection
,
grade_model
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
if
not
is_score_higher_or_equal
(
if
not
is_score_higher_or_equal
(
orig_subsection_grade
.
graded_total
.
earned
,
orig_subsection_grade
.
graded_total
.
earned
,
orig_subsection_grade
.
graded_total
.
possible
,
orig_subsection_grade
.
graded_total
.
possible
,
...
@@ -125,9 +123,7 @@ class SubsectionGradeFactory(object):
...
@@ -125,9 +123,7 @@ class SubsectionGradeFactory(object):
saved_subsection_grades
=
self
.
_get_bulk_cached_subsection_grades
()
saved_subsection_grades
=
self
.
_get_bulk_cached_subsection_grades
()
grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
if
grade
:
if
grade
:
return
ReadSubsectionGrade
(
return
ReadSubsectionGrade
(
subsection
,
grade
,
self
)
subsection
,
grade
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
def
_get_bulk_cached_subsection_grades
(
self
):
def
_get_bulk_cached_subsection_grades
(
self
):
"""
"""
...
...
lms/djangoapps/grades/tests/base.py
View file @
b5435e6f
...
@@ -6,6 +6,7 @@ from student.tests.factories import UserFactory
...
@@ -6,6 +6,7 @@ from student.tests.factories import UserFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
..course_data
import
CourseData
from
..subsection_grade_factory
import
SubsectionGradeFactory
from
..subsection_grade_factory
import
SubsectionGradeFactory
...
@@ -75,6 +76,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
...
@@ -75,6 +76,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
self
.
client
.
login
(
username
=
self
.
request
.
user
.
username
,
password
=
"test"
)
self
.
client
.
login
(
username
=
self
.
request
.
user
.
username
,
password
=
"test"
)
self
.
_set_grading_policy
()
self
.
_set_grading_policy
()
self
.
course_structure
=
get_course_blocks
(
self
.
request
.
user
,
self
.
course
.
location
)
self
.
course_structure
=
get_course_blocks
(
self
.
request
.
user
,
self
.
course
.
location
)
self
.
course_data
=
CourseData
(
self
.
request
.
user
,
structure
=
self
.
course_structure
)
self
.
subsection_grade_factory
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
self
.
subsection_grade_factory
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
CourseEnrollment
.
enroll
(
self
.
request
.
user
,
self
.
course
.
id
)
CourseEnrollment
.
enroll
(
self
.
request
.
user
,
self
.
course
.
id
)
...
@@ -91,6 +93,13 @@ class GradeTestBase(SharedModuleStoreTestCase):
...
@@ -91,6 +93,13 @@ class GradeTestBase(SharedModuleStoreTestCase):
"short_label"
:
"HW"
,
"short_label"
:
"HW"
,
"weight"
:
1.0
,
"weight"
:
1.0
,
},
},
{
"type"
:
"NoCredit"
,
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
"NC"
,
"weight"
:
0.0
,
},
],
],
"GRADE_CUTOFFS"
:
{
"GRADE_CUTOFFS"
:
{
"Pass"
:
passing
,
"Pass"
:
passing
,
...
...
lms/djangoapps/grades/tests/test_course_grade_factory.py
View file @
b5435e6f
...
@@ -97,19 +97,19 @@ class TestCourseGradeFactory(GradeTestBase):
...
@@ -97,19 +97,19 @@ class TestCourseGradeFactory(GradeTestBase):
with
self
.
assertNumQueries
(
29
),
mock_get_score
(
1
,
2
):
with
self
.
assertNumQueries
(
29
),
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
with
self
.
assertNumQueries
(
4
):
with
self
.
assertNumQueries
(
2
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# updated to grade of .5
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# updated to grade of .5
with
self
.
assertNumQueries
(
6
),
mock_get_score
(
1
,
4
):
with
self
.
assertNumQueries
(
4
),
mock_get_score
(
1
,
4
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
False
)
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
False
)
with
self
.
assertNumQueries
(
4
):
with
self
.
assertNumQueries
(
2
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# NOT updated to grade of .25
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# NOT updated to grade of .25
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
2
,
2
):
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
2
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
with
self
.
assertNumQueries
(
4
):
with
self
.
assertNumQueries
(
2
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
1.0
)
# updated to grade of 1.0
_assert_read
(
expected_pass
=
True
,
expected_percent
=
1.0
)
# updated to grade of 1.0
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
...
@@ -124,6 +124,35 @@ class TestCourseGradeFactory(GradeTestBase):
...
@@ -124,6 +124,35 @@ class TestCourseGradeFactory(GradeTestBase):
else
:
else
:
self
.
assertIsNone
(
course_grade
)
self
.
assertIsNone
(
course_grade
)
def
test_read_optimization
(
self
):
grade_factory
=
CourseGradeFactory
()
with
patch
(
'lms.djangoapps.grades.course_data.get_course_blocks'
)
as
mocked_course_blocks
:
mocked_course_blocks
.
return_value
=
self
.
course_structure
with
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
self
.
assertEquals
(
mocked_course_blocks
.
call_count
,
1
)
with
patch
(
'lms.djangoapps.grades.course_data.get_course_blocks'
)
as
mocked_course_blocks
:
with
patch
(
'lms.djangoapps.grades.subsection_grade.get_score'
)
as
mocked_get_score
:
course_grade
=
grade_factory
.
read
(
self
.
request
.
user
,
self
.
course
)
self
.
assertEqual
(
course_grade
.
percent
,
0.5
)
# make sure it's not a zero-valued course grade
self
.
assertFalse
(
mocked_get_score
.
called
)
# no calls to CSM/submissions tables
self
.
assertFalse
(
mocked_course_blocks
.
called
)
# no user-specific transformer calculation
def
test_subsection_grade
(
self
):
grade_factory
=
CourseGradeFactory
()
with
mock_get_score
(
1
,
2
):
grade_factory
.
update
(
self
.
request
.
user
,
self
.
course
,
force_update_subsections
=
True
)
course_grade
=
grade_factory
.
read
(
self
.
request
.
user
,
course_structure
=
self
.
course_structure
)
subsection_grade
=
course_grade
.
subsection_grade
(
self
.
sequence
.
location
)
self
.
assertEqual
(
subsection_grade
.
percent_graded
,
0.5
)
def
test_subsection_type_graders
(
self
):
graders
=
CourseGrade
.
get_subsection_type_graders
(
self
.
course
)
self
.
assertEqual
(
len
(
graders
),
2
)
self
.
assertEqual
(
graders
[
"Homework"
]
.
type
,
"Homework"
)
self
.
assertEqual
(
graders
[
"NoCredit"
]
.
min_count
,
0
)
def
test_create_zero_subs_grade_for_nonzero_course_grade
(
self
):
def
test_create_zero_subs_grade_for_nonzero_course_grade
(
self
):
subsection
=
self
.
course_structure
[
self
.
sequence
.
location
]
subsection
=
self
.
course_structure
[
self
.
sequence
.
location
]
with
mock_get_score
(
1
,
2
):
with
mock_get_score
(
1
,
2
):
...
@@ -158,6 +187,11 @@ class TestCourseGradeFactory(GradeTestBase):
...
@@ -158,6 +187,11 @@ class TestCourseGradeFactory(GradeTestBase):
'category'
:
'Homework'
,
'category'
:
'Homework'
,
'percent'
:
0.25
,
'percent'
:
0.25
,
'detail'
:
'Homework = 25.00
%
of a possible 100.00
%
'
,
'detail'
:
'Homework = 25.00
%
of a possible 100.00
%
'
,
},
'NoCredit'
:
{
'category'
:
'NoCredit'
,
'percent'
:
0.0
,
'detail'
:
'NoCredit = 0.00
%
of a possible 0.00
%
'
,
}
}
},
},
'percent'
:
0.25
,
'percent'
:
0.25
,
...
@@ -181,6 +215,13 @@ class TestCourseGradeFactory(GradeTestBase):
...
@@ -181,6 +215,13 @@ class TestCourseGradeFactory(GradeTestBase):
'percent'
:
0.25
,
'percent'
:
0.25
,
'prominent'
:
True
'prominent'
:
True
},
},
{
'category'
:
'NoCredit'
,
'detail'
:
u'NoCredit Average = 0
%
'
,
'label'
:
u'NC Avg'
,
'percent'
:
0
,
'prominent'
:
True
},
]
]
}
}
self
.
assertEqual
(
expected_summary
,
actual_summary
)
self
.
assertEqual
(
expected_summary
,
actual_summary
)
...
...
lms/djangoapps/grades/tests/test_subsection_grade.py
View file @
b5435e6f
...
@@ -15,6 +15,7 @@ class SubsectionGradeTest(GradeTestBase):
...
@@ -15,6 +15,7 @@ class SubsectionGradeTest(GradeTestBase):
self
.
subsection_grade_factory
.
_csm_scores
,
self
.
subsection_grade_factory
.
_csm_scores
,
)
)
self
.
assertEqual
(
PersistentSubsectionGrade
.
objects
.
count
(),
0
)
self
.
assertEqual
(
PersistentSubsectionGrade
.
objects
.
count
(),
0
)
self
.
assertEqual
(
created_grade
.
percent_graded
,
0.5
)
# save to db, and verify object is in database
# save to db, and verify object is in database
created_grade
.
update_or_create_model
(
self
.
request
.
user
)
created_grade
.
update_or_create_model
(
self
.
request
.
user
)
...
@@ -28,11 +29,10 @@ class SubsectionGradeTest(GradeTestBase):
...
@@ -28,11 +29,10 @@ class SubsectionGradeTest(GradeTestBase):
read_grade
=
ReadSubsectionGrade
(
read_grade
=
ReadSubsectionGrade
(
self
.
sequence
,
self
.
sequence
,
saved_model
,
saved_model
,
self
.
course_structure
,
self
.
subsection_grade_factory
self
.
subsection_grade_factory
.
_submissions_scores
,
self
.
subsection_grade_factory
.
_csm_scores
,
)
)
self
.
assertEqual
(
created_grade
.
url_name
,
read_grade
.
url_name
)
self
.
assertEqual
(
created_grade
.
url_name
,
read_grade
.
url_name
)
read_grade
.
all_total
.
first_attempted
=
created_grade
.
all_total
.
first_attempted
=
None
read_grade
.
all_total
.
first_attempted
=
created_grade
.
all_total
.
first_attempted
=
None
self
.
assertEqual
(
created_grade
.
all_total
,
read_grade
.
all_total
)
self
.
assertEqual
(
created_grade
.
all_total
,
read_grade
.
all_total
)
self
.
assertEqual
(
created_grade
.
percent_graded
,
0.5
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
b5435e6f
...
@@ -164,10 +164,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
...
@@ -164,10 +164,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
False
),
)
)
@ddt.unpack
@ddt.unpack
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
...
@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
...
@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
),
)
)
@ddt.unpack
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
...
@@ -240,8 +240,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
...
@@ -240,8 +240,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEqual
(
len
(
PersistentSubsectionGrade
.
bulk_read_grades
(
self
.
user
.
id
,
self
.
course
.
id
)),
0
)
self
.
assertEqual
(
len
(
PersistentSubsectionGrade
.
bulk_read_grades
(
self
.
user
.
id
,
self
.
course
.
id
)),
0
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
6
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
6
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
4
),
)
)
@ddt.unpack
@ddt.unpack
def
test_persistent_grades_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
def
test_persistent_grades_enabled_on_course
(
self
,
default_store
,
num_mongo_queries
,
num_sql_queries
):
...
...
lms/djangoapps/instructor_analytics/basic.py
View file @
b5435e6f
...
@@ -518,7 +518,7 @@ def dump_grading_context(course):
...
@@ -518,7 +518,7 @@ def dump_grading_context(course):
msg
+=
hbar
msg
+=
hbar
msg
+=
"Listing grading context for course
%
s
\n
"
%
course
.
id
.
to_deprecated_string
()
msg
+=
"Listing grading context for course
%
s
\n
"
%
course
.
id
.
to_deprecated_string
()
gcontext
=
grading_context_for_course
(
course
.
id
)
gcontext
=
grading_context_for_course
(
course
)
msg
+=
"graded sections:
\n
"
msg
+=
"graded sections:
\n
"
msg
+=
'
%
s
\n
'
%
gcontext
[
'all_graded_subsections_by_type'
]
.
keys
()
msg
+=
'
%
s
\n
'
%
gcontext
[
'all_graded_subsections_by_type'
]
.
keys
()
...
@@ -541,6 +541,6 @@ def dump_grading_context(course):
...
@@ -541,6 +541,6 @@ def dump_grading_context(course):
msg
+=
"
%
s (format=
%
s, Assignment=
%
s
%
s)
\n
"
\
msg
+=
"
%
s (format=
%
s, Assignment=
%
s
%
s)
\n
"
\
%
(
sdesc
.
display_name
,
frmat
,
aname
,
notes
)
%
(
sdesc
.
display_name
,
frmat
,
aname
,
notes
)
msg
+=
"all graded blocks:
\n
"
msg
+=
"all graded blocks:
\n
"
msg
+=
"length=
%
d
\n
"
%
len
(
gcontext
[
'all_graded_blocks'
])
msg
+=
"length=
%
d
\n
"
%
gcontext
[
'count_all_graded_blocks'
]
msg
=
'<pre>
%
s</pre>'
%
msg
.
replace
(
'<'
,
'<'
)
msg
=
'<pre>
%
s</pre>'
%
msg
.
replace
(
'<'
,
'<'
)
return
msg
return
msg
lms/djangoapps/instructor_task/tasks_helper/grades.py
View file @
b5435e6f
...
@@ -103,7 +103,7 @@ class _CourseGradeReportContext(object):
...
@@ -103,7 +103,7 @@ class _CourseGradeReportContext(object):
Returns an OrderedDict that maps an assignment type to a dict of
Returns an OrderedDict that maps an assignment type to a dict of
subsection-headers and average-header.
subsection-headers and average-header.
"""
"""
grading_cxt
=
grading_context
(
self
.
course_structure
)
grading_cxt
=
grading_context
(
self
.
course
,
self
.
course
_structure
)
graded_assignments_map
=
OrderedDict
()
graded_assignments_map
=
OrderedDict
()
for
assignment_type_name
,
subsection_infos
in
grading_cxt
[
'all_graded_subsections_by_type'
]
.
iteritems
():
for
assignment_type_name
,
subsection_infos
in
grading_cxt
[
'all_graded_subsections_by_type'
]
.
iteritems
():
graded_subsections_map
=
OrderedDict
()
graded_subsections_map
=
OrderedDict
()
...
@@ -127,7 +127,8 @@ class _CourseGradeReportContext(object):
...
@@ -127,7 +127,8 @@ class _CourseGradeReportContext(object):
graded_assignments_map
[
assignment_type_name
]
=
{
graded_assignments_map
[
assignment_type_name
]
=
{
'subsection_headers'
:
graded_subsections_map
,
'subsection_headers'
:
graded_subsections_map
,
'average_header'
:
average_header
,
'average_header'
:
average_header
,
'separate_subsection_avg_headers'
:
separate_subsection_avg_headers
'separate_subsection_avg_headers'
:
separate_subsection_avg_headers
,
'grader'
:
grading_cxt
[
'subsection_type_graders'
]
.
get
(
assignment_type_name
),
}
}
return
graded_assignments_map
return
graded_assignments_map
...
@@ -297,29 +298,56 @@ class CourseGradeReport(object):
...
@@ -297,29 +298,56 @@ class CourseGradeReport(object):
users
=
users
.
select_related
(
'profile__allow_certificate'
)
users
=
users
.
select_related
(
'profile__allow_certificate'
)
return
grouper
(
users
)
return
grouper
(
users
)
def
_user_grade
_result
s
(
self
,
course_grade
,
context
):
def
_user_grades
(
self
,
course_grade
,
context
):
"""
"""
Returns a list of grade results for the given course_grade corresponding
Returns a list of grade results for the given course_grade corresponding
to the headers for this report.
to the headers for this report.
"""
"""
grade_results
=
[]
grade_results
=
[]
for
assignment_type
,
assignment_info
in
context
.
graded_assignments
.
iteritems
():
for
assignment_type
,
assignment_info
in
context
.
graded_assignments
.
iteritems
():
for
subsection_location
in
assignment_info
[
'subsection_headers'
]:
try
:
subsection_grades
,
subsection_grades_results
=
self
.
_user_subsection_grades
(
subsection_grade
=
course_grade
.
subsection_grade
(
subsection_location
)
course_grade
,
except
KeyError
:
assignment_info
[
'subsection_headers'
],
grade_result
=
u'Not Available'
)
else
:
grade_results
.
extend
(
subsection_grades_results
)
if
subsection_grade
.
attempted_graded
:
grade_result
=
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
assignment_average
=
self
.
_user_assignment_average
(
course_grade
,
subsection_grades
,
assignment_info
)
else
:
if
assignment_average
is
not
None
:
grade_result
=
u'Not Attempted'
grade_results
.
append
([
grade_result
])
if
assignment_info
[
'separate_subsection_avg_headers'
]:
assignment_average
=
course_grade
.
assignment_average
(
assignment_type
)
grade_results
.
append
([
assignment_average
])
grade_results
.
append
([
assignment_average
])
return
[
course_grade
.
percent
]
+
_flatten
(
grade_results
)
return
[
course_grade
.
percent
]
+
_flatten
(
grade_results
)
def
_user_subsection_grades
(
self
,
course_grade
,
subsection_headers
):
"""
Returns a list of grade results for the given course_grade corresponding
to the headers for this report.
"""
subsection_grades
=
[]
grade_results
=
[]
for
subsection_location
in
subsection_headers
:
subsection_grade
=
course_grade
.
subsection_grade
(
subsection_location
)
if
subsection_grade
.
attempted_graded
:
grade_result
=
subsection_grade
.
percent_graded
else
:
grade_result
=
u'Not Attempted'
grade_results
.
append
([
grade_result
])
subsection_grades
.
append
(
subsection_grade
)
return
subsection_grades
,
grade_results
def
_user_assignment_average
(
self
,
course_grade
,
subsection_grades
,
assignment_info
):
if
assignment_info
[
'separate_subsection_avg_headers'
]:
if
assignment_info
[
'grader'
]:
if
course_grade
.
attempted
:
subsection_breakdown
=
[
{
'percent'
:
subsection_grade
.
percent_graded
}
for
subsection_grade
in
subsection_grades
]
assignment_average
,
_
=
assignment_info
[
'grader'
]
.
total_with_drops
(
subsection_breakdown
)
else
:
assignment_average
=
0.0
return
assignment_average
def
_user_cohort_group_names
(
self
,
user
,
context
):
def
_user_cohort_group_names
(
self
,
user
,
context
):
"""
"""
Returns a list of names of cohort groups in which the given user
Returns a list of names of cohort groups in which the given user
...
@@ -411,7 +439,7 @@ class CourseGradeReport(object):
...
@@ -411,7 +439,7 @@ class CourseGradeReport(object):
else
:
else
:
success_rows
.
append
(
success_rows
.
append
(
[
user
.
id
,
user
.
email
,
user
.
username
]
+
[
user
.
id
,
user
.
email
,
user
.
username
]
+
self
.
_user_grade
_result
s
(
course_grade
,
context
)
+
self
.
_user_grades
(
course_grade
,
context
)
+
self
.
_user_cohort_group_names
(
user
,
context
)
+
self
.
_user_cohort_group_names
(
user
,
context
)
+
self
.
_user_experiment_group_names
(
user
,
context
)
+
self
.
_user_experiment_group_names
(
user
,
context
)
+
self
.
_user_team_names
(
user
,
bulk_context
.
teams
)
+
self
.
_user_team_names
(
user
,
bulk_context
.
teams
)
+
...
@@ -440,7 +468,8 @@ class ProblemGradeReport(object):
...
@@ -440,7 +468,8 @@ class ProblemGradeReport(object):
# as the keys. It is structured in this way to keep the values related.
# as the keys. It is structured in this way to keep the values related.
header_row
=
OrderedDict
([(
'id'
,
'Student ID'
),
(
'email'
,
'Email'
),
(
'username'
,
'Username'
)])
header_row
=
OrderedDict
([(
'id'
,
'Student ID'
),
(
'email'
,
'Email'
),
(
'username'
,
'Username'
)])
graded_scorable_blocks
=
cls
.
_graded_scorable_blocks_to_header
(
course_id
)
course
=
get_course_by_id
(
course_id
)
graded_scorable_blocks
=
cls
.
_graded_scorable_blocks_to_header
(
course
)
# Just generate the static fields for now.
# Just generate the static fields for now.
rows
=
[
list
(
header_row
.
values
())
+
[
'Enrollment Status'
,
'Grade'
]
+
_flatten
(
graded_scorable_blocks
.
values
())]
rows
=
[
list
(
header_row
.
values
())
+
[
'Enrollment Status'
,
'Grade'
]
+
_flatten
(
graded_scorable_blocks
.
values
())]
...
@@ -451,7 +480,6 @@ class ProblemGradeReport(object):
...
@@ -451,7 +480,6 @@ class ProblemGradeReport(object):
# whether each user is currently enrolled in the course.
# whether each user is currently enrolled in the course.
CourseEnrollment
.
bulk_fetch_enrollment_states
(
enrolled_students
,
course_id
)
CourseEnrollment
.
bulk_fetch_enrollment_states
(
enrolled_students
,
course_id
)
course
=
get_course_by_id
(
course_id
)
for
student
,
course_grade
,
error
in
CourseGradeFactory
()
.
iter
(
enrolled_students
,
course
):
for
student
,
course_grade
,
error
in
CourseGradeFactory
()
.
iter
(
enrolled_students
,
course
):
student_fields
=
[
getattr
(
student
,
field_name
)
for
field_name
in
header_row
]
student_fields
=
[
getattr
(
student
,
field_name
)
for
field_name
in
header_row
]
task_progress
.
attempted
+=
1
task_progress
.
attempted
+=
1
...
@@ -495,13 +523,13 @@ class ProblemGradeReport(object):
...
@@ -495,13 +523,13 @@ class ProblemGradeReport(object):
return
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
'Uploading CSV'
})
return
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
'Uploading CSV'
})
@classmethod
@classmethod
def
_graded_scorable_blocks_to_header
(
cls
,
course
_key
):
def
_graded_scorable_blocks_to_header
(
cls
,
course
):
"""
"""
Returns an OrderedDict that maps a scorable block's id to its
Returns an OrderedDict that maps a scorable block's id to its
headers in the final report.
headers in the final report.
"""
"""
scorable_blocks_map
=
OrderedDict
()
scorable_blocks_map
=
OrderedDict
()
grading_context
=
grading_context_for_course
(
course
_key
)
grading_context
=
grading_context_for_course
(
course
)
for
assignment_type_name
,
subsection_infos
in
grading_context
[
'all_graded_subsections_by_type'
]
.
iteritems
():
for
assignment_type_name
,
subsection_infos
in
grading_context
[
'all_graded_subsections_by_type'
]
.
iteritems
():
for
subsection_index
,
subsection_info
in
enumerate
(
subsection_infos
,
start
=
1
):
for
subsection_index
,
subsection_info
in
enumerate
(
subsection_infos
,
start
=
1
):
for
scorable_block
in
subsection_info
[
'scored_descendants'
]:
for
scorable_block
in
subsection_info
[
'scored_descendants'
]:
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
b5435e6f
...
@@ -1707,6 +1707,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
...
@@ -1707,6 +1707,7 @@ class TestCohortStudents(TestReportMixin, InstructorTaskCourseTestCase):
)
)
@ddt.ddt
@patch
(
'lms.djangoapps.instructor_task.tasks_helper.misc.DefaultStorage'
,
new
=
MockDefaultStorage
)
@patch
(
'lms.djangoapps.instructor_task.tasks_helper.misc.DefaultStorage'
,
new
=
MockDefaultStorage
)
class
TestGradeReport
(
TestReportMixin
,
InstructorTaskModuleTestCase
):
class
TestGradeReport
(
TestReportMixin
,
InstructorTaskModuleTestCase
):
"""
"""
...
@@ -1782,7 +1783,7 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
...
@@ -1782,7 +1783,7 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
u'Username'
:
self
.
student
.
username
,
u'Username'
:
self
.
student
.
username
,
u'Grade'
:
'0.13'
,
u'Grade'
:
'0.13'
,
u'Homework 1: Subsection'
:
'0.5'
,
u'Homework 1: Subsection'
:
'0.5'
,
u'Homework 2: Hidden'
:
u'Not A
vailable
'
,
u'Homework 2: Hidden'
:
u'Not A
ttempted
'
,
u'Homework 3: Unattempted'
:
u'Not Attempted'
,
u'Homework 3: Unattempted'
:
u'Not Attempted'
,
u'Homework 4: Empty'
:
u'Not Attempted'
,
u'Homework 4: Empty'
:
u'Not Attempted'
,
u'Homework (Avg)'
:
'0.125'
,
u'Homework (Avg)'
:
'0.125'
,
...
@@ -1791,22 +1792,16 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
...
@@ -1791,22 +1792,16 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
ignore_other_columns
=
True
,
ignore_other_columns
=
True
,
)
)
def
test_fast_generation_zero_grade
(
self
):
@ddt.data
(
True
,
False
)
def
test_fast_generation
(
self
,
create_non_zero_grade
):
if
create_non_zero_grade
:
self
.
submit_student_answer
(
self
.
student
.
username
,
u'Problem1'
,
[
'Option 1'
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
with
patch
(
'lms.djangoapps.grades.course_
grade.CourseGradeBase._prep_course_for_grading'
)
as
mock_grader
:
with
patch
(
'lms.djangoapps.grades.course_
data.get_course_blocks'
)
as
mock_course_blocks
:
with
patch
(
'lms.djangoapps.grades.subsection_grade.get_score'
)
as
mock_get_score
:
with
patch
(
'lms.djangoapps.grades.subsection_grade.get_score'
)
as
mock_get_score
:
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertFalse
(
mock_grader
.
called
)
self
.
assertFalse
(
mock_get_score
.
called
)
self
.
assertFalse
(
mock_get_score
.
called
)
self
.
assertFalse
(
mock_course_blocks
.
called
)
def
test_slow_generation_nonzero_grade
(
self
):
self
.
submit_student_answer
(
self
.
student
.
username
,
u'Problem1'
,
[
'Option 1'
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
with
patch
(
'lms.djangoapps.grades.course_grade.CourseGradeBase._prep_course_for_grading'
)
as
mock_grader
:
with
patch
(
'lms.djangoapps.grades.subsection_grade.get_score'
)
as
mock_get_score
:
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertTrue
(
mock_grader
.
called
)
self
.
assertTrue
(
mock_get_score
.
called
)
@ddt.ddt
@ddt.ddt
...
...
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