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):
'course_id'
:
unicode
(
self
.
course
.
id
),
'usage_id'
:
unicode
(
self
.
problem
.
location
),
'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
)
...
...
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 (
SUBSECTION_SCORE_CHANGED
,
SCORE_PUBLISHED
,
)
from
..constants
import
ScoreDatabaseTableEnum
from
..new.course_grade
import
CourseGradeFactory
from
..scores
import
weighted_score
from
..tasks
import
recalculate_subsection_grade_v
2
from
..tasks
import
recalculate_subsection_grade_v
3
log
=
getLogger
(
__name__
)
...
...
@@ -62,9 +63,11 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
weighted_earned
=
points_earned
,
weighted_possible
=
points_possible
,
user_id
=
user
.
id
,
anonymous_user_id
=
kwargs
[
'anonymous_user_id'
],
course_id
=
course_id
,
usage_id
=
usage_id
,
modified
=
kwargs
[
'created_at'
],
score_db_table
=
ScoreDatabaseTableEnum
.
submissions
,
)
...
...
@@ -93,9 +96,11 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
weighted_earned
=
0
,
weighted_possible
=
0
,
user_id
=
user
.
id
,
anonymous_user_id
=
kwargs
[
'anonymous_user_id'
],
course_id
=
course_id
,
usage_id
=
usage_id
,
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_
usage_id
=
unicode
(
block
.
location
),
only_if_higher
=
only_if_higher
,
modified
=
score_modified_time
,
score_db_table
=
ScoreDatabaseTableEnum
.
courseware_student_module
,
)
return
update_score
...
...
@@ -162,6 +168,7 @@ def problem_raw_score_changed_handler(sender, **kwargs): # pylint: disable=unus
only_if_higher
=
kwargs
[
'only_if_higher'
],
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
modified
=
kwargs
[
'modified'
],
score_db_table
=
kwargs
[
'score_db_table'
],
)
...
...
@@ -172,9 +179,10 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
enqueueing a subsection update operation to occur asynchronously.
"""
_emit_problem_submitted_event
(
kwargs
)
result
=
recalculate_subsection_grade_v
2
.
apply_async
(
result
=
recalculate_subsection_grade_v
3
.
apply_async
(
kwargs
=
dict
(
user_id
=
kwargs
[
'user_id'
],
anonymous_user_id
=
kwargs
.
get
(
'anonymous_user_id'
),
course_id
=
kwargs
[
'course_id'
],
usage_id
=
kwargs
[
'usage_id'
],
only_if_higher
=
kwargs
.
get
(
'only_if_higher'
),
...
...
@@ -182,6 +190,7 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum
score_deleted
=
kwargs
.
get
(
'score_deleted'
,
False
),
event_transaction_id
=
unicode
(
get_event_transaction_id
()),
event_transaction_type
=
unicode
(
get_event_transaction_type
()),
score_db_table
=
kwargs
[
'score_db_table'
],
)
)
log
.
info
(
...
...
lms/djangoapps/grades/signals/signals.py
View file @
5a9179bf
...
...
@@ -22,6 +22,7 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
# made only if the new score is higher than previous.
'modified'
,
# A datetime indicating when the database representation of
# 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(
PROBLEM_WEIGHTED_SCORE_CHANGED
=
Signal
(
providing_args
=
[
'user_id'
,
# Integer User ID
'anonymous_user_id'
,
# Anonymous User ID
'course_id'
,
# Unicode string representing the course
'usage_id'
,
# Unicode string indicating the courseware instance
'weighted_earned'
,
# Score obtained by the user
...
...
@@ -43,6 +45,7 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
# made only if the new score is higher than previous.
'modified'
,
# A datetime indicating when the database representation of
# 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(
'raw_possible'
,
# Maximum score available for the exercise
'only_if_higher'
,
# Boolean indicating whether updates should be
# 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
from
xmodule.modulestore.django
import
modulestore
from
.config.models
import
PersistentGradesEnabledFlag
from
.constants
import
ScoreDatabaseTableEnum
from
.new.subsection_grade
import
SubsectionGradeFactory
from
.signals.signals
import
SUBSECTION_SCORE_CHANGED
from
.transformer
import
GradesTransformer
...
...
@@ -35,33 +36,31 @@ log = getLogger(__name__)
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
)
def
recalculate_subsection_grade
(
# pylint: disable=unused-argument
user_id
,
course_id
,
usage_id
,
only_if_higher
,
weighted_earned
,
weighted_possible
,
**
kwargs
):
# TODO (TNL-6373) DELETE ME once v3 is successfully deployed to Prod.
@task
(
base
=
PersistOnFailureTask
,
default_retry_delay
=
30
,
routing_key
=
settings
.
RECALCULATE_GRADES_ROUTING_KEY
)
def
recalculate_subsection_grade_v2
(
**
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
(
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'
],
)
)
_recalculate_subsection_grade
(
recalculate_subsection_grade_v2
,
**
kwargs
)
@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.
Arguments:
Keyword
Arguments:
user_id (int): id of applicable User object
anonymous_user_id (int, OPTIONAL): Anonymous ID of the User
course_id (string): identifying the course
usage_id (string): identifying the course block
only_if_higher (boolean): indicating whether grades should
...
...
@@ -71,36 +70,44 @@ def recalculate_subsection_grade_v2(**kwargs):
was queued so that we can verify the underlying data update.
score_deleted (boolean): indicating whether the grade change is
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_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.
score_db_table (ScoreDatabaseTableEnum): database table that houses
the changed score. Used in conjunction with expected_modified_time.
"""
try
:
course_key
=
CourseLocator
.
from_string
(
kwargs
[
'course_id'
])
if
not
PersistentGradesEnabledFlag
.
feature_enabled
(
course_key
):
return
score_deleted
=
kwargs
[
'score_deleted'
]
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,
# where this code runs. So we take the values from the
# main request cache and store them in the local request
# cache. This correlates model-level grading events with
# higher-level ones.
set_event_transaction_id
(
kwargs
.
pop
(
'event_transaction_id'
,
None
))
set_event_transaction_type
(
kwargs
.
pop
(
'event_transaction_type'
,
None
))
set_event_transaction_id
(
kwargs
.
get
(
'event_transaction_id'
))
set_event_transaction_type
(
kwargs
.
get
(
'event_transaction_type'
))
# Verify the database has been updated with the scores when the task was
# created. This race condition occurs if the transaction in the task
# creator's process hasn't committed before the task initiates in the worker
# process.
if
not
_has_database_updated_with_new_score
(
kwargs
[
'user_id'
],
scored_block_usage_key
,
expected_modified_time
,
score_deleted
,
):
raise
_retry_recalculate_subsection_grade
(
**
kwargs
)
if
task_func
==
recalculate_subsection_grade_v2
:
has_database_updated
=
_has_db_updated_with_new_score_bwc_v2
(
kwargs
[
'user_id'
],
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
(
course_key
,
...
...
@@ -115,13 +122,45 @@ def recalculate_subsection_grade_v2(**kwargs):
repr
(
exc
),
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
,
):
"""
DEPRECATED version for backward compatibility with v2 tasks.
Returns whether the database has been updated with the
expected new score values for the given problem and user.
"""
...
...
@@ -186,7 +225,7 @@ def _update_subsection_grades(
only_if_higher
,
)
SUBSECTION_SCORE_CHANGED
.
send
(
sender
=
recalculate_subsection_grad
e
,
sender
=
Non
e
,
course
=
course
,
course_structure
=
course_structure
,
user
=
student
,
...
...
@@ -194,29 +233,9 @@ def _update_subsection_grades(
)
def
_retry_recalculate_subsection_grade
(
user_id
,
course_id
,
usage_id
,
only_if_higher
,
expected_modified_time
,
score_deleted
,
exc
=
None
,
):
def
_retry_recalculate_subsection_grade
(
task_func
,
exc
=
None
,
**
kwargs
):
"""
Calls retry for the recalculate_subsection_grade task with the
given inputs.
"""
recalculate_subsection_grade_v2
.
retry
(
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
,
)
task_func
.
retry
(
kwargs
=
kwargs
,
exc
=
exc
)
lms/djangoapps/grades/tests/test_signals.py
View file @
5a9179bf
...
...
@@ -11,6 +11,7 @@ from mock import patch, MagicMock
import
pytz
from
util.date_utils
import
to_timestamp
from
..constants
import
ScoreDatabaseTableEnum
from
..signals.handlers
import
(
enqueue_subsection_update
,
submissions_score_set_handler
,
...
...
@@ -32,7 +33,6 @@ SUBMISSION_SET_KWARGS = {
'created_at'
:
FROZEN_NOW_TIMESTAMP
,
}
SUBMISSION_RESET_KWARGS
=
{
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
...
...
@@ -50,6 +50,20 @@ PROBLEM_RAW_SCORE_CHANGED_KWARGS = {
'only_if_higher'
:
False
,
'score_deleted'
:
True
,
'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):
'weighted_possible'
:
possible
,
'weighted_earned'
:
earned
,
'user_id'
:
self
.
user_mock
.
id
,
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
,
'modified'
:
FROZEN_NOW_TIMESTAMP
,
'score_db_table'
:
'submissions'
,
}
self
.
signal_mock
.
assert_called_once_with
(
**
expected_set_kwargs
)
self
.
get_user_mock
.
assert_called_once_with
(
kwargs
[
'anonymous_user_id'
])
...
...
@@ -153,44 +169,26 @@ class ScoreChangedSignalRelayTest(TestCase):
def
test_raw_score_changed_signal_handler
(
self
):
problem_raw_score_changed_handler
(
None
,
**
PROBLEM_RAW_SCORE_CHANGED_KWARGS
)
expected_set_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
}
expected_set_kwargs
=
PROBLEM_WEIGHTED_SCORE_CHANGED_KWARGS
.
copy
()
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
def
test_raw_score_changed_score_deleted_optional
(
self
):
local_kwargs
=
PROBLEM_RAW_SCORE_CHANGED_KWARGS
.
copy
()
del
local_kwargs
[
'score_deleted'
]
problem_raw_score_changed_handler
(
None
,
**
local_kwargs
)
expected_set_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'
:
False
,
'modified'
:
FROZEN_NOW_TIMESTAMP
}
expected_set_kwargs
=
PROBLEM_WEIGHTED_SCORE_CHANGED_KWARGS
.
copy
()
expected_set_kwargs
[
'score_deleted'
]
=
False
self
.
signal_mock
.
assert_called_with
(
**
expected_set_kwargs
)
@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
(
sender
=
'test'
,
user_id
=
1
,
course_id
=
u'course-v1:edX+Demo_Course+DemoX'
,
usage_id
=
u'block-v1:block-key'
,
modified
=
FROZEN_NOW_DATETIME
,
score_db_table
=
ScoreDatabaseTableEnum
.
courseware_student_module
,
)
log_statement
=
mocklog
.
call_args
[
0
][
0
]
log_statement
=
UUID_REGEX
.
sub
(
u'*UUID*'
,
log_statement
)
...
...
@@ -199,6 +197,7 @@ class ScoreChangedSignalRelayTest(TestCase):
(
u'Grades: Request async calculation of subsection grades with args: '
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*]'
)
.
format
(
time
=
FROZEN_NOW_DATETIME
)
)
lms/djangoapps/grades/tests/test_tasks.py
View file @
5a9179bf
...
...
@@ -8,10 +8,10 @@ from datetime import datetime, timedelta
import
ddt
from
django.conf
import
settings
from
django.db.utils
import
IntegrityError
import
itertools
from
mock
import
patch
,
MagicMock
import
pytz
from
util.date_utils
import
to_timestamp
from
unittest
import
skip
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
...
...
@@ -25,8 +25,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
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.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
})
...
...
@@ -40,7 +41,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
user
=
UserFactory
()
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.
"""
...
...
@@ -50,7 +51,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
name
=
'course'
,
run
=
'run'
,
)
if
not
enable_
subsection
_grades
:
if
not
enable_
persistent
_grades
:
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
False
)
self
.
chapter
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Chapter"
)
...
...
@@ -64,10 +65,12 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'weighted_earned'
,
1.0
),
(
'weighted_possible'
,
2.0
),
(
'user_id'
,
self
.
user
.
id
),
(
'anonymous_user_id'
,
5
),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'only_if_higher'
,
None
),
(
'modified'
,
self
.
frozen_now_datetime
),
(
'score_db_table'
,
ScoreDatabaseTableEnum
.
courseware_student_module
),
])
create_new_event_transaction_id
()
...
...
@@ -76,11 +79,13 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
(
'user_id'
,
self
.
user
.
id
),
(
'course_id'
,
unicode
(
self
.
course
.
id
)),
(
'usage_id'
,
unicode
(
self
.
problem
.
location
)),
(
'anonymous_user_id'
,
5
),
(
'only_if_higher'
,
None
),
(
'expected_modified_time'
,
self
.
frozen_now_timestamp
),
(
'score_deleted'
,
False
),
(
'event_transaction_id'
,
unicode
(
get_event_transaction_id
())),
(
'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
...
...
@@ -96,7 +101,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
patch
(
"lms.djangoapps.grades.tasks.get_score"
,
return_value
=
score
):
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.
"""
...
...
@@ -105,27 +110,37 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
local_task_args
=
self
.
recalculate_subsection_grade_kwargs
.
copy
()
local_task_args
[
'event_transaction_type'
]
=
u'edx.grades.problem.submitted'
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
)
as
mock_task_apply
:
PROBLEM_WEIGHTED_SCORE_CHANGED
.
send
(
sender
=
None
,
**
send_args
)
mock_task_apply
.
assert_called_once_with
(
kwargs
=
local_task_args
)
@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
.
_apply_recalculate_subsection_grade
()
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
(
(
ModuleStoreEnum
.
Type
.
mongo
,
1
,
23
),
(
ModuleStoreEnum
.
Type
.
split
,
3
,
22
),
)
@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
):
self
.
set_up_course
()
self
.
assertTrue
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
...
...
@@ -133,6 +148,30 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
with
self
.
assertNumQueries
(
num_sql_calls
):
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'
)
def
test_other_inaccessible_subsection
(
self
,
mock_subsection_signal
):
self
.
set_up_course
()
...
...
@@ -157,68 +196,29 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
{
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
)
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
):
self
.
set_up_course
(
enable_
subsection
_grades
=
False
)
self
.
set_up_course
(
enable_
persistent
_grades
=
False
)
self
.
assertFalse
(
PersistentGradesEnabledFlag
.
feature_enabled
(
self
.
course
.
id
))
with
check_mongo_calls
(
0
):
with
self
.
assertNumQueries
(
0
):
self
.
_apply_recalculate_subsection_grade
()
@skip
(
"Pending completion of TNL-5089"
)
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
True
),
(
ModuleStoreEnum
.
Type
.
split
,
True
),
(
ModuleStoreEnum
.
Type
.
mongo
,
False
),
(
ModuleStoreEnum
.
Type
.
split
,
False
),
)
@ddt.unpack
def
test_query_counts_with_feature_flag
(
self
,
default_store
,
feature_flag
):
PersistentGradesEnabledFlag
.
objects
.
create
(
enabled
=
feature_flag
)
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.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_retry_first_time_only
(
self
,
mock_update
,
mock_course_signal
):
"""
Ensures that a task retry completes after a one-time failure.
"""
self
.
set_up_course
()
mock_update
.
side_effect
=
[
IntegrityError
(
"WHAMMY"
),
None
]
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_course_signal
.
call_count
,
1
)
@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'
)
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.
"""
...
...
@@ -227,55 +227,57 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_apply_recalculate_subsection_grade
()
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
def
test_retry_subsection_grade_on_update_not_complete
(
self
,
mock_retry
):
@ddt.data
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
)
@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
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
modified
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
1
))
)
self
.
_assert_retry_called
(
mock_retry
)
@patch
(
'lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry'
)
def
test_retry_subsection_grade_on_update_not_complete_sub
(
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
=
{
'created_at'
:
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
1
)
}
self
.
recalculate_subsection_grade_kwargs
[
'score_db_table'
]
=
score_db_table
modified_datetime
=
datetime
.
utcnow
()
.
replace
(
tzinfo
=
pytz
.
UTC
)
-
timedelta
(
days
=
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
=
{
'created_at'
:
modified_datetime
}
self
.
_apply_recalculate_subsection_grade
(
mock_score
=
MagicMock
(
module_type
=
'any_block_type'
)
)
else
:
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
)
@patch
(
'lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send'
)
@patch
(
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update'
)
def
test_retry_first_time_only
(
self
,
mock_update
,
mock_course_signal
):
"""
Ensures that a task retry completes after a one-time failure.
"""
@ddt.data
(
*
itertools
.
product
(
(
True
,
False
),
(
ScoreDatabaseTableEnum
.
courseware_student_module
,
ScoreDatabaseTableEnum
.
submissions
),
)
)
@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
()
mock_update
.
side_effect
=
[
IntegrityError
(
"WHAMMY"
),
None
]
self
.
_apply_recalculate_subsection_grade
()
self
.
assertEquals
(
mock_course_signal
.
call_count
,
1
)
self
.
recalculate_subsection_grade_kwargs
[
'score_deleted'
]
=
score_deleted
self
.
recalculate_subsection_grade_kwargs
[
'score_db_table'
]
=
score_db_table
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.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'
)
def
test_log_unknown_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
"""
...
...
@@ -288,7 +290,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self
.
_assert_retry_called
(
mock_retry
)
@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'
)
def
test_no_log_known_error
(
self
,
mock_update
,
mock_retry
,
mock_log
):
"""
...
...
@@ -309,7 +311,7 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
mocking in place.
"""
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
):
"""
...
...
@@ -318,3 +320,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
"""
self
.
assertTrue
(
mock_retry
.
called
)
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
from
eventtracking
import
tracker
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.constants
import
ScoreDatabaseTableEnum
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
from
openedx.core.djangoapps.user_api.models
import
UserPreference
...
...
@@ -329,19 +327,22 @@ def _fire_score_changed_for_block(
The earned points are always zero. We must retrieve the possible points
from the XModule, as noted below. The effective time is now().
"""
if
block
and
block
.
has_score
and
block
.
max_score
()
is
not
None
:
PROBLEM_RAW_SCORE_CHANGED
.
send
(
sender
=
None
,
raw_earned
=
0
,
raw_possible
=
block
.
max_score
(),
weight
=
getattr
(
block
,
'weight'
,
None
),
user_id
=
student
.
id
,
course_id
=
unicode
(
course_id
),
usage_id
=
unicode
(
module_state_key
),
score_deleted
=
True
,
only_if_higher
=
False
,
modified
=
datetime
.
now
()
.
replace
(
tzinfo
=
pytz
.
UTC
),
)
if
block
and
block
.
has_score
:
max_score
=
block
.
max_score
()
if
max_score
is
not
None
:
PROBLEM_RAW_SCORE_CHANGED
.
send
(
sender
=
None
,
raw_earned
=
0
,
raw_possible
=
max_score
,
weight
=
getattr
(
block
,
'weight'
,
None
),
user_id
=
student
.
id
,
course_id
=
unicode
(
course_id
),
usage_id
=
unicode
(
module_state_key
),
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
):
...
...
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