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
379a2c18
Commit
379a2c18
authored
Apr 26, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor Grade Report in prep for parallelization.
parent
cc98111d
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
466 additions
and
369 deletions
+466
-369
lms/djangoapps/ccx/views.py
+1
-1
lms/djangoapps/grades/new/course_data.py
+7
-0
lms/djangoapps/grades/new/course_grade_factory.py
+21
-12
lms/djangoapps/grades/tasks.py
+1
-1
lms/djangoapps/grades/tests/test_grades.py
+2
-2
lms/djangoapps/instructor_task/tasks.py
+6
-6
lms/djangoapps/instructor_task/tasks_helper/grades.py
+407
-325
lms/djangoapps/instructor_task/tests/test_integration.py
+5
-5
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+16
-17
No files found.
lms/djangoapps/ccx/views.py
View file @
379a2c18
...
...
@@ -564,7 +564,7 @@ def ccx_grades_csv(request, course, ccx=None):
courseenrollment__course_id
=
ccx_key
,
courseenrollment__is_active
=
1
)
.
order_by
(
'username'
)
.
select_related
(
"profile"
)
grades
=
CourseGradeFactory
()
.
iter
(
course
,
enrolled_students
)
grades
=
CourseGradeFactory
()
.
iter
(
enrolled_students
,
course
)
header
=
None
rows
=
[]
...
...
lms/djangoapps/grades/new/course_data.py
View file @
379a2c18
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
openedx.core.djangoapps.content.block_structure.api
import
get_block_structure_manager
from
xmodule.modulestore.django
import
modulestore
from
..transformer
import
GradesTransformer
...
...
@@ -57,6 +58,12 @@ class CourseData(object):
return
self
.
_structure
@property
def
collected_structure
(
self
):
if
not
self
.
_collected_block_structure
:
self
.
_collected_block_structure
=
get_block_structure_manager
(
self
.
course_key
)
.
get_collected
()
return
self
.
_collected_block_structure
@property
def
course
(
self
):
if
not
self
.
_course
:
self
.
_course
=
modulestore
()
.
get_course
(
self
.
course_key
)
...
...
lms/djangoapps/grades/new/course_grade_factory.py
View file @
379a2c18
...
...
@@ -2,7 +2,6 @@ from collections import namedtuple
import
dogstats_wrapper
as
dog_stats_api
from
logging
import
getLogger
from
openedx.core.djangoapps.content.block_structure.api
import
get_block_structure_manager
from
openedx.core.djangoapps.signals.signals
import
COURSE_GRADE_CHANGED
from
..config
import
assume_zero_if_absent
,
should_persist_grades
...
...
@@ -77,7 +76,15 @@ class CourseGradeFactory(object):
course_data
=
CourseData
(
user
,
course
,
collected_block_structure
,
course_structure
,
course_key
)
return
self
.
_update
(
user
,
course_data
,
read_only
=
False
)
def
iter
(
self
,
course
,
students
,
force_update
=
False
):
def
iter
(
self
,
users
,
course
=
None
,
collected_block_structure
=
None
,
course_structure
=
None
,
course_key
=
None
,
force_update
=
False
,
):
"""
Given a course and an iterable of students (User), yield a GradeResult
for every student enrolled in the course. GradeResult is a named tuple of:
...
...
@@ -92,25 +99,27 @@ class CourseGradeFactory(object):
# compute the grade for all students.
# 2. Optimization: the collected course_structure is not
# retrieved from the data store multiple times.
collected_block_structure
=
get_block_structure_manager
(
course
.
id
)
.
get_collected
()
for
student
in
students
:
with
dog_stats_api
.
timer
(
'lms.grades.CourseGradeFactory.iter'
,
tags
=
[
u'action:{}'
.
format
(
course
.
id
)]):
course_data
=
CourseData
(
None
,
course
,
collected_block_structure
,
course_structure
,
course_key
)
for
user
in
users
:
with
dog_stats_api
.
timer
(
'lms.grades.CourseGradeFactory.iter'
,
tags
=
[
u'action:{}'
.
format
(
course_data
.
course_key
)]
):
try
:
operation
=
CourseGradeFactory
()
.
update
if
force_update
else
CourseGradeFactory
()
.
create
course_grade
=
operation
(
student
,
course
,
collected_block_structure
)
yield
self
.
GradeResult
(
student
,
course_grade
,
""
)
method
=
CourseGradeFactory
()
.
update
if
force_update
else
CourseGradeFactory
()
.
create
course_grade
=
method
(
user
,
course
,
course_data
.
collected_structure
,
course_structure
,
course_key
)
yield
self
.
GradeResult
(
user
,
course_grade
,
""
)
except
Exception
as
exc
:
# pylint: disable=broad-except
# Keep marching on even if this student couldn't be graded for
# some reason, but log it for future reference.
log
.
exception
(
'Cannot grade student
%
s in course
%
s because of exception:
%
s'
,
student
.
id
,
course
.
id
,
user
.
id
,
course
_data
.
course_key
,
exc
.
message
)
yield
self
.
GradeResult
(
student
,
None
,
exc
.
message
)
yield
self
.
GradeResult
(
user
,
None
,
exc
.
message
)
@staticmethod
def
_create_zero
(
user
,
course_data
):
...
...
lms/djangoapps/grades/tasks.py
View file @
379a2c18
...
...
@@ -96,7 +96,7 @@ def compute_grades_for_course(course_key, offset, batch_size, **kwargs): # pyli
course
=
courses
.
get_course_by_id
(
CourseKey
.
from_string
(
course_key
))
enrollments
=
CourseEnrollment
.
objects
.
filter
(
course_id
=
course
.
id
)
.
order_by
(
'created'
)
student_iter
=
(
enrollment
.
user
for
enrollment
in
enrollments
[
offset
:
offset
+
batch_size
])
list
(
CourseGradeFactory
()
.
iter
(
course
,
students
=
student_iter
,
force_update
=
True
))
list
(
CourseGradeFactory
()
.
iter
(
users
=
student_iter
,
course
=
course
,
force_update
=
True
))
@task
(
bind
=
True
,
base
=
_BaseTask
,
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
...
...
lms/djangoapps/grades/tests/test_grades.py
View file @
379a2c18
...
...
@@ -60,7 +60,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
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
,
[]
))
grade_results
=
list
(
CourseGradeFactory
()
.
iter
(
[],
self
.
course
))
self
.
assertEqual
(
grade_results
,
[])
def
test_all_empty_grades
(
self
):
...
...
@@ -130,7 +130,7 @@ class TestGradeIteration(SharedModuleStoreTestCase):
students_to_course_grades
=
{}
students_to_errors
=
{}
for
student
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
course
,
students
):
for
student
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
students
,
course
):
students_to_course_grades
[
student
]
=
course_grade
if
err_msg
:
students_to_errors
[
student
]
=
err_msg
...
...
lms/djangoapps/instructor_task/tasks.py
View file @
379a2c18
...
...
@@ -39,9 +39,9 @@ from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
upload_students_csv
,
)
from
lms.djangoapps.instructor_task.tasks_helper.grades
import
(
generate_course_grade_r
eport
,
generate_problem_grade_r
eport
,
upload_problem_responses_csv
,
CourseGradeR
eport
,
ProblemGradeR
eport
,
ProblemResponses
,
)
from
lms.djangoapps.instructor_task.tasks_helper.misc
import
(
cohort_students_and_upload
,
...
...
@@ -160,7 +160,7 @@ def calculate_problem_responses_csv(entry_id, xmodule_instance_args):
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name
=
ugettext_noop
(
'generated'
)
task_fn
=
partial
(
upload_problem_responses_csv
,
xmodule_instance_args
)
task_fn
=
partial
(
ProblemResponses
.
generate
,
xmodule_instance_args
)
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
...
...
@@ -176,7 +176,7 @@ def calculate_grades_csv(entry_id, xmodule_instance_args):
xmodule_instance_args
.
get
(
'task_id'
),
entry_id
,
action_name
)
task_fn
=
partial
(
generate_course_grade_report
,
xmodule_instance_args
)
task_fn
=
partial
(
CourseGradeReport
.
generate
,
xmodule_instance_args
)
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
...
...
@@ -193,7 +193,7 @@ def calculate_problem_grade_report(entry_id, xmodule_instance_args):
xmodule_instance_args
.
get
(
'task_id'
),
entry_id
,
action_name
)
task_fn
=
partial
(
generate_problem_grade_report
,
xmodule_instance_args
)
task_fn
=
partial
(
ProblemGradeReport
.
generate
,
xmodule_instance_args
)
return
run_main_task
(
entry_id
,
task_fn
,
action_name
)
...
...
lms/djangoapps/instructor_task/tasks_helper/grades.py
View file @
379a2c18
...
...
@@ -3,7 +3,8 @@ Functionality for generating grade reports.
"""
from
collections
import
OrderedDict
from
datetime
import
datetime
from
itertools
import
chain
from
itertools
import
chain
,
izip_longest
,
izip
from
lazy
import
lazy
import
logging
from
pytz
import
UTC
import
re
...
...
@@ -29,355 +30,436 @@ from .utils import upload_csv_to_report_store
TASK_LOG
=
logging
.
getLogger
(
'edx.celery.task'
)
def
generate_course_grade_report
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
# pylint: disable=too-many-statements
class
CourseGradeReportContext
(
object
):
"""
For a given `course_id`, generate a grades CSV file for all students that
are enrolled, and store using a `ReportStore`. Once created, the files can
be accessed by instantiating another `ReportStore` (via
`ReportStore.from_config()`) and calling `link_for()` on it. Writes are
buffered, so we'll never write part of a CSV file to S3 -- i.e. any files
that are visible in ReportStore will be complete ones.
As we start to add more CSV downloads, it will probably be worthwhile to
make a more general CSVDoc class instead of building out the rows like we
do here.
Internal class that provides a common context to use for a single grade
report. When a report is parallelized across multiple processes,
elements of this context are serialized and parsed across process
boundaries.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
status_interval
=
100
enrolled_students
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_id
)
total_enrolled_students
=
enrolled_students
.
count
()
task_progress
=
TaskProgress
(
action_name
,
total_enrolled_students
,
start_time
)
fmt
=
u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
task_info_string
=
fmt
.
format
(
task_id
=
_xmodule_instance_args
.
get
(
'task_id'
)
if
_xmodule_instance_args
is
not
None
else
None
,
entry_id
=
_entry_id
,
course_id
=
course_id
,
task_input
=
_task_input
)
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Starting task execution'
,
task_info_string
,
action_name
)
course
=
get_course_by_id
(
course_id
)
course_is_cohorted
=
is_course_cohorted
(
course
.
id
)
teams_enabled
=
course
.
teams_enabled
cohorts_header
=
[
'Cohort Name'
]
if
course_is_cohorted
else
[]
teams_header
=
[
'Team Name'
]
if
teams_enabled
else
[]
experiment_partitions
=
get_split_user_partitions
(
course
.
user_partitions
)
group_configs_header
=
[
u'Experiment Group ({})'
.
format
(
partition
.
name
)
for
partition
in
experiment_partitions
]
certificate_info_header
=
[
'Certificate Eligible'
,
'Certificate Delivered'
,
'Certificate Type'
]
certificate_whitelist
=
CertificateWhitelist
.
objects
.
filter
(
course_id
=
course_id
,
whitelist
=
True
)
whitelisted_user_ids
=
[
entry
.
user_id
for
entry
in
certificate_whitelist
]
# Loop over all our students and build our CSV lists in memory
rows
=
[]
err_rows
=
[[
"id"
,
"username"
,
"error_msg"
]]
current_step
=
{
'step'
:
'Calculating Grades'
}
student_counter
=
0
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Current step:
%
s, Starting grade calculation for total students:
%
s'
,
task_info_string
,
action_name
,
current_step
,
total_enrolled_students
,
)
graded_assignments
=
_graded_assignments
(
course_id
)
grade_header
=
[]
for
assignment_info
in
graded_assignments
.
itervalues
():
if
assignment_info
[
'use_subsection_headers'
]:
grade_header
.
extend
(
assignment_info
[
'subsection_headers'
]
.
itervalues
())
grade_header
.
append
(
assignment_info
[
'average_header'
])
rows
.
append
(
[
"Student ID"
,
"Email"
,
"Username"
,
"Grade"
]
+
grade_header
+
cohorts_header
+
group_configs_header
+
teams_header
+
[
'Enrollment Track'
,
'Verification Status'
]
+
certificate_info_header
)
for
student
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
course
,
enrolled_students
):
# Periodically update task status (this is a cache write)
if
task_progress
.
attempted
%
status_interval
==
0
:
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
task_progress
.
attempted
+=
1
# Now add a log entry after each student is graded to get a sense
# of the task's progress
student_counter
+=
1
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Current step:
%
s, Grade calculation in-progress for students:
%
s/
%
s'
,
task_info_string
,
action_name
,
current_step
,
student_counter
,
total_enrolled_students
def
__init__
(
self
,
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
self
.
task_info_string
=
(
u'Task: {task_id}, '
u'InstructorTask ID: {entry_id}, '
u'Course: {course_id}, '
u'Input: {task_input}'
)
.
format
(
task_id
=
_xmodule_instance_args
.
get
(
'task_id'
)
if
_xmodule_instance_args
is
not
None
else
None
,
entry_id
=
_entry_id
,
course_id
=
course_id
,
task_input
=
_task_input
,
)
self
.
action_name
=
action_name
self
.
course_id
=
course_id
self
.
task_progress
=
TaskProgress
(
self
.
action_name
,
total
=
None
,
start_time
=
time
())
@lazy
def
course
(
self
):
return
get_course_by_id
(
self
.
course_id
)
@lazy
def
course_experiments
(
self
):
return
get_split_user_partitions
(
self
.
course
.
user_partitions
)
@lazy
def
teams_enabled
(
self
):
return
self
.
course
.
teams_enabled
@lazy
def
cohorts_enabled
(
self
):
return
is_course_cohorted
(
self
.
course_id
)
@lazy
def
graded_assignments
(
self
):
"""
Returns an OrderedDict that maps an assignment type to a dict of
subsection-headers and average-header.
"""
grading_context
=
grading_context_for_course
(
self
.
course_id
)
graded_assignments_map
=
OrderedDict
()
for
assignment_type_name
,
subsection_infos
in
grading_context
[
'all_graded_subsections_by_type'
]
.
iteritems
():
graded_subsections_map
=
OrderedDict
()
for
subsection_index
,
subsection_info
in
enumerate
(
subsection_infos
,
start
=
1
):
subsection
=
subsection_info
[
'subsection_block'
]
header_name
=
u"{assignment_type} {subsection_index}: {subsection_name}"
.
format
(
assignment_type
=
assignment_type_name
,
subsection_index
=
subsection_index
,
subsection_name
=
subsection
.
display_name
,
)
graded_subsections_map
[
subsection
.
location
]
=
header_name
average_header
=
u"{assignment_type}"
.
format
(
assignment_type
=
assignment_type_name
)
if
not
course_grade
:
#
An empty gradeset means we failed to grade a student
.
task_progress
.
failed
+=
1
err_rows
.
append
([
student
.
id
,
student
.
username
,
err_msg
])
continue
# Use separate subsection and average columns only if
#
there's more than one subsection
.
separate_subsection_avg_headers
=
len
(
subsection_infos
)
>
1
if
separate_subsection_avg_headers
:
average_header
+=
u" (Avg)"
# We were able to successfully grade this student for this course.
task_progress
.
succeeded
+=
1
graded_assignments_map
[
assignment_type_name
]
=
{
'subsection_headers'
:
graded_subsections_map
,
'average_header'
:
average_header
,
'separate_subsection_avg_headers'
:
separate_subsection_avg_headers
}
return
graded_assignments_map
cohorts_group_name
=
[]
if
course_is_cohorted
:
group
=
get_cohort
(
student
,
course_id
,
assign
=
False
)
cohorts_group_name
.
append
(
group
.
name
if
group
else
''
)
def
update_status
(
self
,
message
):
"""
Updates the status on the celery task to the given message.
Also logs the update.
"""
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s,
%
s'
,
self
.
task_info_string
,
self
.
action_name
,
message
)
return
self
.
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
message
})
group_configs_group_names
=
[]
for
partition
in
experiment_partitions
:
group
=
PartitionService
(
course_id
)
.
get_group
(
student
,
partition
,
assign
=
False
)
group_configs_group_names
.
append
(
group
.
name
if
group
else
''
)
team_name
=
[]
if
teams_enabled
:
class
CourseGradeReport
(
object
):
"""
Class to encapsulate functionality related to generating Grade Reports.
"""
@classmethod
def
generate
(
cls
,
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
"""
Public method to generate a grade report.
"""
context
=
CourseGradeReportContext
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
)
return
CourseGradeReport
()
.
_generate
(
context
)
def
_generate
(
self
,
context
):
"""
Internal method for generating a grade report for the given context.
"""
context
.
update_status
(
u'Starting grades'
)
success_headers
=
self
.
_success_headers
(
context
)
error_headers
=
self
.
_error_headers
()
batched_rows
=
self
.
_batched_rows
(
context
)
context
.
update_status
(
u'Compiling grades'
)
success_rows
,
error_rows
=
self
.
_compile
(
context
,
batched_rows
)
context
.
update_status
(
u'Uploading grades'
)
self
.
_upload
(
context
,
success_headers
,
success_rows
,
error_headers
,
error_rows
)
return
context
.
update_status
(
u'Completed grades'
)
def
_success_headers
(
self
,
context
):
"""
Returns a list of all applicable column headers for this grade report.
"""
return
(
[
"Student ID"
,
"Email"
,
"Username"
,
"Grade"
]
+
self
.
_grades_header
(
context
)
+
([
'Cohort Name'
]
if
context
.
cohorts_enabled
else
[])
+
[
u'Experiment Group ({})'
.
format
(
partition
.
name
)
for
partition
in
context
.
course_experiments
]
+
([
'Team Name'
]
if
context
.
teams_enabled
else
[])
+
[
'Enrollment Track'
,
'Verification Status'
]
+
[
'Certificate Eligible'
,
'Certificate Delivered'
,
'Certificate Type'
]
)
def
_error_headers
(
self
):
"""
Returns a list of error headers for this grade report.
"""
return
[
"Student ID"
,
"Username"
,
"Error"
]
def
_batched_rows
(
self
,
context
):
"""
A generator of batches of (success_rows, error_rows) for this report.
"""
for
users
in
self
.
_batch_users
(
context
):
yield
self
.
_rows_for_users
(
context
,
users
)
def
_compile
(
self
,
context
,
batched_rows
):
"""
Compiles and returns the complete list of (success_rows, error_rows) for
the given batched_rows and context.
"""
# partition and chain successes and errors
success_rows
,
error_rows
=
izip
(
*
batched_rows
)
success_rows
=
list
(
chain
(
*
success_rows
))
error_rows
=
list
(
chain
(
*
error_rows
))
# update metrics on task status
context
.
task_progress
.
succeeded
=
len
(
success_rows
)
context
.
task_progress
.
failed
=
len
(
error_rows
)
context
.
task_progress
.
attempted
=
context
.
task_progress
.
succeeded
+
context
.
task_progress
.
failed
context
.
task_progress
.
total
=
context
.
task_progress
.
attempted
return
success_rows
,
error_rows
def
_upload
(
self
,
context
,
success_headers
,
success_rows
,
error_headers
,
error_rows
):
"""
Creates and uploads a CSV for the given headers and rows.
"""
date
=
datetime
.
now
(
UTC
)
upload_csv_to_report_store
([
success_headers
]
+
success_rows
,
'grade_report'
,
context
.
course_id
,
date
)
if
len
(
error_rows
)
>
0
:
error_rows
=
[
error_headers
]
+
error_rows
upload_csv_to_report_store
(
error_rows
,
'grade_report_err'
,
context
.
course_id
,
date
)
def
_grades_header
(
self
,
context
):
"""
Returns the applicable grades-related headers for this report.
"""
graded_assignments
=
context
.
graded_assignments
grades_header
=
[]
for
assignment_info
in
graded_assignments
.
itervalues
():
if
assignment_info
[
'separate_subsection_avg_headers'
]:
grades_header
.
extend
(
assignment_info
[
'subsection_headers'
]
.
itervalues
())
grades_header
.
append
(
assignment_info
[
'average_header'
])
return
grades_header
def
_batch_users
(
self
,
context
):
"""
Returns a generator of batches of users.
"""
def
grouper
(
iterable
,
chunk_size
=
1
,
fillvalue
=
None
):
args
=
[
iter
(
iterable
)]
*
chunk_size
return
izip_longest
(
*
args
,
fillvalue
=
fillvalue
)
users
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
context
.
course_id
)
return
grouper
(
users
)
def
_user_grade_results
(
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
.
graded_subsections_by_format
[
assignment_type
][
subsection_location
]
except
KeyError
:
grade_result
=
u'Not Available'
else
:
if
subsection_grade
.
graded_total
.
first_attempted
is
not
None
:
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
.
grader_result
[
'grade_breakdown'
]
.
get
(
assignment_type
,
{})
.
get
(
'percent'
)
grade_results
.
append
([
assignment_average
])
return
[
course_grade
.
percent
]
+
list
(
chain
.
from_iterable
(
grade_results
))
def
_user_cohort_group_names
(
self
,
user
,
context
):
"""
Returns a list of names of cohort groups in which the given user
belongs.
"""
cohort_group_names
=
[]
if
context
.
cohorts_enabled
:
group
=
get_cohort
(
user
,
context
.
course_id
,
assign
=
False
)
cohort_group_names
.
append
(
group
.
name
if
group
else
''
)
return
cohort_group_names
def
_user_experiment_group_names
(
self
,
user
,
context
):
"""
Returns a list of names of course experiments in which the given user
belongs.
"""
experiment_group_names
=
[]
for
partition
in
context
.
course_experiments
:
group
=
PartitionService
(
context
.
course_id
)
.
get_group
(
user
,
partition
,
assign
=
False
)
experiment_group_names
.
append
(
group
.
name
if
group
else
''
)
return
experiment_group_names
def
_user_team_names
(
self
,
user
,
context
):
"""
Returns a list of names of teams in which the given user belongs.
"""
team_names
=
[]
if
context
.
teams_enabled
:
try
:
membership
=
CourseTeamMembership
.
objects
.
get
(
user
=
student
,
team__course_id
=
course_id
)
team_name
.
append
(
membership
.
team
.
name
)
membership
=
CourseTeamMembership
.
objects
.
get
(
user
=
user
,
team__course_id
=
context
.
course_id
)
team_name
s
.
append
(
membership
.
team
.
name
)
except
CourseTeamMembership
.
DoesNotExist
:
team_name
.
append
(
''
)
enrollment_mode
=
CourseEnrollment
.
enrollment_mode_for_user
(
student
,
course_id
)[
0
]
team_names
.
append
(
''
)
return
team_names
def
_user_verification_mode
(
self
,
user
,
context
):
"""
Returns a list of enrollment-mode and verification-status for the
given user.
"""
enrollment_mode
=
CourseEnrollment
.
enrollment_mode_for_user
(
user
,
context
.
course_id
)[
0
]
verification_status
=
SoftwareSecurePhotoVerification
.
verification_status_for_user
(
student
,
course_id
,
user
,
co
ntext
.
co
urse_id
,
enrollment_mode
)
return
[
enrollment_mode
,
verification_status
]
def
_user_certificate_info
(
self
,
user
,
context
,
course_grade
,
whitelisted_user_ids
):
"""
Returns the course certification information for the given user.
"""
certificate_info
=
certificate_info_for_user
(
student
,
course_id
,
user
,
co
ntext
.
co
urse_id
,
course_grade
.
letter_grade
,
student
.
id
in
whitelisted_user_ids
user
.
id
in
whitelisted_user_ids
)
TASK_LOG
.
info
(
u'Student certificate eligibility:
%
s '
u'(user=
%
s, course_id=
%
s, grade_percent=
%
s letter_grade=
%
s gradecutoffs=
%
s, allow_certificate=
%
s, '
u'is_whitelisted=
%
s)'
,
certificate_info
[
0
],
student
,
course_id
,
user
,
co
ntext
.
co
urse_id
,
course_grade
.
percent
,
course_grade
.
letter_grade
,
course
.
grade_cutoffs
,
student
.
profile
.
allow_certificate
,
student
.
id
in
whitelisted_user_ids
co
ntext
.
co
urse
.
grade_cutoffs
,
user
.
profile
.
allow_certificate
,
user
.
id
in
whitelisted_user_ids
,
)
grade_results
=
[]
for
assignment_type
,
assignment_info
in
graded_assignments
.
iteritems
():
for
subsection_location
in
assignment_info
[
'subsection_headers'
]:
return
certificate_info
def
_rows_for_users
(
self
,
context
,
users
):
"""
Returns a list of rows for the given users for this report.
"""
certificate_whitelist
=
CertificateWhitelist
.
objects
.
filter
(
course_id
=
context
.
course_id
,
whitelist
=
True
)
whitelisted_user_ids
=
[
entry
.
user_id
for
entry
in
certificate_whitelist
]
success_rows
,
error_rows
=
[],
[]
for
user
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
users
,
course_key
=
context
.
course_id
):
if
not
course_grade
:
# An empty gradeset means we failed to grade a student.
error_rows
.
append
([
user
.
id
,
user
.
username
,
err_msg
])
else
:
success_rows
.
append
(
[
user
.
id
,
user
.
email
,
user
.
username
]
+
self
.
_user_grade_results
(
course_grade
,
context
)
+
self
.
_user_cohort_group_names
(
user
,
context
)
+
self
.
_user_experiment_group_names
(
user
,
context
)
+
self
.
_user_team_names
(
user
,
context
)
+
self
.
_user_verification_mode
(
user
,
context
)
+
self
.
_user_certificate_info
(
user
,
context
,
course_grade
,
whitelisted_user_ids
)
)
return
success_rows
,
error_rows
class
ProblemGradeReport
(
object
):
@classmethod
def
generate
(
cls
,
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
"""
Generate a CSV containing all students' problem grades within a given
`course_id`.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
status_interval
=
100
enrolled_students
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_id
)
task_progress
=
TaskProgress
(
action_name
,
enrolled_students
.
count
(),
start_time
)
# This struct encapsulates both the display names of each static item in the
# header row as values as well as the django User field names of those items
# 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
)
# Just generate the static fields for now.
rows
=
[
list
(
header_row
.
values
())
+
[
'Grade'
]
+
list
(
chain
.
from_iterable
(
graded_scorable_blocks
.
values
()))]
error_rows
=
[
list
(
header_row
.
values
())
+
[
'error_msg'
]]
current_step
=
{
'step'
:
'Calculating Grades'
}
course
=
get_course_by_id
(
course_id
)
for
student
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
enrolled_students
,
course
):
student_fields
=
[
getattr
(
student
,
field_name
)
for
field_name
in
header_row
]
task_progress
.
attempted
+=
1
if
not
course_grade
:
# There was an error grading this student.
if
not
err_msg
:
err_msg
=
u'Unknown error'
error_rows
.
append
(
student_fields
+
[
err_msg
])
task_progress
.
failed
+=
1
continue
earned_possible_values
=
[]
for
block_location
in
graded_scorable_blocks
:
try
:
subsection_grade
=
course_grade
.
graded_subsections_by_format
[
assignment_type
][
subsection
_location
]
problem_score
=
course_grade
.
problem_scores
[
block
_location
]
except
KeyError
:
grade_results
.
append
([
u'Not Available'
])
earned_possible_values
.
append
([
u'Not Available'
,
u'Not Available'
])
else
:
if
subsection_grade
.
graded_total
.
first_attempted
is
not
None
:
grade_results
.
append
(
[
subsection_grade
.
graded_total
.
earned
/
subsection_grade
.
graded_total
.
possible
]
)
if
problem_score
.
first_attempted
:
earned_possible_values
.
append
([
problem_score
.
earned
,
problem_score
.
possible
])
else
:
grade_results
.
append
([
u'Not Attempted'
])
if
assignment_info
[
'use_subsection_headers'
]:
assignment_average
=
course_grade
.
grader_result
[
'grade_breakdown'
]
.
get
(
assignment_type
,
{})
.
get
(
'percent'
)
grade_results
.
append
([
assignment_average
])
grade_results
=
list
(
chain
.
from_iterable
(
grade_results
))
rows
.
append
(
[
student
.
id
,
student
.
email
,
student
.
username
,
course_grade
.
percent
]
+
grade_results
+
cohorts_group_name
+
group_configs_group_names
+
team_name
+
[
enrollment_mode
]
+
[
verification_status
]
+
certificate_info
)
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Current step:
%
s, Grade calculation completed for students:
%
s/
%
s'
,
task_info_string
,
action_name
,
current_step
,
student_counter
,
total_enrolled_students
)
# By this point, we've got the rows we're going to stuff into our CSV files.
current_step
=
{
'step'
:
'Uploading CSVs'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Current step:
%
s'
,
task_info_string
,
action_name
,
current_step
)
# Perform the actual upload
upload_csv_to_report_store
(
rows
,
'grade_report'
,
course_id
,
start_date
)
# If there are any error rows (don't count the header), write them out as well
if
len
(
err_rows
)
>
1
:
upload_csv_to_report_store
(
err_rows
,
'grade_report_err'
,
course_id
,
start_date
)
# One last update before we close out...
TASK_LOG
.
info
(
u'
%
s, Task type:
%
s, Finalizing grade task'
,
task_info_string
,
action_name
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
generate_problem_grade_report
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
_task_input
,
action_name
):
"""
Generate a CSV containing all students' problem grades within a given
`course_id`.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
status_interval
=
100
enrolled_students
=
CourseEnrollment
.
objects
.
users_enrolled_in
(
course_id
)
task_progress
=
TaskProgress
(
action_name
,
enrolled_students
.
count
(),
start_time
)
# This struct encapsulates both the display names of each static item in the
# header row as values as well as the django User field names of those items
# 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
=
_graded_scorable_blocks_to_header
(
course_id
)
# Just generate the static fields for now.
rows
=
[
list
(
header_row
.
values
())
+
[
'Grade'
]
+
list
(
chain
.
from_iterable
(
graded_scorable_blocks
.
values
()))]
error_rows
=
[
list
(
header_row
.
values
())
+
[
'error_msg'
]]
current_step
=
{
'step'
:
'Calculating Grades'
}
course
=
get_course_by_id
(
course_id
)
for
student
,
course_grade
,
err_msg
in
CourseGradeFactory
()
.
iter
(
course
,
enrolled_students
):
student_fields
=
[
getattr
(
student
,
field_name
)
for
field_name
in
header_row
]
task_progress
.
attempted
+=
1
if
not
course_grade
:
# There was an error grading this student.
if
not
err_msg
:
err_msg
=
u'Unknown error'
error_rows
.
append
(
student_fields
+
[
err_msg
])
task_progress
.
failed
+=
1
continue
earned_possible_values
=
[]
for
block_location
in
graded_scorable_blocks
:
try
:
problem_score
=
course_grade
.
problem_scores
[
block_location
]
except
KeyError
:
earned_possible_values
.
append
([
u'Not Available'
,
u'Not Available'
])
else
:
if
problem_score
.
first_attempted
:
earned_possible_values
.
append
([
problem_score
.
earned
,
problem_score
.
possible
])
else
:
earned_possible_values
.
append
([
u'Not Attempted'
,
problem_score
.
possible
])
rows
.
append
(
student_fields
+
[
course_grade
.
percent
]
+
list
(
chain
.
from_iterable
(
earned_possible_values
)))
task_progress
.
succeeded
+=
1
if
task_progress
.
attempted
%
status_interval
==
0
:
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Perform the upload if any students have been successfully graded
if
len
(
rows
)
>
1
:
upload_csv_to_report_store
(
rows
,
'problem_grade_report'
,
course_id
,
start_date
)
# If there are any error rows, write them out as well
if
len
(
error_rows
)
>
1
:
upload_csv_to_report_store
(
error_rows
,
'problem_grade_report_err'
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
'Uploading CSV'
})
def
upload_problem_responses_csv
(
_xmodule_instance_args
,
_entry_id
,
course_id
,
task_input
,
action_name
):
"""
For a given `course_id`, generate a CSV file containing
all student answers to a given problem, and store using a `ReportStore`.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
num_reports
=
1
task_progress
=
TaskProgress
(
action_name
,
num_reports
,
start_time
)
current_step
=
{
'step'
:
'Calculating students answers to problem'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Compute result table and format it
problem_location
=
task_input
.
get
(
'problem_location'
)
student_data
=
list_problem_responses
(
course_id
,
problem_location
)
features
=
[
'username'
,
'state'
]
header
,
rows
=
format_dictlist
(
student_data
,
features
)
task_progress
.
attempted
=
task_progress
.
succeeded
=
len
(
rows
)
task_progress
.
skipped
=
task_progress
.
total
-
task_progress
.
attempted
rows
.
insert
(
0
,
header
)
current_step
=
{
'step'
:
'Uploading CSV'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Perform the upload
problem_location
=
re
.
sub
(
r'[:/]'
,
'_'
,
problem_location
)
csv_name
=
'student_state_from_{}'
.
format
(
problem_location
)
upload_csv_to_report_store
(
rows
,
csv_name
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
def
_graded_assignments
(
course_key
):
"""
Returns an OrderedDict that maps an assignment type to a dict of subsection-headers and average-header.
"""
grading_context
=
grading_context_for_course
(
course_key
)
graded_assignments_map
=
OrderedDict
()
for
assignment_type_name
,
subsection_infos
in
grading_context
[
'all_graded_subsections_by_type'
]
.
iteritems
():
graded_subsections_map
=
OrderedDict
()
for
subsection_index
,
subsection_info
in
enumerate
(
subsection_infos
,
start
=
1
):
subsection
=
subsection_info
[
'subsection_block'
]
header_name
=
u"{assignment_type} {subsection_index}: {subsection_name}"
.
format
(
assignment_type
=
assignment_type_name
,
subsection_index
=
subsection_index
,
subsection_name
=
subsection
.
display_name
,
)
graded_subsections_map
[
subsection
.
location
]
=
header_name
average_header
=
u"{assignment_type}"
.
format
(
assignment_type
=
assignment_type_name
)
# Use separate subsection and average columns only if
# there's more than one subsection.
use_subsection_headers
=
len
(
subsection_infos
)
>
1
if
use_subsection_headers
:
average_header
+=
u" (Avg)"
graded_assignments_map
[
assignment_type_name
]
=
{
'subsection_headers'
:
graded_subsections_map
,
'average_header'
:
average_header
,
'use_subsection_headers'
:
use_subsection_headers
}
return
graded_assignments_map
def
_graded_scorable_blocks_to_header
(
course_key
):
"""
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
)
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'
]:
header_name
=
(
u"{assignment_type} {subsection_index}: "
u"{subsection_name} - {scorable_block_name}"
)
.
format
(
scorable_block_name
=
scorable_block
.
display_name
,
assignment_type
=
assignment_type_name
,
subsection_index
=
subsection_index
,
subsection_name
=
subsection_info
[
'subsection_block'
]
.
display_name
,
)
scorable_blocks_map
[
scorable_block
.
location
]
=
[
header_name
+
" (Earned)"
,
header_name
+
" (Possible)"
]
return
scorable_blocks_map
earned_possible_values
.
append
([
u'Not Attempted'
,
problem_score
.
possible
])
rows
.
append
(
student_fields
+
[
course_grade
.
percent
]
+
list
(
chain
.
from_iterable
(
earned_possible_values
)))
task_progress
.
succeeded
+=
1
if
task_progress
.
attempted
%
status_interval
==
0
:
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Perform the upload if any students have been successfully graded
if
len
(
rows
)
>
1
:
upload_csv_to_report_store
(
rows
,
'problem_grade_report'
,
course_id
,
start_date
)
# If there are any error rows, write them out as well
if
len
(
error_rows
)
>
1
:
upload_csv_to_report_store
(
error_rows
,
'problem_grade_report_err'
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
{
'step'
:
'Uploading CSV'
})
@classmethod
def
_graded_scorable_blocks_to_header
(
cls
,
course_key
):
"""
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
)
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'
]:
header_name
=
(
u"{assignment_type} {subsection_index}: "
u"{subsection_name} - {scorable_block_name}"
)
.
format
(
scorable_block_name
=
scorable_block
.
display_name
,
assignment_type
=
assignment_type_name
,
subsection_index
=
subsection_index
,
subsection_name
=
subsection_info
[
'subsection_block'
]
.
display_name
,
)
scorable_blocks_map
[
scorable_block
.
location
]
=
[
header_name
+
" (Earned)"
,
header_name
+
" (Possible)"
]
return
scorable_blocks_map
class
ProblemResponses
(
object
):
@classmethod
def
generate
(
cls
,
_xmodule_instance_args
,
_entry_id
,
course_id
,
task_input
,
action_name
):
"""
For a given `course_id`, generate a CSV file containing
all student answers to a given problem, and store using a `ReportStore`.
"""
start_time
=
time
()
start_date
=
datetime
.
now
(
UTC
)
num_reports
=
1
task_progress
=
TaskProgress
(
action_name
,
num_reports
,
start_time
)
current_step
=
{
'step'
:
'Calculating students answers to problem'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Compute result table and format it
problem_location
=
task_input
.
get
(
'problem_location'
)
student_data
=
list_problem_responses
(
course_id
,
problem_location
)
features
=
[
'username'
,
'state'
]
header
,
rows
=
format_dictlist
(
student_data
,
features
)
task_progress
.
attempted
=
task_progress
.
succeeded
=
len
(
rows
)
task_progress
.
skipped
=
task_progress
.
total
-
task_progress
.
attempted
rows
.
insert
(
0
,
header
)
current_step
=
{
'step'
:
'Uploading CSV'
}
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
# Perform the upload
problem_location
=
re
.
sub
(
r'[:/]'
,
'_'
,
problem_location
)
csv_name
=
'student_state_from_{}'
.
format
(
problem_location
)
upload_csv_to_report_store
(
rows
,
csv_name
,
course_id
,
start_date
)
return
task_progress
.
update_task_state
(
extra_meta
=
current_step
)
lms/djangoapps/instructor_task/tests/test_integration.py
View file @
379a2c18
...
...
@@ -32,7 +32,7 @@ from lms.djangoapps.instructor_task.api import (
submit_delete_problem_state_for_all_students
)
from
lms.djangoapps.instructor_task.models
import
InstructorTask
from
lms.djangoapps.instructor_task.tasks_helper.grades
import
generate_course_grade_r
eport
from
lms.djangoapps.instructor_task.tasks_helper.grades
import
CourseGradeR
eport
from
lms.djangoapps.instructor_task.tests.test_base
import
(
InstructorTaskModuleTestCase
,
TestReportMixin
,
...
...
@@ -572,10 +572,10 @@ class TestGradeReportConditionalContent(TestReportMixin, TestConditionalContent,
def
verify_csv_task_success
(
self
,
task_result
):
"""
Verify that all students were successfully graded by
`
generate_course_grade_r
eport`.
`
CourseGradeR
eport`.
Arguments:
task_result (dict): Return value of `
generate_course_grade_report
`.
task_result (dict): Return value of `
CourseGradeReport.generate
`.
"""
self
.
assertDictContainsSubset
({
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
task_result
)
...
...
@@ -636,7 +636,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestConditionalContent,
self
.
submit_student_answer
(
self
.
student_b
.
username
,
problem_b_url
,
[
OPTION_1
,
OPTION_2
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
verify_csv_task_success
(
result
)
self
.
verify_grades_in_csv
(
[
...
...
@@ -669,7 +669,7 @@ class TestGradeReportConditionalContent(TestReportMixin, TestConditionalContent,
self
.
submit_student_answer
(
self
.
student_a
.
username
,
problem_a_url
,
[
OPTION_1
,
OPTION_1
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
verify_csv_task_success
(
result
)
self
.
verify_grades_in_csv
(
[
...
...
lms/djangoapps/instructor_task/tests/test_tasks_helper.py
View file @
379a2c18
...
...
@@ -59,14 +59,13 @@ from lms.djangoapps.instructor_task.tasks_helper.enrollments import (
upload_students_csv
,
)
from
lms.djangoapps.instructor_task.tasks_helper.grades
import
(
generate_course_grade_r
eport
,
generate_problem_grade_r
eport
,
upload_problem_responses_csv
,
CourseGradeR
eport
,
ProblemGradeR
eport
,
ProblemResponses
,
)
from
lms.djangoapps.instructor_task.tasks_helper.misc
import
(
cohort_students_and_upload
,
upload_course_survey_report
,
upload_proctored_exam_results_report
,
upload_ora2_data
,
)
from
..tasks_helper.utils
import
(
...
...
@@ -89,7 +88,7 @@ class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCas
Verify cell data in the grades CSV for a particular user.
"""
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_course_grade_report
(
None
,
None
,
course_id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
course_id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
report_csv_filename
=
report_store
.
links_for
(
course_id
)[
0
][
0
]
...
...
@@ -121,7 +120,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
self
.
current_task
.
update_state
=
Mock
()
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
)
as
mock_current_task
:
mock_current_task
.
return_value
=
self
.
current_task
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
num_students
=
len
(
emails
)
self
.
assertDictContainsSubset
({
'attempted'
:
num_students
,
'succeeded'
:
num_students
,
'failed'
:
0
},
result
)
...
...
@@ -135,7 +134,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
mock_grades_iter
.
return_value
=
[
(
self
.
create_student
(
'username'
,
'student@example.com'
),
None
,
'Cannot grade student'
)
]
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'attempted'
:
1
,
'succeeded'
:
0
,
'failed'
:
1
},
result
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
...
...
@@ -319,7 +318,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
''
,
)
]
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'attempted'
:
1
,
'succeeded'
:
1
,
'failed'
:
0
},
result
)
...
...
@@ -378,7 +377,7 @@ class TestProblemResponsesReport(TestReportMixin, InstructorTaskCourseTestCase):
{
'username'
:
'user1'
,
'state'
:
u'state1'
},
{
'username'
:
'user2'
,
'state'
:
u'state2'
},
]
result
=
upload_problem_responses_csv
(
None
,
None
,
self
.
course
.
id
,
task_input
,
'calculated'
)
result
=
ProblemResponses
.
generate
(
None
,
None
,
self
.
course
.
id
,
task_input
,
'calculated'
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
links
=
report_store
.
links_for
(
self
.
course
.
id
)
...
...
@@ -609,7 +608,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
Verify that we see no grade information for a course with no graded
problems.
"""
result
=
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'action_name'
:
'graded'
,
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
self
.
verify_rows_in_csv
([
dict
(
zip
(
...
...
@@ -633,7 +632,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
self
.
define_option_problem
(
u'Problem1'
,
parent
=
vertical
)
self
.
submit_student_answer
(
self
.
student_1
.
username
,
u'Problem1'
,
[
'Option 1'
])
result
=
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'action_name'
:
'graded'
,
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
problem_name
=
u'Homework 1: Subsection - Problem1'
header_row
=
self
.
csv_header_row
+
[
problem_name
+
' (Earned)'
,
problem_name
+
' (Possible)'
]
...
...
@@ -670,7 +669,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
mock_grades_iter
.
return_value
=
[
(
student
,
None
,
error_message
)
]
result
=
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
({
'attempted'
:
1
,
'succeeded'
:
0
,
'failed'
:
1
},
result
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
...
...
@@ -720,7 +719,7 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent,
self
.
submit_student_answer
(
self
.
student_b
.
username
,
self
.
problem_b_url
,
[
self
.
OPTION_1
,
self
.
OPTION_2
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'graded'
,
'attempted'
:
2
,
'succeeded'
:
2
,
'failed'
:
0
},
result
)
...
...
@@ -812,7 +811,7 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent,
header_row
+=
[
problem
+
' (Earned)'
,
problem
+
' (Possible)'
]
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertEquals
(
self
.
get_csv_row_with_headers
(),
header_row
)
...
...
@@ -868,7 +867,7 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In
self
.
submit_student_answer
(
self
.
beta_user
.
username
,
u'Problem1'
,
[
'Option 1'
,
'Option 2'
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_problem_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
ProblemGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'graded'
,
'attempted'
:
4
,
'succeeded'
:
4
,
'failed'
:
0
},
result
)
...
...
@@ -1579,7 +1578,7 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
self
.
submit_student_answer
(
self
.
student
.
username
,
u'Problem1'
,
[
'Option 1'
])
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
result
=
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
result
=
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
self
.
assertDictContainsSubset
(
{
'action_name'
:
'graded'
,
'attempted'
:
1
,
'succeeded'
:
1
,
'failed'
:
0
},
result
,
...
...
@@ -1654,7 +1653,7 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas
Verify grade report data.
"""
with
patch
(
'lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'
):
generate_course_grade_report
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
CourseGradeReport
.
generate
(
None
,
None
,
self
.
course
.
id
,
None
,
'graded'
)
report_store
=
ReportStore
.
from_config
(
config_name
=
'GRADES_DOWNLOAD'
)
report_csv_filename
=
report_store
.
links_for
(
self
.
course
.
id
)[
0
][
0
]
report_path
=
report_store
.
path_to
(
self
.
course
.
id
,
report_csv_filename
)
...
...
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