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):
weight
=
subgraderconf
.
pop
(
"weight"
,
0
)
try
:
if
'min_count'
in
subgraderconf
:
#This is an AssignmentFormatGrader
subgrader_class
=
AssignmentFormatGrader
else
:
raise
ValueError
(
"Configuration has no appropriate grader class."
)
...
...
@@ -344,28 +343,28 @@ class AssignmentFormatGrader(CourseGrader):
self
.
starting_index
=
starting_index
self
.
hide_average
=
hide_average
def
grade
(
self
,
grade_sheet
,
generate_random_scores
=
False
):
def
total_with_drops
(
breakdown
,
drop_count
):
"""
Calculates total score for a section while dropping lowest scores
"""
# Create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown
=
sorted
(
enumerate
(
breakdown
),
key
=
lambda
x
:
-
x
[
1
][
'percent'
])
# A list of the indices of the dropped scores
dropped_indices
=
[]
if
drop_count
>
0
:
dropped_indices
=
[
x
[
0
]
for
x
in
sorted_breakdown
[
-
drop_count
:]]
aggregate_score
=
0
for
index
,
mark
in
enumerate
(
breakdown
):
if
index
not
in
dropped_indices
:
aggregate_score
+=
mark
[
'percent'
]
if
len
(
breakdown
)
-
drop_count
>
0
:
aggregate_score
/=
len
(
breakdown
)
-
drop_count
return
aggregate_score
,
dropped_indices
def
total_with_drops
(
self
,
breakdown
):
"""
Calculates total score for a section while dropping lowest scores
"""
# Create an array of tuples with (index, mark), sorted by mark['percent'] descending
sorted_breakdown
=
sorted
(
enumerate
(
breakdown
),
key
=
lambda
x
:
-
x
[
1
][
'percent'
])
# A list of the indices of the dropped scores
dropped_indices
=
[]
if
self
.
drop_count
>
0
:
dropped_indices
=
[
x
[
0
]
for
x
in
sorted_breakdown
[
-
self
.
drop_count
:]]
aggregate_score
=
0
for
index
,
mark
in
enumerate
(
breakdown
):
if
index
not
in
dropped_indices
:
aggregate_score
+=
mark
[
'percent'
]
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
()
breakdown
=
[]
for
i
in
range
(
max
(
self
.
min_count
,
len
(
scores
))):
...
...
@@ -405,7 +404,7 @@ class AssignmentFormatGrader(CourseGrader):
breakdown
.
append
({
'percent'
:
percentage
,
'label'
:
short_label
,
'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
:
breakdown
[
dropped_index
][
'mark'
]
=
{
...
...
lms/djangoapps/grades/context.py
View file @
b5435e6f
...
...
@@ -5,18 +5,19 @@ from collections import OrderedDict
from
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
.course_grade
import
CourseGrade
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.
"""
course_structure
=
get_course_in_cache
(
course
_key
)
return
grading_context
(
course_structure
)
course_structure
=
get_course_in_cache
(
course
.
id
)
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
a student.
...
...
@@ -36,7 +37,7 @@ def grading_context(course_structure):
the descriptor tree again.
"""
all_graded_blocks
=
[]
count_all_graded_blocks
=
0
all_graded_subsections_by_type
=
OrderedDict
()
for
chapter_key
in
course_structure
.
get_children
(
course_structure
.
root_block_usage_key
):
...
...
@@ -64,9 +65,10 @@ def grading_context(course_structure):
if
subsection_format
not
in
all_graded_subsections_by_type
:
all_graded_subsections_by_type
[
subsection_format
]
=
[]
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
{
'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):
if
self
.
_course
:
self
.
_course_key
=
self
.
_course
.
id
else
:
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
self
.
_course_key
=
structure
.
root_block_usage_key
.
course_key
return
self
.
_course_key
@property
def
location
(
self
):
if
not
self
.
_location
:
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
if
structure
:
self
.
_location
=
structure
.
root_block_usage_key
elif
self
.
_course
:
...
...
@@ -72,7 +72,7 @@ class CourseData(object):
@property
def
grading_policy_hash
(
self
):
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
if
structure
:
return
structure
.
get_transformer_block_field
(
structure
.
root_block_usage_key
,
...
...
@@ -84,14 +84,14 @@ class CourseData(object):
@property
def
version
(
self
):
structure
=
self
.
_
effective_structure
structure
=
self
.
effective_structure
course_block
=
structure
[
self
.
location
]
if
structure
else
self
.
course
return
getattr
(
course_block
,
'course_version'
,
None
)
@property
def
edited_on
(
self
):
# 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
:
course_block
=
structure
[
self
.
location
]
return
getattr
(
course_block
,
'subtree_edited_on'
,
None
)
...
...
@@ -100,7 +100,7 @@ class CourseData(object):
return
u'Course: course_key: {}'
.
format
(
self
.
course_key
)
def
full_string
(
self
):
if
self
.
_
effective_structure
:
if
self
.
effective_structure
:
return
u'Course: course_key: {}, version: {}, edited_on: {}, grading_policy: {}'
.
format
(
self
.
course_key
,
self
.
version
,
self
.
edited_on
,
self
.
grading_policy_hash
,
)
...
...
@@ -108,5 +108,5 @@ class CourseData(object):
return
u'Course: course_key: {}, empty course structure'
.
format
(
self
.
course_key
)
@property
def
_
effective_structure
(
self
):
def
effective_structure
(
self
):
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
from
ccx_keys.locator
import
CCXLocator
from
xmodule
import
block_metadata_utils
from
.config
import
assume_zero_if_absent
from
.subsection_grade
import
ZeroSubsectionGrade
from
.subsection_grade_factory
import
SubsectionGradeFactory
...
...
@@ -46,17 +47,14 @@ class CourseGradeBase(object):
def
subsection_grade
(
self
,
subsection_key
):
"""
Returns the subsection grade for 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
])
Returns the subsection grade for the given subsection usage key.
@abstractmethod
def
assignment_average
(
self
,
assignment_type
):
"""
Returns the average of all assignments of the given assignment type
.
Note: does NOT check whether the user has access to the subsection.
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
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
def
graded_subsections_by_format
(
self
):
...
...
@@ -148,7 +146,7 @@ class CourseGradeBase(object):
"""
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
(
self
.
graded_subsections_by_format
,
generate_random_scores
=
settings
.
GENERATE_PROFILE_SCORES
,
...
...
@@ -166,7 +164,21 @@ class CourseGradeBase(object):
grade_summary
[
'grade'
]
=
self
.
letter_grade
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.
This is most relevant for CCX courses.
...
...
@@ -176,8 +188,7 @@ class CourseGradeBase(object):
this will no longer be needed - since BlockStructure correctly
retrieves/uses all field overrides.
"""
course
=
self
.
course_data
.
course
if
isinstance
(
self
.
course_data
.
course_key
,
CCXLocator
):
if
isinstance
(
course
.
id
,
CCXLocator
):
# clean out any field values that may have been set from the
# parent course of the CCX course.
course
.
_field_data_cache
=
{}
# pylint: disable=protected-access
...
...
@@ -221,9 +232,6 @@ class ZeroCourseGrade(CourseGradeBase):
Course Grade class for Zero-value grades when no problems were
attempted in the course.
"""
def
assignment_average
(
self
,
assignment_type
):
return
0.0
def
_get_subsection_grade
(
self
,
subsection
):
return
ZeroSubsectionGrade
(
subsection
,
self
.
course_data
)
...
...
@@ -259,15 +267,15 @@ class CourseGrade(CourseGradeBase):
Returns whether any of the subsections in this course
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
subsection_grade
in
chapter
[
'sections'
]:
if
subsection_grade
.
all_total
.
first_attempted
:
return
True
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
):
if
self
.
force_update_subsections
:
return
self
.
_subsection_grade_factory
.
update
(
subsection
)
...
...
lms/djangoapps/grades/course_grade_factory.py
View file @
b5435e6f
...
...
@@ -85,7 +85,7 @@ class CourseGradeFactory(object):
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.
"""
# 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
# compute the grade for all students.
# 2. Optimization: the collected course_structure is not
...
...
lms/djangoapps/grades/subsection_grade.py
View file @
b5435e6f
...
...
@@ -63,6 +63,13 @@ class SubsectionGradeBase(object):
"""
raise
NotImplementedError
@property
def
percent_graded
(
self
):
"""
Returns the percent score of the graded problems in this subsection.
"""
raise
NotImplementedError
class
ZeroSubsectionGrade
(
SubsectionGradeBase
):
"""
...
...
@@ -78,6 +85,10 @@ class ZeroSubsectionGrade(SubsectionGradeBase):
return
False
@property
def
percent_graded
(
self
):
return
0.0
@property
def
all_total
(
self
):
return
self
.
_aggregate_scores
[
0
]
...
...
@@ -128,6 +139,10 @@ class NonZeroSubsectionGrade(SubsectionGradeBase):
def
attempted_graded
(
self
):
return
self
.
graded_total
.
first_attempted
is
not
None
@property
def
percent_graded
(
self
):
return
self
.
graded_total
.
earned
/
self
.
graded_total
.
possible
@staticmethod
def
_compute_block_score
(
block_key
,
...
...
@@ -157,7 +172,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
"""
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
(
tw_earned
=
model
.
earned_all
,
tw_possible
=
model
.
possible_all
,
...
...
@@ -174,9 +189,7 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
# save these for later since we compute problem_scores lazily
self
.
model
=
model
self
.
course_structure
=
course_structure
self
.
submissions_scores
=
submissions_scores
self
.
csm_scores
=
csm_scores
self
.
factory
=
factory
super
(
ReadSubsectionGrade
,
self
)
.
__init__
(
subsection
,
all_total
,
graded_total
,
override
)
...
...
@@ -185,7 +198,11 @@ class ReadSubsectionGrade(NonZeroSubsectionGrade):
problem_scores
=
OrderedDict
()
for
block
in
self
.
model
.
visible_blocks
.
blocks
:
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
:
problem_scores
[
block
.
locator
]
=
problem_score
...
...
lms/djangoapps/grades/subsection_grade_factory.py
View file @
b5435e6f
...
...
@@ -80,9 +80,7 @@ class SubsectionGradeFactory(object):
except
PersistentSubsectionGrade
.
DoesNotExist
:
pass
else
:
orig_subsection_grade
=
ReadSubsectionGrade
(
subsection
,
grade_model
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
orig_subsection_grade
=
ReadSubsectionGrade
(
subsection
,
grade_model
,
self
)
if
not
is_score_higher_or_equal
(
orig_subsection_grade
.
graded_total
.
earned
,
orig_subsection_grade
.
graded_total
.
possible
,
...
...
@@ -125,9 +123,7 @@ class SubsectionGradeFactory(object):
saved_subsection_grades
=
self
.
_get_bulk_cached_subsection_grades
()
grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
if
grade
:
return
ReadSubsectionGrade
(
subsection
,
grade
,
self
.
course_data
.
structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
return
ReadSubsectionGrade
(
subsection
,
grade
,
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
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
..course_data
import
CourseData
from
..subsection_grade_factory
import
SubsectionGradeFactory
...
...
@@ -75,6 +76,7 @@ class GradeTestBase(SharedModuleStoreTestCase):
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
.
course_data
=
CourseData
(
self
.
request
.
user
,
structure
=
self
.
course_structure
)
self
.
subsection_grade_factory
=
SubsectionGradeFactory
(
self
.
request
.
user
,
self
.
course
,
self
.
course_structure
)
CourseEnrollment
.
enroll
(
self
.
request
.
user
,
self
.
course
.
id
)
...
...
@@ -91,6 +93,13 @@ class GradeTestBase(SharedModuleStoreTestCase):
"short_label"
:
"HW"
,
"weight"
:
1.0
,
},
{
"type"
:
"NoCredit"
,
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
"NC"
,
"weight"
:
0.0
,
},
],
"GRADE_CUTOFFS"
:
{
"Pass"
:
passing
,
...
...
lms/djangoapps/grades/tests/test_course_grade_factory.py
View file @
b5435e6f
...
...
@@ -97,19 +97,19 @@ class TestCourseGradeFactory(GradeTestBase):
with
self
.
assertNumQueries
(
29
),
mock_get_score
(
1
,
2
):
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
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
)
with
self
.
assertNumQueries
(
4
):
with
self
.
assertNumQueries
(
2
):
_assert_read
(
expected_pass
=
True
,
expected_percent
=
0.5
)
# NOT updated to grade of .25
with
self
.
assertNumQueries
(
12
),
mock_get_score
(
2
,
2
):
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
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
...
...
@@ -124,6 +124,35 @@ class TestCourseGradeFactory(GradeTestBase):
else
:
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
):
subsection
=
self
.
course_structure
[
self
.
sequence
.
location
]
with
mock_get_score
(
1
,
2
):
...
...
@@ -158,6 +187,11 @@ class TestCourseGradeFactory(GradeTestBase):
'category'
:
'Homework'
,
'percent'
:
0.25
,
'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
,
...
...
@@ -181,6 +215,13 @@ class TestCourseGradeFactory(GradeTestBase):
'percent'
:
0.25
,
'prominent'
:
True
},
{
'category'
:
'NoCredit'
,
'detail'
:
u'NoCredit Average = 0
%
'
,
'label'
:
u'NC Avg'
,
'percent'
:
0
,
'prominent'
:
True
},
]
}
self
.
assertEqual
(
expected_summary
,
actual_summary
)
...
...
lms/djangoapps/grades/tests/test_subsection_grade.py
View file @
b5435e6f
...
...
@@ -15,6 +15,7 @@ class SubsectionGradeTest(GradeTestBase):
self
.
subsection_grade_factory
.
_csm_scores
,
)
self
.
assertEqual
(
PersistentSubsectionGrade
.
objects
.
count
(),
0
)
self
.
assertEqual
(
created_grade
.
percent_graded
,
0.5
)
# save to db, and verify object is in database
created_grade
.
update_or_create_model
(
self
.
request
.
user
)
...
...
@@ -28,11 +29,10 @@ class SubsectionGradeTest(GradeTestBase):
read_grade
=
ReadSubsectionGrade
(
self
.
sequence
,
saved_model
,
self
.
course_structure
,
self
.
subsection_grade_factory
.
_submissions_scores
,
self
.
subsection_grade_factory
.
_csm_scores
,
self
.
subsection_grade_factory
)
self
.
assertEqual
(
created_grade
.
url_name
,
read_grade
.
url_name
)
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
.
percent_graded
,
0.5
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
b5435e6f
...
...
@@ -164,10 +164,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
,
False
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
,
False
),
)
@ddt.unpack
def
test_query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
,
create_multiple_subsections
):
...
...
@@ -179,8 +179,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
5
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
3
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
3
),
)
@ddt.unpack
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
self
.
assertEqual
(
len
(
PersistentSubsectionGrade
.
bulk_read_grades
(
self
.
user
.
id
,
self
.
course
.
id
)),
0
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
6
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
6
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
2
4
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
2
4
),
)
@ddt.unpack
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):
msg
+=
hbar
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
+=
'
%
s
\n
'
%
gcontext
[
'all_graded_subsections_by_type'
]
.
keys
()
...
...
@@ -541,6 +541,6 @@ def dump_grading_context(course):
msg
+=
"
%
s (format=
%
s, Assignment=
%
s
%
s)
\n
"
\
%
(
sdesc
.
display_name
,
frmat
,
aname
,
notes
)
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
(
'<'
,
'<'
)
return
msg
lms/djangoapps/instructor_task/tasks_helper/grades.py
View file @
b5435e6f
...
...
@@ -103,7 +103,7 @@ class _CourseGradeReportContext(object):
Returns an OrderedDict that maps an assignment type to a dict of
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
()
for
assignment_type_name
,
subsection_infos
in
grading_cxt
[
'all_graded_subsections_by_type'
]
.
iteritems
():
graded_subsections_map
=
OrderedDict
()
...
...
@@ -127,7 +127,8 @@ class _CourseGradeReportContext(object):
graded_assignments_map
[
assignment_type_name
]
=
{
'subsection_headers'
:
graded_subsections_map
,
'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
...
...
@@ -297,29 +298,56 @@ class CourseGradeReport(object):
users
=
users
.
select_related
(
'profile__allow_certificate'
)
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
to the headers for this report.
"""
grade_results
=
[]
for
assignment_type
,
assignment_info
in
context
.
graded_assignments
.
iteritems
():
for
subsection_location
in
assignment_info
[
'subsection_headers'
]:
try
:
subsection_grade
=
course_grade
.
subsection_grade
(
subsection_location
)
except
KeyError
:
grade_result
=
u'Not Available'
else
:
if
subsection_grade
.
attempted_graded
:
grade_result
=
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
else
:
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
)
subsection_grades
,
subsection_grades_results
=
self
.
_user_subsection_grades
(
course_grade
,
assignment_info
[
'subsection_headers'
],
)
grade_results
.
extend
(
subsection_grades_results
)
assignment_average
=
self
.
_user_assignment_average
(
course_grade
,
subsection_grades
,
assignment_info
)
if
assignment_average
is
not
None
:
grade_results
.
append
([
assignment_average
])
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
):
"""
Returns a list of names of cohort groups in which the given user
...
...
@@ -411,7 +439,7 @@ class CourseGradeReport(object):
else
:
success_rows
.
append
(
[
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_experiment_group_names
(
user
,
context
)
+
self
.
_user_team_names
(
user
,
bulk_context
.
teams
)
+
...
...
@@ -440,7 +468,8 @@ class ProblemGradeReport(object):
# as the keys. It is structured in this way to keep the values related.
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.
rows
=
[
list
(
header_row
.
values
())
+
[
'Enrollment Status'
,
'Grade'
]
+
_flatten
(
graded_scorable_blocks
.
values
())]
...
...
@@ -451,7 +480,6 @@ class ProblemGradeReport(object):
# whether each user is currently enrolled in the course.
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
):
student_fields
=
[
getattr
(
student
,
field_name
)
for
field_name
in
header_row
]
task_progress
.
attempted
+=
1
...
...
@@ -495,13 +523,13 @@ class ProblemGradeReport(object):
return
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
'Uploading CSV'
})
@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
headers in the final report.
"""
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
subsection_index
,
subsection_info
in
enumerate
(
subsection_infos
,
start
=
1
):
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):
)
@ddt.ddt
@patch
(
'lms.djangoapps.instructor_task.tasks_helper.misc.DefaultStorage'
,
new
=
MockDefaultStorage
)
class
TestGradeReport
(
TestReportMixin
,
InstructorTaskModuleTestCase
):
"""
...
...
@@ -1782,7 +1783,7 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
u'Username'
:
self
.
student
.
username
,
u'Grade'
:
'0.13'
,
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 4: Empty'
:
u'Not Attempted'
,
u'Homework (Avg)'
:
'0.125'
,
...
...
@@ -1791,22 +1792,16 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
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.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
:
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertFalse
(
mock_grader
.
called
)
self
.
assertFalse
(
mock_get_score
.
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
)
self
.
assertFalse
(
mock_course_blocks
.
called
)
@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