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
266bfb1a
Commit
266bfb1a
authored
Nov 08, 2016
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Optimize async grades computation
TNL-5909
parent
8a784eef
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
110 additions
and
170 deletions
+110
-170
lms/djangoapps/grades/new/course_grade.py
+1
-2
lms/djangoapps/grades/new/subsection_grade.py
+15
-35
lms/djangoapps/grades/signals/handlers.py
+6
-9
lms/djangoapps/grades/signals/signals.py
+1
-0
lms/djangoapps/grades/tasks.py
+37
-57
lms/djangoapps/grades/tests/test_tasks.py
+6
-67
openedx/core/djangoapps/performance/utils.py
+44
-0
No files found.
lms/djangoapps/grades/new/course_grade.py
View file @
266bfb1a
...
@@ -340,11 +340,10 @@ class CourseGradeFactory(object):
...
@@ -340,11 +340,10 @@ class CourseGradeFactory(object):
self
.
_compute_and_update_grade
(
course
,
course_structure
,
read_only
)
self
.
_compute_and_update_grade
(
course
,
course_structure
,
read_only
)
)
)
def
update
(
self
,
course
):
def
update
(
self
,
course
,
course_structure
):
"""
"""
Updates the CourseGrade for this Factory's student.
Updates the CourseGrade for this Factory's student.
"""
"""
course_structure
=
get_course_blocks
(
self
.
student
,
course
.
location
)
self
.
_compute_and_update_grade
(
course
,
course_structure
)
self
.
_compute_and_update_grade
(
course
,
course_structure
)
def
get_persisted
(
self
,
course
):
def
get_persisted
(
self
,
course
):
...
...
lms/djangoapps/grades/new/subsection_grade.py
View file @
266bfb1a
...
@@ -12,7 +12,7 @@ from lms.djangoapps.grades.scores import get_score, possibly_scored
...
@@ -12,7 +12,7 @@ from lms.djangoapps.grades.scores import get_score, possibly_scored
from
lms.djangoapps.grades.models
import
BlockRecord
,
PersistentSubsectionGrade
from
lms.djangoapps.grades.models
import
BlockRecord
,
PersistentSubsectionGrade
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
openedx.core.lib.grade_utils
import
is_score_higher
from
openedx.core.lib.grade_utils
import
is_score_higher
from
student.models
import
anonymous_id_for_user
,
User
from
student.models
import
anonymous_id_for_user
from
submissions
import
api
as
submissions_api
from
submissions
import
api
as
submissions_api
from
traceback
import
format_exc
from
traceback
import
format_exc
from
xmodule
import
block_metadata_utils
,
graders
from
xmodule
import
block_metadata_utils
,
graders
...
@@ -49,7 +49,7 @@ class SubsectionGrade(object):
...
@@ -49,7 +49,7 @@ class SubsectionGrade(object):
self
.
due
=
getattr
(
subsection
,
'due'
,
None
)
self
.
due
=
getattr
(
subsection
,
'due'
,
None
)
self
.
graded
=
getattr
(
subsection
,
'graded'
,
False
)
self
.
graded
=
getattr
(
subsection
,
'graded'
,
False
)
self
.
course_version
=
getattr
(
course
,
'course_version'
,
None
)
self
.
course_version
=
getattr
(
subsection
,
'course_version'
,
None
)
self
.
subtree_edited_timestamp
=
subsection
.
subtree_edited_on
self
.
subtree_edited_timestamp
=
subsection
.
subtree_edited_on
self
.
graded_total
=
None
# aggregated grade for all graded problems
self
.
graded_total
=
None
# aggregated grade for all graded problems
...
@@ -211,24 +211,20 @@ class SubsectionGradeFactory(object):
...
@@ -211,24 +211,20 @@ class SubsectionGradeFactory(object):
self
.
_cached_subsection_grades
=
None
self
.
_cached_subsection_grades
=
None
self
.
_unsaved_subsection_grades
=
[]
self
.
_unsaved_subsection_grades
=
[]
def
create
(
self
,
subsection
,
block_structure
=
None
,
read_only
=
False
):
def
create
(
self
,
subsection
,
read_only
=
False
):
"""
"""
Returns the SubsectionGrade object for the student and subsection.
Returns the SubsectionGrade object for the student and subsection.
If block_structure is provided, uses it for finding and computing
the grade instead of the course_structure passed in earlier.
If read_only is True, doesn't save any updates to the grades.
If read_only is True, doesn't save any updates to the grades.
"""
"""
self
.
_log_event
(
self
.
_log_event
(
log
.
debug
,
u"create, read_only: {0}, subsection: {1}"
.
format
(
read_only
,
subsection
.
location
)
log
.
debug
,
u"create, read_only: {0}, subsection: {1}"
.
format
(
read_only
,
subsection
.
location
)
,
subsection
,
)
)
block_structure
=
self
.
_get_block_structure
(
block_structure
)
subsection_grade
=
self
.
_get_bulk_cached_grade
(
subsection
)
subsection_grade
=
self
.
_get_bulk_cached_grade
(
subsection
,
block_structure
)
if
not
subsection_grade
:
if
not
subsection_grade
:
subsection_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_structure
(
subsection_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_structure
(
self
.
student
,
block
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
self
.
student
,
self
.
course
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
)
if
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
if
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
if
read_only
:
if
read_only
:
...
@@ -243,13 +239,11 @@ class SubsectionGradeFactory(object):
...
@@ -243,13 +239,11 @@ class SubsectionGradeFactory(object):
"""
"""
Bulk creates all the unsaved subsection_grades to this point.
Bulk creates all the unsaved subsection_grades to this point.
"""
"""
self
.
_log_event
(
log
.
debug
,
u"bulk_create_unsaved"
)
with
persistence_safe_fallback
():
with
persistence_safe_fallback
():
SubsectionGrade
.
bulk_create_models
(
self
.
student
,
self
.
_unsaved_subsection_grades
,
self
.
course
.
id
)
SubsectionGrade
.
bulk_create_models
(
self
.
student
,
self
.
_unsaved_subsection_grades
,
self
.
course
.
id
)
self
.
_unsaved_subsection_grades
=
[]
self
.
_unsaved_subsection_grades
=
[]
def
update
(
self
,
subsection
,
block_structure
=
None
,
only_if_higher
=
None
):
def
update
(
self
,
subsection
,
only_if_higher
=
None
):
"""
"""
Updates the SubsectionGrade object for the student and subsection.
Updates the SubsectionGrade object for the student and subsection.
"""
"""
...
@@ -258,11 +252,10 @@ class SubsectionGradeFactory(object):
...
@@ -258,11 +252,10 @@ class SubsectionGradeFactory(object):
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
):
return
return
self
.
_log_event
(
log
.
warning
,
u"update, subsection: {}"
.
format
(
subsection
.
location
))
self
.
_log_event
(
log
.
warning
,
u"update, subsection: {}"
.
format
(
subsection
.
location
)
,
subsection
)
block_structure
=
self
.
_get_block_structure
(
block_structure
)
calculated_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_structure
(
calculated_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_structure
(
self
.
student
,
block
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
self
.
student
,
self
.
course
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
)
if
only_if_higher
:
if
only_if_higher
:
...
@@ -272,7 +265,7 @@ class SubsectionGradeFactory(object):
...
@@ -272,7 +265,7 @@ class SubsectionGradeFactory(object):
pass
pass
else
:
else
:
orig_subsection_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_model
(
orig_subsection_grade
=
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_model
(
self
.
student
,
grade_model
,
block
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
self
.
student
,
grade_model
,
self
.
course
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
)
if
not
is_score_higher
(
if
not
is_score_higher
(
orig_subsection_grade
.
graded_total
.
earned
,
orig_subsection_grade
.
graded_total
.
earned
,
...
@@ -304,7 +297,7 @@ class SubsectionGradeFactory(object):
...
@@ -304,7 +297,7 @@ class SubsectionGradeFactory(object):
anonymous_user_id
=
anonymous_id_for_user
(
self
.
student
,
self
.
course
.
id
)
anonymous_user_id
=
anonymous_id_for_user
(
self
.
student
,
self
.
course
.
id
)
return
submissions_api
.
get_scores
(
unicode
(
self
.
course
.
id
),
anonymous_user_id
)
return
submissions_api
.
get_scores
(
unicode
(
self
.
course
.
id
),
anonymous_user_id
)
def
_get_bulk_cached_grade
(
self
,
subsection
,
block_structure
):
# pylint: disable=unused-argument
def
_get_bulk_cached_grade
(
self
,
subsection
):
"""
"""
Returns the student's SubsectionGrade for the subsection,
Returns the student's SubsectionGrade for the subsection,
while caching the results of a bulk retrieval for the
while caching the results of a bulk retrieval for the
...
@@ -318,7 +311,7 @@ class SubsectionGradeFactory(object):
...
@@ -318,7 +311,7 @@ class SubsectionGradeFactory(object):
subsection_grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
subsection_grade
=
saved_subsection_grades
.
get
(
subsection
.
location
)
if
subsection_grade
:
if
subsection_grade
:
return
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_model
(
return
SubsectionGrade
(
subsection
,
self
.
course
)
.
init_from_model
(
self
.
student
,
subsection_grade
,
block
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
self
.
student
,
subsection_grade
,
self
.
course
_structure
,
self
.
_submissions_scores
,
self
.
_csm_scores
,
)
)
def
_get_bulk_cached_subsection_grades
(
self
):
def
_get_bulk_cached_subsection_grades
(
self
):
...
@@ -342,27 +335,14 @@ class SubsectionGradeFactory(object):
...
@@ -342,27 +335,14 @@ class SubsectionGradeFactory(object):
if
self
.
_cached_subsection_grades
is
not
None
:
if
self
.
_cached_subsection_grades
is
not
None
:
self
.
_cached_subsection_grades
[
subsection_usage_key
]
=
subsection_model
self
.
_cached_subsection_grades
[
subsection_usage_key
]
=
subsection_model
def
_get_block_structure
(
self
,
block_structure
):
def
_log_event
(
self
,
log_func
,
log_statement
,
subsection
):
"""
If block_structure is None, returns self.course_structure.
Otherwise, returns block_structure after verifying that the
given block_structure is a sub-structure of self.course_structure.
"""
if
block_structure
:
if
block_structure
.
root_block_usage_key
not
in
self
.
course_structure
:
raise
ValueError
return
block_structure
else
:
return
self
.
course_structure
def
_log_event
(
self
,
log_func
,
log_statement
):
"""
"""
Logs the given statement, for this instance.
Logs the given statement, for this instance.
"""
"""
log_func
(
u"Persistent Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}"
.
format
(
log_func
(
u"Persistent Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}"
.
format
(
log_statement
,
log_statement
,
self
.
course
.
id
,
self
.
course
.
id
,
getattr
(
s
elf
.
course
,
'course_version'
,
None
),
getattr
(
s
ubsection
,
'course_version'
,
None
),
s
elf
.
course
.
subtree_edited_on
,
s
ubsection
.
subtree_edited_on
,
self
.
student
.
id
,
self
.
student
.
id
,
))
))
lms/djangoapps/grades/signals/handlers.py
View file @
266bfb1a
...
@@ -2,7 +2,6 @@
...
@@ -2,7 +2,6 @@
Grades related signals.
Grades related signals.
"""
"""
from
celery
import
Task
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
from
logging
import
getLogger
from
logging
import
getLogger
...
@@ -12,7 +11,8 @@ from student.models import user_by_anonymous_id
...
@@ -12,7 +11,8 @@ from student.models import user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
submissions.models
import
score_set
,
score_reset
from
.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
,
SCORE_PUBLISHED
from
.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
,
SCORE_PUBLISHED
from
..tasks
import
recalculate_subsection_grade
,
recalculate_course_grade
from
..new.course_grade
import
CourseGradeFactory
from
..tasks
import
recalculate_subsection_grade
log
=
getLogger
(
__name__
)
log
=
getLogger
(
__name__
)
...
@@ -92,7 +92,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
...
@@ -92,7 +92,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
if
only_if_higher
:
if
only_if_higher
:
previous_score
=
get_score
(
user
.
id
,
block
.
location
)
previous_score
=
get_score
(
user
.
id
,
block
.
location
)
if
previous_score
:
if
previous_score
is
not
None
:
prev_raw_earned
,
prev_raw_possible
=
previous_score
# pylint: disable=unpacking-non-sequence
prev_raw_earned
,
prev_raw_possible
=
previous_score
# pylint: disable=unpacking-non-sequence
if
not
is_score_higher
(
prev_raw_earned
,
prev_raw_possible
,
raw_earned
,
raw_possible
):
if
not
is_score_higher
(
prev_raw_earned
,
prev_raw_possible
,
raw_earned
,
raw_possible
):
...
@@ -136,11 +136,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
...
@@ -136,11 +136,8 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
@receiver
(
SUBSECTION_SCORE_CHANGED
)
@receiver
(
SUBSECTION_SCORE_CHANGED
)
def
enqueue_course_update
(
send
er
,
**
kwargs
):
# pylint: disable=unused-argument
def
recalculate_course_grade
(
sender
,
course
,
course_structure
,
us
er
,
**
kwargs
):
# pylint: disable=unused-argument
"""
"""
Handles the SUBSECTION_SCORE_CHANGED signal by enqueueing a course update operation to occur asynchronously
.
Updates a saved course grade
.
"""
"""
if
isinstance
(
sender
,
Task
):
# We're already in a async worker, just do the task
CourseGradeFactory
(
user
)
.
update
(
course
,
course_structure
)
recalculate_course_grade
.
apply
(
args
=
(
kwargs
[
'user'
]
.
id
,
unicode
(
kwargs
[
'course'
]
.
id
)))
else
:
# Otherwise, queue the work to be done asynchronously
recalculate_course_grade
.
apply_async
(
args
=
(
kwargs
[
'user'
]
.
id
,
unicode
(
kwargs
[
'course'
]
.
id
)))
lms/djangoapps/grades/signals/signals.py
View file @
266bfb1a
...
@@ -45,6 +45,7 @@ SCORE_PUBLISHED = Signal(
...
@@ -45,6 +45,7 @@ SCORE_PUBLISHED = Signal(
SUBSECTION_SCORE_CHANGED
=
Signal
(
SUBSECTION_SCORE_CHANGED
=
Signal
(
providing_args
=
[
providing_args
=
[
'course'
,
# Course object
'course'
,
# Course object
'course_structure'
,
# BlockStructure object
'user'
,
# User object
'user'
,
# User object
'subsection_grade'
,
# SubsectionGrade object
'subsection_grade'
,
# SubsectionGrade object
]
]
...
...
lms/djangoapps/grades/tasks.py
View file @
266bfb1a
...
@@ -12,11 +12,9 @@ from courseware.model_data import get_score
...
@@ -12,11 +12,9 @@ from courseware.model_data import get_score
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
lms.djangoapps.course_blocks.api
import
get_course_blocks
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.keys
import
UsageKey
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.locator
import
CourseLocator
from
openedx.core.djangoapps.content.block_structure.api
import
get_course_in_cache
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
.config.models
import
PersistentGradesEnabledFlag
from
.config.models
import
PersistentGradesEnabledFlag
from
.new.course_grade
import
CourseGradeFactory
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.transformer
import
GradesTransformer
from
.transformer
import
GradesTransformer
...
@@ -40,17 +38,19 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
...
@@ -40,17 +38,19 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
- raw_possible: the max raw points the leaner could have earned
- raw_possible: the max raw points the leaner could have earned
on the problem
on the problem
"""
"""
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_id
):
course_key
=
CourseLocator
.
from_string
(
course_id
)
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
return
return
course_key
=
CourseLocator
.
from_string
(
course_id
)
scored_block_usage_key
=
UsageKey
.
from_string
(
usage_id
)
.
replace
(
course_key
=
course_key
)
scored_block_usage_key
=
UsageKey
.
from_string
(
usage_id
)
.
replace
(
course_key
=
course_key
)
score
=
get_score
(
user_id
,
scored_block_usage_key
)
score
=
get_score
(
user_id
,
scored_block_usage_key
)
# If the score is None, it has not been saved at all yet
# If the score is None, it has not been saved at all yet
# and we need to retry until it has been saved.
# and we need to retry until it has been saved.
if
score
is
None
:
if
score
is
None
:
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
)
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
,
)
else
:
else
:
module_raw_earned
,
module_raw_possible
=
score
# pylint: disable=unpacking-non-sequence
module_raw_earned
,
module_raw_possible
=
score
# pylint: disable=unpacking-non-sequence
...
@@ -65,7 +65,9 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
...
@@ -65,7 +65,9 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
state_deleted
=
module_raw_earned
is
None
and
module_raw_possible
is
None
and
raw_earned
==
0
state_deleted
=
module_raw_earned
is
None
and
module_raw_possible
is
None
and
raw_earned
==
0
if
not
(
state_deleted
or
grades_match
):
if
not
(
state_deleted
or
grades_match
):
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
)
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
,
)
_update_subsection_grades
(
_update_subsection_grades
(
course_key
,
course_key
,
...
@@ -79,44 +81,6 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
...
@@ -79,44 +81,6 @@ def recalculate_subsection_grade(user_id, course_id, usage_id, only_if_higher, r
)
)
@task
(
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
def
recalculate_course_grade
(
user_id
,
course_id
):
"""
Updates a saved course grade.
This method expects the following parameters:
- user_id: serialized id of applicable User object
- course_id: Unicode string representing the course
"""
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_id
):
return
student
=
User
.
objects
.
get
(
id
=
user_id
)
course_key
=
CourseLocator
.
from_string
(
course_id
)
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
try
:
CourseGradeFactory
(
student
)
.
update
(
course
)
except
IntegrityError
as
exc
:
raise
recalculate_course_grade
.
retry
(
args
=
[
user_id
,
course_id
],
exc
=
exc
)
def
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
grade
,
max_grade
,
exc
=
None
):
"""
Calls retry for the recalculate_subsection_grade task with the
given inputs.
"""
raise
recalculate_subsection_grade
.
retry
(
args
=
[
user_id
,
course_id
,
usage_id
,
only_if_higher
,
grade
,
max_grade
,
],
exc
=
exc
)
def
_update_subsection_grades
(
def
_update_subsection_grades
(
course_key
,
course_key
,
scored_block_usage_key
,
scored_block_usage_key
,
...
@@ -132,35 +96,51 @@ def _update_subsection_grades(
...
@@ -132,35 +96,51 @@ def _update_subsection_grades(
for each subsection containing the given block, and to signal
for each subsection containing the given block, and to signal
that those subsection grades were updated.
that those subsection grades were updated.
"""
"""
collected_block_structure
=
get_course_in_cache
(
course_key
)
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
student
=
User
.
objects
.
get
(
id
=
user_id
)
student
=
User
.
objects
.
get
(
id
=
user_id
)
subsection_grade_factory
=
SubsectionGradeFactory
(
student
,
course
,
collected_block_structure
)
course_structure
=
get_course_blocks
(
student
,
modulestore
()
.
make_course_usage_key
(
course_key
)
)
subsections_to_update
=
co
llected_block
_structure
.
get_transformer_block_field
(
subsections_to_update
=
co
urse
_structure
.
get_transformer_block_field
(
scored_block_usage_key
,
scored_block_usage_key
,
GradesTransformer
,
GradesTransformer
,
'subsections'
,
'subsections'
,
set
()
set
()
,
)
)
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
subsection_grade_factory
=
SubsectionGradeFactory
(
student
,
course
,
course_structure
)
try
:
try
:
for
subsection_usage_key
in
subsections_to_update
:
for
subsection_usage_key
in
subsections_to_update
:
transformed_subsection_structure
=
get_course_blocks
(
student
,
subsection_usage_key
,
collected_block_structure
=
collected_block_structure
,
)
subsection_grade
=
subsection_grade_factory
.
update
(
subsection_grade
=
subsection_grade_factory
.
update
(
transformed_subsection_structure
[
subsection_usage_key
],
course_structure
[
subsection_usage_key
],
transformed_subsection_structure
,
only_if_higher
,
only_if_higher
,
)
)
SUBSECTION_SCORE_CHANGED
.
send
(
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
recalculate_subsection_grade
,
sender
=
recalculate_subsection_grade
,
course
=
course
,
course
=
course
,
course_structure
=
course_structure
,
user
=
student
,
user
=
student
,
subsection_grade
=
subsection_grade
,
subsection_grade
=
subsection_grade
,
)
)
except
IntegrityError
as
exc
:
except
IntegrityError
as
exc
:
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
,
exc
)
raise
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
raw_earned
,
raw_possible
,
exc
,
)
def
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
grade
,
max_grade
,
exc
=
None
):
"""
Calls retry for the recalculate_subsection_grade task with the
given inputs.
"""
recalculate_subsection_grade
.
retry
(
args
=
[
user_id
,
course_id
,
usage_id
,
only_if_higher
,
grade
,
max_grade
,
],
exc
=
exc
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
266bfb1a
...
@@ -21,7 +21,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
...
@@ -21,7 +21,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
recalculate_
course_grade
,
recalculate_
subsection_grade
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
...
@@ -85,7 +85,6 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -85,7 +85,6 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
@ddt.data
(
@ddt.data
(
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
PROBLEM_SCORE_CHANGED
),
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.apply_async'
,
PROBLEM_SCORE_CHANGED
),
(
'lms.djangoapps.grades.tasks.recalculate_course_grade.apply_async'
,
SUBSECTION_SCORE_CHANGED
)
)
)
@ddt.unpack
@ddt.unpack
def
test_signal_queues_task
(
self
,
enqueue_op
,
test_signal
):
def
test_signal_queues_task
(
self
,
enqueue_op
,
test_signal
):
...
@@ -115,52 +114,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -115,52 +114,8 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
Ensures that the subsection update operation also updates the course grade.
Ensures that the subsection update operation also updates the course grade.
"""
"""
self
.
set_up_course
()
self
.
set_up_course
()
mock_update_return
=
uuid4
()
self
.
_apply_recalculate_subsection_grade
()
self
.
assertTrue
(
mock_course_signal
.
called
)
course_key
=
CourseLocator
.
from_string
(
unicode
(
self
.
course
.
id
))
course
=
modulestore
()
.
get_course
(
course_key
,
depth
=
0
)
with
patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
,
return_value
=
mock_update_return
):
self
.
_apply_recalculate_subsection_grade
()
mock_course_signal
.
assert_called_once_with
(
sender
=
recalculate_subsection_grade
,
course
=
course
,
user
=
self
.
user
,
subsection_grade
=
mock_update_return
,
)
@ddt.data
(
True
,
False
)
def
test_course_update_enqueuing
(
self
,
should_be_async
):
"""
Ensures that the course update operation is enqueued on an async queue (or not) as expected.
"""
base
=
'lms.djangoapps.grades.tasks.recalculate_course_grade'
if
should_be_async
:
executed
=
base
+
'.apply_async'
other
=
base
+
'.apply'
sender
=
None
else
:
executed
=
base
+
'.apply'
other
=
base
+
'.apply_async'
sender
=
recalculate_subsection_grade
self
.
set_up_course
()
with
patch
(
executed
)
as
executed_task
:
with
patch
(
other
)
as
other_task
:
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
sender
,
course
=
self
.
course
,
user
=
self
.
user
,
)
other_task
.
assert_not_called
()
executed_task
.
assert_called_once_with
(
args
=
(
self
.
problem_score_changed_kwargs
[
'user_id'
],
self
.
problem_score_changed_kwargs
[
'course_id'
],
)
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
...
@@ -171,7 +126,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -171,7 +126,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
2
5
+
added_queries
):
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
2
2
+
added_queries
):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
def
test_single_call_to_create_block_structure
(
self
):
def
test_single_call_to_create_block_structure
(
self
):
...
@@ -182,7 +137,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -182,7 +137,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
return_value
=
None
,
return_value
=
None
,
)
as
mock_block_structure_create
:
)
as
mock_block_structure_create
:
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
2
)
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
),
...
@@ -195,7 +150,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -195,7 +150,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
2
5
+
added_queries
):
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
2
2
+
added_queries
):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
...
@@ -232,22 +187,6 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -232,22 +187,6 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
self
.
assertTrue
(
mock_retry
.
called
)
self
.
assertTrue
(
mock_retry
.
called
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_course_grade.retry'
)
@patch
(
'lms.djangoapps.grades.new.course_grade.CourseGradeFactory.update'
)
def
test_retry_course_update_on_integrity_error
(
self
,
mock_update
,
mock_retry
):
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
self
.
set_up_course
()
mock_update
.
side_effect
=
IntegrityError
(
"WHAMMY"
)
recalculate_course_grade
.
apply
(
args
=
(
self
.
problem_score_changed_kwargs
[
'user_id'
],
self
.
problem_score_changed_kwargs
[
'course_id'
],
)
)
self
.
assertTrue
(
mock_retry
.
called
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade.retry'
)
def
test_retry_subsection_grade_on_update_not_complete
(
self
,
mock_retry
):
def
test_retry_subsection_grade_on_update_not_complete
(
self
,
mock_retry
):
self
.
set_up_course
()
self
.
set_up_course
()
...
...
openedx/core/djangoapps/performance/utils.py
0 → 100644
View file @
266bfb1a
"""
Common utilities for performance testing.
"""
from
contextlib
import
contextmanager
def
collect_profile_func
(
file_prefix
,
enabled
=
False
):
"""
Method decorator for collecting profile.
"""
import
functools
def
_outer
(
func
):
"""
Outer function decorator.
"""
@functools.wraps
(
func
)
def
_inner
(
self
,
*
args
,
**
kwargs
):
"""
Inner wrapper function.
"""
if
enabled
:
with
collect_profile
(
file_prefix
):
return
func
(
self
,
*
args
,
**
kwargs
)
else
:
return
func
(
self
,
*
args
,
**
kwargs
)
return
_inner
return
_outer
@contextmanager
def
collect_profile
(
file_prefix
):
"""
Context manager to collect profile information.
"""
import
cProfile
import
uuid
profiler
=
cProfile
.
Profile
()
profiler
.
enable
()
try
:
yield
finally
:
profiler
.
disable
()
profiler
.
dump_stats
(
"{0}_{1}_master.profile"
.
format
(
file_prefix
,
uuid
.
uuid4
()))
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