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
5a9179bf
Commit
5a9179bf
authored
Jan 22, 2017
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update has_database_updated_with_new_score to handle all block types
TNL-6331
parent
b83ed6ef
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
252 additions
and
200 deletions
+252
-200
lms/djangoapps/courseware/tests/test_module_render.py
+2
-1
lms/djangoapps/grades/constants.py
+11
-0
lms/djangoapps/grades/signals/handlers.py
+11
-2
lms/djangoapps/grades/signals/signals.py
+4
-0
lms/djangoapps/grades/tasks.py
+72
-53
lms/djangoapps/grades/tests/test_signals.py
+23
-24
lms/djangoapps/grades/tests/test_tasks.py
+112
-104
lms/djangoapps/instructor/enrollment.py
+17
-16
No files found.
lms/djangoapps/courseware/tests/test_module_render.py
View file @
5a9179bf
...
@@ -1848,7 +1848,8 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
...
@@ -1848,7 +1848,8 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
'course_id'
:
unicode
(
self
.
course
.
id
),
'course_id'
:
unicode
(
self
.
course
.
id
),
'usage_id'
:
unicode
(
self
.
problem
.
location
),
'usage_id'
:
unicode
(
self
.
problem
.
location
),
'only_if_higher'
:
None
,
'only_if_higher'
:
None
,
'modified'
:
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
'modified'
:
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
'score_db_table'
:
'csm'
,
}
}
send_mock
.
assert_called_with
(
**
expected_signal_kwargs
)
send_mock
.
assert_called_with
(
**
expected_signal_kwargs
)
...
...
lms/djangoapps/grades/constants.py
0 → 100644
View file @
5a9179bf
"""
Constants and Enums used by Grading.
"""
class
ScoreDatabaseTableEnum
(
object
):
"""
The various database tables that store scores.
"""
courseware_student_module
=
'csm'
submissions
=
'submissions'
lms/djangoapps/grades/signals/handlers.py
View file @
5a9179bf
...
@@ -24,9 +24,10 @@ from .signals import (
...
@@ -24,9 +24,10 @@ from .signals import (
SUBSECTION_SCORE_CHANGED
,
SUBSECTION_SCORE_CHANGED
,
SCORE_PUBLISHED
,
SCORE_PUBLISHED
,
)
)
from
..constants
import
ScoreDatabaseTableEnum
from
..new.course_grade
import
CourseGradeFactory
from
..new.course_grade
import
CourseGradeFactory
from
..scores
import
weighted_score
from
..scores
import
weighted_score
from
..tasks
import
recalculate_subsection_grade_v
2
from
..tasks
import
recalculate_subsection_grade_v
3
log
=
getLogger
(
__name__
)
log
=
getLogger
(
__name__
)
...
@@ -62,9 +63,11 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
...
@@ -62,9 +63,11 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
weighted_earned
=
points_earned
,
weighted_earned
=
points_earned
,
weighted_possible
=
points_possible
,
weighted_possible
=
points_possible
,
user_id
=
user
.
id
,
user_id
=
user
.
id
,
anonymous_user_id
=
kwargs
[
'anonymous_user_id'
],
course_id
=
course_id
,
course_id
=
course_id
,
usage_id
=
usage_id
,
usage_id
=
usage_id
,
modified
=
kwargs
[
'created_at'
],
modified
=
kwargs
[
'created_at'
],
score_db_table
=
ScoreDatabaseTableEnum
.
submissions
,
)
)
...
@@ -93,9 +96,11 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
...
@@ -93,9 +96,11 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
weighted_earned
=
0
,
weighted_earned
=
0
,
weighted_possible
=
0
,
weighted_possible
=
0
,
user_id
=
user
.
id
,
user_id
=
user
.
id
,
anonymous_user_id
=
kwargs
[
'anonymous_user_id'
],
course_id
=
course_id
,
course_id
=
course_id
,
usage_id
=
usage_id
,
usage_id
=
usage_id
,
modified
=
kwargs
[
'created_at'
],
modified
=
kwargs
[
'created_at'
],
score_db_table
=
ScoreDatabaseTableEnum
.
submissions
,
)
)
...
@@ -133,6 +138,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
...
@@ -133,6 +138,7 @@ def score_published_handler(sender, block, user, raw_earned, raw_possible, only_
usage_id
=
unicode
(
block
.
location
),
usage_id
=
unicode
(
block
.
location
),
only_if_higher
=
only_if_higher
,
only_if_higher
=
only_if_higher
,
modified
=
score_modified_time
,
modified
=
score_modified_time
,
score_db_table
=
ScoreDatabaseTableEnum
.
courseware_student_module
,
)
)
return
update_score
return
update_score
...
@@ -162,6 +168,7 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
...
@@ -162,6 +168,7 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
only_if_higher
=
kwargs
[
'only_if_higher'
],
only_if_higher
=
kwargs
[
'only_if_higher'
],
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
modified
=
kwargs
[
'modified'
],
modified
=
kwargs
[
'modified'
],
score_db_table
=
kwargs
[
'score_db_table'
],
)
)
...
@@ -172,9 +179,10 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
...
@@ -172,9 +179,10 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
enqueueing a subsection update operation to occur asynchronously.
enqueueing a subsection update operation to occur asynchronously.
"""
"""
_emit_problem_submitted_event
(
kwargs
)
_emit_problem_submitted_event
(
kwargs
)
result
=
recalculate_subsection_grade_v
2
.
apply_async
(
result
=
recalculate_subsection_grade_v
3
.
apply_async
(
kwargs
=
dict
(
kwargs
=
dict
(
user_id
=
kwargs
[
'user_id'
],
user_id
=
kwargs
[
'user_id'
],
anonymous_user_id
=
kwargs
.
get
(
'anonymous_user_id'
),
course_id
=
kwargs
[
'course_id'
],
course_id
=
kwargs
[
'course_id'
],
usage_id
=
kwargs
[
'usage_id'
],
usage_id
=
kwargs
[
'usage_id'
],
only_if_higher
=
kwargs
.
get
(
'only_if_higher'
),
only_if_higher
=
kwargs
.
get
(
'only_if_higher'
),
...
@@ -182,6 +190,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
...
@@ -182,6 +190,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
score_db_table
=
kwargs
[
'score_db_table'
],
)
)
)
)
log
.
info
(
log
.
info
(
...
...
lms/djangoapps/grades/signals/signals.py
View file @
5a9179bf
...
@@ -22,6 +22,7 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
...
@@ -22,6 +22,7 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
# made only if the new score is higher than previous.
# made only if the new score is higher than previous.
'modified'
,
# A datetime indicating when the database representation of
'modified'
,
# A datetime indicating when the database representation of
# this the problem score was saved.
# this the problem score was saved.
'score_db_table'
,
# The database table that houses the score that changed.
]
]
)
)
...
@@ -35,6 +36,7 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
...
@@ -35,6 +36,7 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
PROBLEM_WEIGHTED_SCORE_CHANGED
=
Signal
(
PROBLEM_WEIGHTED_SCORE_CHANGED
=
Signal
(
providing_args
=
[
providing_args
=
[
'user_id'
,
# Integer User ID
'user_id'
,
# Integer User ID
'anonymous_user_id'
,
# Anonymous User ID
'course_id'
,
# Unicode string representing the course
'course_id'
,
# Unicode string representing the course
'usage_id'
,
# Unicode string indicating the courseware instance
'usage_id'
,
# Unicode string indicating the courseware instance
'weighted_earned'
,
# Score obtained by the user
'weighted_earned'
,
# Score obtained by the user
...
@@ -43,6 +45,7 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
...
@@ -43,6 +45,7 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
# made only if the new score is higher than previous.
# made only if the new score is higher than previous.
'modified'
,
# A datetime indicating when the database representation of
'modified'
,
# A datetime indicating when the database representation of
# this the problem score was saved.
# this the problem score was saved.
'score_db_table'
,
# The database table that houses the score that changed.
]
]
)
)
...
@@ -59,6 +62,7 @@ SCORE_PUBLISHED = Signal(
...
@@ -59,6 +62,7 @@ SCORE_PUBLISHED = Signal(
'raw_possible'
,
# Maximum score available for the exercise
'raw_possible'
,
# Maximum score available for the exercise
'only_if_higher'
,
# Boolean indicating whether updates should be
'only_if_higher'
,
# Boolean indicating whether updates should be
# made only if the new score is higher than previous.
# made only if the new score is higher than previous.
'score_db_table'
,
# The database table that houses the score that changed.
]
]
)
)
...
...
lms/djangoapps/grades/tasks.py
View file @
5a9179bf
...
@@ -26,6 +26,7 @@ from util.date_utils import from_timestamp
...
@@ -26,6 +26,7 @@ from util.date_utils import from_timestamp
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
from
.config.models
import
PersistentGradesEnabledFlag
from
.config.models
import
PersistentGradesEnabledFlag
from
.constants
import
ScoreDatabaseTableEnum
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
...
@@ -35,33 +36,31 @@ log = getLogger(__name__)
...
@@ -35,33 +36,31 @@ log = getLogger(__name__)
KNOWN_RETRY_ERRORS
=
(
DatabaseError
,
ValidationError
)
# Errors we expect occasionally, should be resolved on retry
KNOWN_RETRY_ERRORS
=
(
DatabaseError
,
ValidationError
)
# Errors we expect occasionally, should be resolved on retry
@task
(
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
# TODO (TNL-6373) DELETE ME once v3 is successfully deployed to Prod.
def
recalculate_subsection_grade
(
@task
(
base
=
PersistOnFailureTask
,
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
# pylint: disable=unused-argument
def
recalculate_subsection_grade_v2
(
**
kwargs
):
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
**
kwargs
):
"""
"""
Shim to
allow us to modify this task's signature without blowing up production on
deployment.
Shim to
support tasks enqueued by older workers during initial
deployment.
"""
"""
recalculate_subsection_grade_v2
.
apply
(
_recalculate_subsection_grade
(
recalculate_subsection_grade_v2
,
**
kwargs
)
kwargs
=
dict
(
user_id
=
user_id
,
course_id
=
course_id
,
usage_id
=
usage_id
,
only_if_higher
=
only_if_higher
,
expected_modified_time
=
kwargs
.
get
(
'expected_modified_time'
,
0
),
# Use the unix epoch as a default
score_deleted
=
kwargs
[
'score_deleted'
],
)
)
@task
(
base
=
PersistOnFailureTask
,
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
@task
(
base
=
PersistOnFailureTask
,
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
def
recalculate_subsection_grade_v2
(
**
kwargs
):
def
recalculate_subsection_grade_v3
(
**
kwargs
):
"""
Latest version of the recalculate_subsection_grade task. See docstring
for _recalculate_subsection_grade for further description.
"""
_recalculate_subsection_grade
(
recalculate_subsection_grade_v3
,
**
kwargs
)
def
_recalculate_subsection_grade
(
task_func
,
**
kwargs
):
"""
"""
Updates a saved subsection grade.
Updates a saved subsection grade.
Arguments:
Keyword
Arguments:
user_id (int): id of applicable User object
user_id (int): id of applicable User object
anonymous_user_id (int, OPTIONAL): Anonymous ID of the User
course_id (string): identifying the course
course_id (string): identifying the course
usage_id (string): identifying the course block
usage_id (string): identifying the course block
only_if_higher (boolean): indicating whether grades should
only_if_higher (boolean): indicating whether grades should
...
@@ -71,36 +70,44 @@ def recalculate_subsection_grade_v2(**kwargs):
...
@@ -71,36 +70,44 @@ def recalculate_subsection_grade_v2(**kwargs):
was queued so that we can verify the underlying data update.
was queued so that we can verify the underlying data update.
score_deleted (boolean): indicating whether the grade change is
score_deleted (boolean): indicating whether the grade change is
a result of the problem's score being deleted.
a result of the problem's score being deleted.
event_transaction_id(string): uuid identifying the current
event_transaction_id
(string): uuid identifying the current
event transaction.
event transaction.
event_transaction_type(string): human-readable type of the
event_transaction_type
(string): human-readable type of the
event at the root of the current event transaction.
event at the root of the current event transaction.
score_db_table (ScoreDatabaseTableEnum): database table that houses
the changed score. Used in conjunction with expected_modified_time.
"""
"""
try
:
try
:
course_key
=
CourseLocator
.
from_string
(
kwargs
[
'course_id'
])
course_key
=
CourseLocator
.
from_string
(
kwargs
[
'course_id'
])
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
return
return
score_deleted
=
kwargs
[
'score_deleted'
]
scored_block_usage_key
=
UsageKey
.
from_string
(
kwargs
[
'usage_id'
])
.
replace
(
course_key
=
course_key
)
scored_block_usage_key
=
UsageKey
.
from_string
(
kwargs
[
'usage_id'
])
.
replace
(
course_key
=
course_key
)
expected_modified_time
=
from_timestamp
(
kwargs
[
'expected_modified_time'
])
# The request cache is not maintained on celery workers,
# The request cache is not maintained on celery workers,
# where this code runs. So we take the values from the
# where this code runs. So we take the values from the
# main request cache and store them in the local request
# main request cache and store them in the local request
# cache. This correlates model-level grading events with
# cache. This correlates model-level grading events with
# higher-level ones.
# higher-level ones.
set_event_transaction_id
(
kwargs
.
pop
(
'event_transaction_id'
,
None
))
set_event_transaction_id
(
kwargs
.
get
(
'event_transaction_id'
))
set_event_transaction_type
(
kwargs
.
pop
(
'event_transaction_type'
,
None
))
set_event_transaction_type
(
kwargs
.
get
(
'event_transaction_type'
))
# Verify the database has been updated with the scores when the task was
# Verify the database has been updated with the scores when the task was
# created. This race condition occurs if the transaction in the task
# created. This race condition occurs if the transaction in the task
# creator's process hasn't committed before the task initiates in the worker
# creator's process hasn't committed before the task initiates in the worker
# process.
# process.
if
not
_has_database_updated_with_new_score
(
if
task_func
==
recalculate_subsection_grade_v2
:
kwargs
[
'user_id'
],
scored_block_usage_key
,
expected_modified_time
,
score_deleted
,
has_database_updated
=
_has_db_updated_with_new_score_bwc_v2
(
):
kwargs
[
'user_id'
],
raise
_retry_recalculate_subsection_grade
(
**
kwargs
)
scored_block_usage_key
,
from_timestamp
(
kwargs
[
'expected_modified_time'
]),
kwargs
[
'score_deleted'
],
)
else
:
has_database_updated
=
_has_db_updated_with_new_score
(
scored_block_usage_key
,
**
kwargs
)
if
not
has_database_updated
:
raise
_retry_recalculate_subsection_grade
(
task_func
,
**
kwargs
)
_update_subsection_grades
(
_update_subsection_grades
(
course_key
,
course_key
,
...
@@ -115,13 +122,45 @@ def recalculate_subsection_grade_v2(**kwargs):
...
@@ -115,13 +122,45 @@ def recalculate_subsection_grade_v2(**kwargs):
repr
(
exc
),
repr
(
exc
),
kwargs
kwargs
))
))
raise
_retry_recalculate_subsection_grade
(
exc
=
exc
,
**
kwargs
)
raise
_retry_recalculate_subsection_grade
(
task_func
,
exc
=
exc
,
**
kwargs
)
def
_has_database_updated_with_new_score
(
def
_has_db_updated_with_new_score
(
scored_block_usage_key
,
**
kwargs
):
"""
Returns whether the database has been updated with the
expected new score values for the given problem and user.
"""
if
kwargs
[
'score_db_table'
]
==
ScoreDatabaseTableEnum
.
courseware_student_module
:
score
=
get_score
(
kwargs
[
'user_id'
],
scored_block_usage_key
)
found_modified_time
=
score
.
modified
if
score
is
not
None
else
None
else
:
assert
kwargs
[
'score_db_table'
]
==
ScoreDatabaseTableEnum
.
submissions
score
=
sub_api
.
get_score
(
{
"student_id"
:
kwargs
[
'anonymous_user_id'
],
"course_id"
:
unicode
(
scored_block_usage_key
.
course_key
),
"item_id"
:
unicode
(
scored_block_usage_key
),
"item_type"
:
scored_block_usage_key
.
block_type
,
}
)
found_modified_time
=
score
[
'created_at'
]
if
score
is
not
None
else
None
if
score
is
None
:
# score should be None only if it was deleted.
# Otherwise, it hasn't yet been saved.
return
kwargs
[
'score_deleted'
]
return
found_modified_time
>=
from_timestamp
(
kwargs
[
'expected_modified_time'
])
# TODO (TNL-6373) DELETE ME once v3 is successfully deployed to Prod.
def
_has_db_updated_with_new_score_bwc_v2
(
user_id
,
scored_block_usage_key
,
expected_modified_time
,
score_deleted
,
user_id
,
scored_block_usage_key
,
expected_modified_time
,
score_deleted
,
):
):
"""
"""
DEPRECATED version for backward compatibility with v2 tasks.
Returns whether the database has been updated with the
Returns whether the database has been updated with the
expected new score values for the given problem and user.
expected new score values for the given problem and user.
"""
"""
...
@@ -186,7 +225,7 @@ def _update_subsection_grades(
...
@@ -186,7 +225,7 @@ def _update_subsection_grades(
only_if_higher
,
only_if_higher
,
)
)
SUBSECTION_SCORE_CHANGED
.
send
(
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
recalculate_subsection_grad
e
,
sender
=
Non
e
,
course
=
course
,
course
=
course
,
course_structure
=
course_structure
,
course_structure
=
course_structure
,
user
=
student
,
user
=
student
,
...
@@ -194,29 +233,9 @@ def _update_subsection_grades(
...
@@ -194,29 +233,9 @@ def _update_subsection_grades(
)
)
def
_retry_recalculate_subsection_grade
(
def
_retry_recalculate_subsection_grade
(
task_func
,
exc
=
None
,
**
kwargs
):
user_id
,
course_id
,
usage_id
,
only_if_higher
,
expected_modified_time
,
score_deleted
,
exc
=
None
,
):
"""
"""
Calls retry for the recalculate_subsection_grade task with the
Calls retry for the recalculate_subsection_grade task with the
given inputs.
given inputs.
"""
"""
recalculate_subsection_grade_v2
.
retry
(
task_func
.
retry
(
kwargs
=
kwargs
,
exc
=
exc
)
kwargs
=
dict
(
user_id
=
user_id
,
course_id
=
course_id
,
usage_id
=
usage_id
,
only_if_higher
=
only_if_higher
,
expected_modified_time
=
expected_modified_time
,
score_deleted
=
score_deleted
,
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
),
exc
=
exc
,
)
lms/djangoapps/grades/tests/test_signals.py
View file @
5a9179bf
...
@@ -11,6 +11,7 @@ from mock import patch, MagicMock
...
@@ -11,6 +11,7 @@ from mock import patch, MagicMock
import
pytz
import
pytz
from
util.date_utils
import
to_timestamp
from
util.date_utils
import
to_timestamp
from
..constants
import
ScoreDatabaseTableEnum
from
..signals.handlers
import
(
from
..signals.handlers
import
(
enqueue_subsection_update
,
enqueue_subsection_update
,
submissions_score_set_handler
,
submissions_score_set_handler
,
...
@@ -32,7 +33,6 @@ SUBMISSION_SET_KWARGS = {
...
@@ -32,7 +33,6 @@ SUBMISSION_SET_KWARGS = {
'created_at'
:
FROZEN_NOW_TIMESTAMP
,
'created_at'
:
FROZEN_NOW_TIMESTAMP
,
}
}
SUBMISSION_RESET_KWARGS
=
{
SUBMISSION_RESET_KWARGS
=
{
'anonymous_user_id'
:
'anonymous_id'
,
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
'course_id'
:
'CourseID'
,
...
@@ -50,6 +50,20 @@ PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
...
@@ -50,6 +50,20 @@ PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
'only_if_higher'
:
False
,
'only_if_higher'
:
False
,
'score_deleted'
:
True
,
'score_deleted'
:
True
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'score_db_table'
:
ScoreDatabaseTableEnum
.
courseware_student_module
,
}
PROBLEM_WEIGHTED_SCORE_CHANGED_KWARGS
=
{
'sender'
:
None
,
'weighted_earned'
:
2.0
,
'weighted_possible'
:
4.0
,
'user_id'
:
'UserID'
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'only_if_higher'
:
False
,
'score_deleted'
:
True
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'score_db_table'
:
ScoreDatabaseTableEnum
.
courseware_student_module
,
}
}
...
@@ -110,9 +124,11 @@ class ScoreChangedSignalRelayTest(TestCase):
...
@@ -110,9 +124,11 @@ class ScoreChangedSignalRelayTest(TestCase):
'weighted_possible'
:
possible
,
'weighted_possible'
:
possible
,
'weighted_earned'
:
earned
,
'weighted_earned'
:
earned
,
'user_id'
:
self
.
user_mock
.
id
,
'user_id'
:
self
.
user_mock
.
id
,
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'score_db_table'
:
'submissions'
,
}
}
self
.
signal_mock
.
assert_called_once_with
(
**
expected_set_kwargs
)
self
.
signal_mock
.
assert_called_once_with
(
**
expected_set_kwargs
)
self
.
get_user_mock
.
assert_called_once_with
(
kwargs
[
'anonymous_user_id'
])
self
.
get_user_mock
.
assert_called_once_with
(
kwargs
[
'anonymous_user_id'
])
...
@@ -153,44 +169,26 @@ class ScoreChangedSignalRelayTest(TestCase):
...
@@ -153,44 +169,26 @@ class ScoreChangedSignalRelayTest(TestCase):
def
test_raw_score_changed_signal_handler
(
self
):
def
test_raw_score_changed_signal_handler
(
self
):
problem_raw_score_changed_handler
(
None
,
**
PROBLEM_RAW_SCORE_CHANGED_KWARGS
)
problem_raw_score_changed_handler
(
None
,
**
PROBLEM_RAW_SCORE_CHANGED_KWARGS
)
expected_set_kwargs
=
{
expected_set_kwargs
=
PROBLEM_WEIGHTED_SCORE_CHANGED_KWARGS
.
copy
()
'sender'
:
None
,
'weighted_earned'
:
2.0
,
'weighted_possible'
:
4.0
,
'user_id'
:
'UserID'
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'only_if_higher'
:
False
,
'score_deleted'
:
True
,
'modified'
:
FROZEN_NOW_TIMESTAMP
}
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
def
test_raw_score_changed_score_deleted_optional
(
self
):
def
test_raw_score_changed_score_deleted_optional
(
self
):
local_kwargs
=
PROBLEM_RAW_SCORE_CHANGED_KWARGS
.
copy
()
local_kwargs
=
PROBLEM_RAW_SCORE_CHANGED_KWARGS
.
copy
()
del
local_kwargs
[
'score_deleted'
]
del
local_kwargs
[
'score_deleted'
]
problem_raw_score_changed_handler
(
None
,
**
local_kwargs
)
problem_raw_score_changed_handler
(
None
,
**
local_kwargs
)
expected_set_kwargs
=
{
expected_set_kwargs
=
PROBLEM_WEIGHTED_SCORE_CHANGED_KWARGS
.
copy
()
'sender'
:
None
,
expected_set_kwargs
[
'score_deleted'
]
=
False
'weighted_earned'
:
2.0
,
'weighted_possible'
:
4.0
,
'user_id'
:
'UserID'
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'only_if_higher'
:
False
,
'score_deleted'
:
False
,
'modified'
:
FROZEN_NOW_TIMESTAMP
}
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
@patch
(
'lms.djangoapps.grades.signals.handlers.log.info'
)
@patch
(
'lms.djangoapps.grades.signals.handlers.log.info'
)
def
test_
problem_score_changed
_logging
(
self
,
mocklog
):
def
test_
subsection_update
_logging
(
self
,
mocklog
):
enqueue_subsection_update
(
enqueue_subsection_update
(
sender
=
'test'
,
sender
=
'test'
,
user_id
=
1
,
user_id
=
1
,
course_id
=
u'course-v1:edX+Demo_Course+DemoX'
,
course_id
=
u'course-v1:edX+Demo_Course+DemoX'
,
usage_id
=
u'block-v1:block-key'
,
usage_id
=
u'block-v1:block-key'
,
modified
=
FROZEN_NOW_DATETIME
,
modified
=
FROZEN_NOW_DATETIME
,
score_db_table
=
ScoreDatabaseTableEnum
.
courseware_student_module
,
)
)
log_statement
=
mocklog
.
call_args
[
0
][
0
]
log_statement
=
mocklog
.
call_args
[
0
][
0
]
log_statement
=
UUID_REGEX
.
sub
(
u'*UUID*'
,
log_statement
)
log_statement
=
UUID_REGEX
.
sub
(
u'*UUID*'
,
log_statement
)
...
@@ -199,6 +197,7 @@ class ScoreChangedSignalRelayTest(TestCase):
...
@@ -199,6 +197,7 @@ class ScoreChangedSignalRelayTest(TestCase):
(
(
u'Grades: Request async calculation of subsection grades with args: '
u'Grades: Request async calculation of subsection grades with args: '
u'course_id:course-v1:edX+Demo_Course+DemoX, modified:{time}, '
u'course_id:course-v1:edX+Demo_Course+DemoX, modified:{time}, '
u'score_db_table:csm, '
u'usage_id:block-v1:block-key, user_id:1. Task [*UUID*]'
u'usage_id:block-v1:block-key, user_id:1. Task [*UUID*]'
)
.
format
(
time
=
FROZEN_NOW_DATETIME
)
)
.
format
(
time
=
FROZEN_NOW_DATETIME
)
)
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
5a9179bf
...
@@ -8,10 +8,10 @@ from datetime import datetime, timedelta
...
@@ -8,10 +8,10 @@ from datetime import datetime, timedelta
import
ddt
import
ddt
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db.utils
import
IntegrityError
from
django.db.utils
import
IntegrityError
import
itertools
from
mock
import
patch
,
MagicMock
from
mock
import
patch
,
MagicMock
import
pytz
import
pytz
from
util.date_utils
import
to_timestamp
from
util.date_utils
import
to_timestamp
from
unittest
import
skip
from
student.models
import
anonymous_id_for_user
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
...
@@ -25,8 +25,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...
@@ -25,8 +25,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.config.models
import
PersistentGradesEnabledFlag
from
lms.djangoapps.grades.constants
import
ScoreDatabaseTableEnum
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_WEIGHTED_SCORE_CHANGED
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade_v
2
from
lms.djangoapps.grades.tasks
import
recalculate_subsection_grade_v
3
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
@patch.dict
(
settings
.
FEATURES
,
{
'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS'
:
False
})
...
@@ -40,7 +41,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -40,7 +41,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
user
=
UserFactory
()
self
.
user
=
UserFactory
()
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled_for_all_courses
=
True
,
enabled
=
True
)
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled_for_all_courses
=
True
,
enabled
=
True
)
def
set_up_course
(
self
,
enable_
subsection
_grades
=
True
):
def
set_up_course
(
self
,
enable_
persistent
_grades
=
True
):
"""
"""
Configures the course for this test.
Configures the course for this test.
"""
"""
...
@@ -50,7 +51,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -50,7 +51,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
name
=
'course'
,
name
=
'course'
,
run
=
'run'
,
run
=
'run'
,
)
)
if
not
enable_
subsection
_grades
:
if
not
enable_
persistent
_grades
:
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
False
)
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
False
)
self
.
chapter
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Chapter"
)
self
.
chapter
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Chapter"
)
...
@@ -64,10 +65,12 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -64,10 +65,12 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'weighted_earned'
,
1.0
),
(
'weighted_earned'
,
1.0
),
(
'weighted_possible'
,
2.0
),
(
'weighted_possible'
,
2.0
),
(
'user_id'
,
self
.
user
.
id
),
(
'user_id'
,
self
.
user
.
id
),
(
'anonymous_user_id'
,
5
),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'only_if_higher'
,
None
),
(
'only_if_higher'
,
None
),
(
'modified'
,
self
.
frozen_now_datetime
),
(
'modified'
,
self
.
frozen_now_datetime
),
(
'score_db_table'
,
ScoreDatabaseTableEnum
.
courseware_student_module
),
])
])
create_new_event_transaction_id
()
create_new_event_transaction_id
()
...
@@ -76,11 +79,13 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -76,11 +79,13 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'user_id'
,
self
.
user
.
id
),
(
'user_id'
,
self
.
user
.
id
),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'anonymous_user_id'
,
5
),
(
'only_if_higher'
,
None
),
(
'only_if_higher'
,
None
),
(
'expected_modified_time'
,
self
.
frozen_now_timestamp
),
(
'expected_modified_time'
,
self
.
frozen_now_timestamp
),
(
'score_deleted'
,
False
),
(
'score_deleted'
,
False
),
(
'event_transaction_id'
,
unicode
(
get_event_transaction_id
())),
(
'event_transaction_id'
,
unicode
(
get_event_transaction_id
())),
(
'event_transaction_type'
,
u'edx.grades.problem.submitted'
),
(
'event_transaction_type'
,
u'edx.grades.problem.submitted'
),
(
'score_db_table'
,
ScoreDatabaseTableEnum
.
courseware_student_module
),
])
])
# this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
# this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
...
@@ -96,7 +101,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -96,7 +101,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
patch
(
"lms.djangoapps.grades.tasks.get_score"
,
return_value
=
score
):
with
patch
(
"lms.djangoapps.grades.tasks.get_score"
,
return_value
=
score
):
yield
yield
def
test_
problem_weighted_score_changed_queues_task
(
self
):
def
test_
triggered_by_problem_weighted_score_change
(
self
):
"""
"""
Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
Ensures that the PROBLEM_WEIGHTED_SCORE_CHANGED signal enqueues the correct task.
"""
"""
...
@@ -105,27 +110,37 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -105,27 +110,37 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
local_task_args
=
self
.
recalculate_subsection_grade_kwargs
.
copy
()
local_task_args
=
self
.
recalculate_subsection_grade_kwargs
.
copy
()
local_task_args
[
'event_transaction_type'
]
=
u'edx.grades.problem.submitted'
local_task_args
[
'event_transaction_type'
]
=
u'edx.grades.problem.submitted'
with
self
.
mock_get_score
()
and
patch
(
with
self
.
mock_get_score
()
and
patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
2
.apply_async'
,
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
3
.apply_async'
,
return_value
=
None
return_value
=
None
)
as
mock_task_apply
:
)
as
mock_task_apply
:
PROBLEM_WEIGHTED_SCORE_CHANGED
.
send
(
sender
=
None
,
**
send_args
)
PROBLEM_WEIGHTED_SCORE_CHANGED
.
send
(
sender
=
None
,
**
send_args
)
mock_task_apply
.
assert_called_once_with
(
kwargs
=
local_task_args
)
mock_task_apply
.
assert_called_once_with
(
kwargs
=
local_task_args
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_
subsection_update_triggers
_signal
(
self
,
mock_subsection_signal
):
def
test_
triggers_subsection_score
_signal
(
self
,
mock_subsection_signal
):
"""
"""
Ensures that
the subsection update oper
ation triggers a signal.
Ensures that
a subsection grade recalcul
ation triggers a signal.
"""
"""
self
.
set_up_course
()
self
.
set_up_course
()
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
self
.
assertTrue
(
mock_subsection_signal
.
called
)
self
.
assertTrue
(
mock_subsection_signal
.
called
)
def
test_block_structure_created_only_once
(
self
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
patch
(
'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache'
,
return_value
=
None
,
)
as
mock_block_structure_create
:
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
23
),
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
23
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
22
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
22
),
)
)
@ddt.unpack
@ddt.unpack
def
test_
subsection_grade_updated
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
def
test_
query_counts
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
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
))
...
@@ -133,6 +148,30 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -133,6 +148,30 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
assertNumQueries
(
num_sql_calls
):
with
self
.
assertNumQueries
(
num_sql_calls
):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
# TODO (TNL-6225) Fix the number of SQL queries so they
# don't grow linearly with the number of sequentials.
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
46
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
45
),
)
@ddt.unpack
def
test_query_counts_dont_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
num_problems
=
10
for
_
in
range
(
num_problems
):
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
)
num_sequentials
=
10
for
_
in
range
(
num_sequentials
):
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
)
with
check_mongo_calls
(
num_mongo_calls
):
with
self
.
assertNumQueries
(
num_sql_calls
):
self
.
_apply_recalculate_subsection_grade
()
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
def
test_other_inaccessible_subsection
(
self
,
mock_subsection_signal
):
def
test_other_inaccessible_subsection
(
self
,
mock_subsection_signal
):
self
.
set_up_course
()
self
.
set_up_course
()
...
@@ -157,68 +196,29 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -157,68 +196,29 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
{
self
.
sequential
.
location
,
accessible_seq
.
location
},
{
self
.
sequential
.
location
,
accessible_seq
.
location
},
)
)
def
test_single_call_to_create_block_structure
(
self
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
patch
(
'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache'
,
return_value
=
None
,
)
as
mock_block_structure_create
:
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_block_structure_create
.
call_count
,
1
)
# TODO (TNL-6225) Fix the number of SQL queries so they
# don't grow linearly with the number of sequentials.
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
46
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
45
),
)
@ddt.unpack
def
test_query_count_does_not_change_with_more_content
(
self
,
default_store
,
num_mongo_calls
,
num_sql_calls
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
num_problems
=
10
for
_
in
range
(
num_problems
):
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
)
num_sequentials
=
10
for
_
in
range
(
num_sequentials
):
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
)
with
check_mongo_calls
(
num_mongo_calls
):
with
self
.
assertNumQueries
(
num_sql_calls
):
self
.
_apply_recalculate_subsection_grade
()
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_
subsection
_grades_not_enabled_on_course
(
self
,
default_store
):
def
test_
persistent
_grades_not_enabled_on_course
(
self
,
default_store
):
with
self
.
store
.
default_store
(
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_
subsection
_grades
=
False
)
self
.
set_up_course
(
enable_
persistent
_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
0
):
with
check_mongo_calls
(
0
):
with
self
.
assertNumQueries
(
0
):
with
self
.
assertNumQueries
(
0
):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
@skip
(
"Pending completion of TNL-5089"
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@ddt.data
(
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
(
ModuleStoreEnum
.
Type
.
mongo
,
True
),
def
test_retry_first_time_only
(
self
,
mock_update
,
mock_course_signal
):
(
ModuleStoreEnum
.
Type
.
split
,
True
),
"""
(
ModuleStoreEnum
.
Type
.
mongo
,
False
),
Ensures that a task retry completes after a one-time failure.
(
ModuleStoreEnum
.
Type
.
split
,
False
),
"""
)
self
.
set_up_course
()
@ddt.unpack
mock_update
.
side_effect
=
[
IntegrityError
(
"WHAMMY"
),
None
]
def
test_query_counts_with_feature_flag
(
self
,
default_store
,
feature_flag
):
self
.
_apply_recalculate_subsection_grade
()
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
feature_flag
)
self
.
assertEquals
(
mock_course_signal
.
call_count
,
1
)
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
with
check_mongo_calls
(
0
):
with
self
.
assertNumQueries
(
3
if
feature_flag
else
2
):
self
.
_apply_recalculate_subsection_grade
()
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
2
.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
3
.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_retry_
subsection_update_
on_integrity_error
(
self
,
mock_update
,
mock_retry
):
def
test_retry_on_integrity_error
(
self
,
mock_update
,
mock_retry
):
"""
"""
Ensures that tasks will be retried if IntegrityErrors are encountered.
Ensures that tasks will be retried if IntegrityErrors are encountered.
"""
"""
...
@@ -227,55 +227,57 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -227,55 +227,57 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
@ddt.data
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
)
def
test_retry_subsection_grade_on_update_not_complete
(
self
,
mock_retry
):
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry'
)
def
test_retry_when_db_not_updated
(
self
,
score_db_table
,
mock_retry
):
self
.
set_up_course
()
self
.
set_up_course
()
self
.
_apply_recalculate_subsection_grade
(
self
.
recalculate_subsection_grade_kwargs
[
'score_db_table'
]
=
score_db_table
mock_score
=
MagicMock
(
modified
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
1
))
)
modified_datetime
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
1
)
self
.
_assert_retry_called
(
mock_retry
)
if
score_db_table
==
ScoreDatabaseTableEnum
.
submissions
:
with
patch
(
'lms.djangoapps.grades.tasks.sub_api.get_score'
)
as
mock_sub_score
:
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
mock_sub_score
.
return_value
=
{
def
test_retry_subsection_grade_on_update_not_complete_sub
(
self
,
mock_retry
):
'created_at'
:
modified_datetime
self
.
set_up_course
()
}
with
patch
(
'lms.djangoapps.grades.tasks.sub_api.get_score'
)
as
mock_sub_score
:
self
.
_apply_recalculate_subsection_grade
(
mock_sub_score
.
return_value
=
{
mock_score
=
MagicMock
(
module_type
=
'any_block_type'
)
'created_at'
:
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
1
)
)
}
else
:
self
.
_apply_recalculate_subsection_grade
(
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
mod
ule_type
=
'openassessment'
)
mock_score
=
MagicMock
(
mod
ified
=
modified_datetime
)
)
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
def
test_retry_subsection_grade_on_no_score
(
self
,
mock_retry
):
self
.
set_up_course
()
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
None
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
def
test_retry_subsection_grade_on_no_sub_score
(
self
,
mock_retry
):
self
.
set_up_course
()
with
patch
(
'lms.djangoapps.grades.tasks.sub_api.get_score'
)
as
mock_sub_score
:
mock_sub_score
.
return_value
=
None
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
module_type
=
'openassessment'
)
)
self
.
_assert_retry_called
(
mock_retry
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@ddt.data
(
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
*
itertools
.
product
(
def
test_retry_first_time_only
(
self
,
mock_update
,
mock_course_signal
):
(
True
,
False
),
"""
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
),
Ensures that a task retry completes after a one-time failure.
)
"""
)
@ddt.unpack
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v3.retry'
)
def
test_when_no_score_found
(
self
,
score_deleted
,
score_db_table
,
mock_retry
):
self
.
set_up_course
()
self
.
set_up_course
()
mock_update
.
side_effect
=
[
IntegrityError
(
"WHAMMY"
),
None
]
self
.
recalculate_subsection_grade_kwargs
[
'score_deleted'
]
=
score_deleted
self
.
_apply_recalculate_subsection_grade
()
self
.
recalculate_subsection_grade_kwargs
[
'score_db_table'
]
=
score_db_table
self
.
assertEquals
(
mock_course_signal
.
call_count
,
1
)
if
score_db_table
==
ScoreDatabaseTableEnum
.
submissions
:
with
patch
(
'lms.djangoapps.grades.tasks.sub_api.get_score'
)
as
mock_sub_score
:
mock_sub_score
.
return_value
=
None
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
module_type
=
'any_block_type'
)
)
else
:
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
None
)
if
score_deleted
:
self
.
_assert_retry_not_called
(
mock_retry
)
else
:
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.log'
)
@patch
(
'lms.djangoapps.grades.tasks.log'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
2
.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
3
.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_log_unknown_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
def
test_log_unknown_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
"""
"""
...
@@ -288,7 +290,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -288,7 +290,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_assert_retry_called
(
mock_retry
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.log'
)
@patch
(
'lms.djangoapps.grades.tasks.log'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
2
.retry'
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v
3
.retry'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_no_log_known_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
def
test_no_log_known_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
"""
"""
...
@@ -309,7 +311,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -309,7 +311,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
mocking in place.
mocking in place.
"""
"""
with
self
.
mock_get_score
(
mock_score
):
with
self
.
mock_get_score
(
mock_score
):
recalculate_subsection_grade_v
2
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
recalculate_subsection_grade_v
3
.
apply
(
kwargs
=
self
.
recalculate_subsection_grade_kwargs
)
def
_assert_retry_called
(
self
,
mock_retry
):
def
_assert_retry_called
(
self
,
mock_retry
):
"""
"""
...
@@ -318,3 +320,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
...
@@ -318,3 +320,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
"""
"""
self
.
assertTrue
(
mock_retry
.
called
)
self
.
assertTrue
(
mock_retry
.
called
)
self
.
assertEquals
(
len
(
mock_retry
.
call_args
[
1
][
'kwargs'
]),
len
(
self
.
recalculate_subsection_grade_kwargs
))
self
.
assertEquals
(
len
(
mock_retry
.
call_args
[
1
][
'kwargs'
]),
len
(
self
.
recalculate_subsection_grade_kwargs
))
def
_assert_retry_not_called
(
self
,
mock_retry
):
"""
Verifies the task was not retried.
"""
self
.
assertFalse
(
mock_retry
.
called
)
lms/djangoapps/instructor/enrollment.py
View file @
5a9179bf
...
@@ -16,10 +16,8 @@ from django.utils.translation import override as override_language
...
@@ -16,10 +16,8 @@ from django.utils.translation import override as override_language
from
eventtracking
import
tracker
from
eventtracking
import
tracker
import
pytz
import
pytz
from
course_modes.models
import
CourseMode
from
courseware.models
import
StudentModule
from
edxmako.shortcuts
import
render_to_string
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_RAW_SCORE_CHANGED
from
lms.djangoapps.grades.signals.signals
import
PROBLEM_RAW_SCORE_CHANGED
from
lms.djangoapps.grades.constants
import
ScoreDatabaseTableEnum
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.user_api.models
import
UserPreference
from
openedx.core.djangoapps.user_api.models
import
UserPreference
...
@@ -329,19 +327,22 @@ def _fire_score_changed_for_block(
...
@@ -329,19 +327,22 @@ def _fire_score_changed_for_block(
The earned points are always zero. We must retrieve the possible points
The earned points are always zero. We must retrieve the possible points
from the XModule, as noted below. The effective time is now().
from the XModule, as noted below. The effective time is now().
"""
"""
if
block
and
block
.
has_score
and
block
.
max_score
()
is
not
None
:
if
block
and
block
.
has_score
:
PROBLEM_RAW_SCORE_CHANGED
.
send
(
max_score
=
block
.
max_score
()
sender
=
None
,
if
max_score
is
not
None
:
raw_earned
=
0
,
PROBLEM_RAW_SCORE_CHANGED
.
send
(
raw_possible
=
block
.
max_score
(),
sender
=
None
,
weight
=
getattr
(
block
,
'weight'
,
None
),
raw_earned
=
0
,
user_id
=
student
.
id
,
raw_possible
=
max_score
,
course_id
=
unicode
(
course_id
),
weight
=
getattr
(
block
,
'weight'
,
None
),
usage_id
=
unicode
(
module_state_key
),
user_id
=
student
.
id
,
score_deleted
=
True
,
course_id
=
unicode
(
course_id
),
only_if_higher
=
False
,
usage_id
=
unicode
(
module_state_key
),
modified
=
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
score_deleted
=
True
,
)
only_if_higher
=
False
,
modified
=
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
score_db_table
=
ScoreDatabaseTableEnum
.
courseware_student_module
,
)
def
get_email_params
(
course
,
auto_enroll
,
secure
=
True
,
course_key
=
None
,
display_name
=
None
):
def
get_email_params
(
course
,
auto_enroll
,
secure
=
True
,
course_key
=
None
,
display_name
=
None
):
...
...
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