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
b9dade55
Commit
b9dade55
authored
Aug 16, 2016
by
Sanford Student
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
handler for SCORE_CHANGED signal
parent
22046d40
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
176 additions
and
19 deletions
+176
-19
lms/djangoapps/courseware/module_render.py
+1
-1
lms/djangoapps/courseware/tests/test_module_render.py
+1
-1
lms/djangoapps/gating/signals.py
+1
-1
lms/djangoapps/gating/tests/test_signals.py
+5
-4
lms/djangoapps/grades/new/subsection_grade.py
+18
-0
lms/djangoapps/grades/signals.py
+37
-3
lms/djangoapps/grades/tests/test_signals.py
+107
-3
lms/djangoapps/lti_provider/tasks.py
+6
-6
No files found.
lms/djangoapps/courseware/module_render.py
View file @
b9dade55
...
...
@@ -539,7 +539,7 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
sender
=
None
,
points_possible
=
event
[
'max_value'
],
points_earned
=
event
[
'value'
],
user
_id
=
user_id
,
user
=
user
,
course_id
=
unicode
(
course_id
),
usage_id
=
unicode
(
descriptor
.
location
)
)
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
b9dade55
...
...
@@ -1843,7 +1843,7 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
'sender'
:
None
,
'points_possible'
:
self
.
grade_dict
[
'max_value'
],
'points_earned'
:
self
.
grade_dict
[
'value'
],
'user
_id'
:
self
.
student_user
.
id
,
'user
'
:
self
.
student_user
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'usage_id'
:
unicode
(
self
.
problem
.
location
)
}
...
...
lms/djangoapps/gating/signals.py
View file @
b9dade55
...
...
@@ -26,5 +26,5 @@ def handle_score_changed(**kwargs):
gating_api
.
evaluate_prerequisite
(
course
,
UsageKey
.
from_string
(
kwargs
.
get
(
'usage_id'
)),
kwargs
.
get
(
'user
_id'
)
,
kwargs
.
get
(
'user
'
)
.
id
,
)
lms/djangoapps/gating/tests/test_signals.py
View file @
b9dade55
...
...
@@ -4,6 +4,7 @@ Unit tests for gating.signals module
from
mock
import
patch
from
opaque_keys.edx.keys
import
UsageKey
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -18,7 +19,7 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
def
setUp
(
self
):
super
(
TestHandleScoreChanged
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
(
org
=
'TestX'
,
number
=
'TS01'
,
run
=
'2016_Q1'
)
self
.
test_user_id
=
0
self
.
user
=
UserFactory
()
self
.
test_usage_key
=
UsageKey
.
from_string
(
'i4x://the/content/key/12345678'
)
@patch
(
'gating.signals.gating_api.evaluate_prerequisite'
)
...
...
@@ -30,11 +31,11 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
sender
=
None
,
points_possible
=
1
,
points_earned
=
1
,
user
_id
=
self
.
test_user_id
,
user
=
self
.
user
,
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
)
mock_evaluate
.
assert_called_with
(
self
.
course
,
self
.
test_usage_key
,
self
.
test_user_id
)
mock_evaluate
.
assert_called_with
(
self
.
course
,
self
.
test_usage_key
,
self
.
user
.
id
)
# pylint: disable=no-member
@patch
(
'gating.signals.gating_api.evaluate_prerequisite'
)
def
test_gating_disabled
(
self
,
mock_evaluate
):
...
...
@@ -43,7 +44,7 @@ class TestHandleScoreChanged(ModuleStoreTestCase):
sender
=
None
,
points_possible
=
1
,
points_earned
=
1
,
user
_id
=
self
.
test_user_id
,
user
=
self
.
user
,
course_id
=
unicode
(
self
.
course
.
id
),
usage_id
=
unicode
(
self
.
test_usage_key
)
)
...
...
lms/djangoapps/grades/new/subsection_grade.py
View file @
b9dade55
...
...
@@ -6,6 +6,7 @@ from lazy import lazy
from
django.conf
import
settings
from
course_blocks.api
import
get_course_blocks
from
courseware.model_data
import
ScoresClient
from
lms.djangoapps.grades.scores
import
get_score
,
possibly_scored
from
lms.djangoapps.grades.models
import
BlockRecord
,
PersistentSubsectionGrade
...
...
@@ -59,6 +60,7 @@ class SubsectionGrade(object):
BlockRecord
(
location
,
weight
,
score
.
possible
)
for
location
,
(
score
,
weight
)
in
self
.
locations_to_weighted_scores
.
iteritems
()
]
PersistentSubsectionGrade
.
save_grade
(
user_id
=
student
.
id
,
usage_key
=
self
.
location
,
...
...
@@ -166,6 +168,22 @@ class SubsectionGradeFactory(object):
self
.
_compute_and_save_grade
(
subsection
,
course_structure
,
course
)
)
def
update
(
self
,
usage_key
,
course_key
):
"""
Updates the SubsectionGrade object for the student and subsection
identified by the given usage key.
"""
from
courseware.courses
import
get_course_by_id
# avoids circular import with courseware.py
course
=
get_course_by_id
(
course_key
,
depth
=
0
)
# save ourselves the extra queries if the course does not use subsection grades
if
not
course
.
enable_subsection_grades_saved
:
return
course_structure
=
get_course_blocks
(
self
.
student
,
usage_key
)
subsection
=
course_structure
[
usage_key
]
self
.
_prefetch_scores
(
course_structure
,
course
)
return
self
.
_compute_and_save_grade
(
subsection
,
course_structure
,
course
)
def
_compute_and_save_grade
(
self
,
subsection
,
course_structure
,
course
):
"""
Freshly computes and updates the grade for the student and subsection.
...
...
lms/djangoapps/grades/signals.py
View file @
b9dade55
"""
Grades related signals.
"""
from
django.conf
import
settings
from
django.dispatch
import
receiver
,
Signal
from
logging
import
getLogger
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.keys
import
UsageKey
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
log
=
getLogger
(
__name__
)
...
...
@@ -58,7 +60,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a
sender
=
None
,
points_possible
=
points_possible
,
points_earned
=
points_earned
,
user
_id
=
user
.
id
,
user
=
user
,
course_id
=
course_id
,
usage_id
=
usage_id
)
...
...
@@ -97,7 +99,7 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
sender
=
None
,
points_possible
=
0
,
points_earned
=
0
,
user
_id
=
user
.
id
,
user
=
user
,
course_id
=
course_id
,
usage_id
=
usage_id
)
...
...
@@ -106,3 +108,35 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
u"Failed to process score_reset signal from Submissions API. "
"user:
%
s, course_id:
%
s, usage_id:
%
s"
,
user
,
course_id
,
usage_id
)
@receiver
(
SCORE_CHANGED
)
def
recalculate_subsection_grade_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume the SCORE_CHANGED signal and trigger an update.
This method expects that the kwargs dictionary will contain the following
entries (See the definition of SCORE_CHANGED):
- points_possible: Maximum score available for the exercise
- points_earned: Score obtained by the user
- user: User object
- course_id: Unicode string representing the course
- usage_id: Unicode string indicating the courseware instance
"""
if
not
settings
.
FEATURES
.
get
(
'ENABLE_SUBSECTION_GRADES_SAVED'
,
False
):
return
try
:
course_id
=
kwargs
.
get
(
'course_id'
,
None
)
usage_id
=
kwargs
.
get
(
'usage_id'
,
None
)
student
=
kwargs
.
get
(
'user'
,
None
)
course_key
=
CourseLocator
.
from_string
(
course_id
)
usage_key
=
UsageKey
.
from_string
(
usage_id
)
.
replace
(
course_key
=
course_key
)
from
lms.djangoapps.grades.new.subsection_grade
import
SubsectionGradeFactory
SubsectionGradeFactory
(
student
)
.
update
(
usage_key
,
course_key
)
except
Exception
as
ex
:
# pylint: disable=broad-except
log
.
exception
(
u"Failed to process SCORE_CHANGED signal. "
"user:
%
s, course_id:
%
s, "
"usage_id:
%
s. Exception:
%
s"
,
unicode
(
student
),
course_id
,
usage_id
,
ex
.
message
)
lms/djangoapps/grades/tests/test_signals.py
View file @
b9dade55
...
...
@@ -2,10 +2,22 @@
Tests for the score change signals defined in the courseware models module.
"""
import
ddt
from
unittest
import
skip
from
django.test
import
TestCase
from
mock
import
patch
,
MagicMock
from
student.models
import
anonymous_id_for_user
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
,
check_mongo_calls
from
..signals
import
submissions_score_set_handler
,
submissions_score_reset_handler
from
..signals
import
(
submissions_score_set_handler
,
submissions_score_reset_handler
,
recalculate_subsection_grade_handler
,
SCORE_CHANGED
)
SUBMISSION_SET_KWARGS
=
{
...
...
@@ -62,7 +74,7 @@ class SubmissionSignalRelayTest(TestCase):
'sender'
:
None
,
'points_possible'
:
10
,
'points_earned'
:
5
,
'user
_id'
:
42
,
'user
'
:
self
.
user_mock
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
}
...
...
@@ -111,7 +123,7 @@ class SubmissionSignalRelayTest(TestCase):
'sender'
:
None
,
'points_possible'
:
0
,
'points_earned'
:
0
,
'user
_id'
:
42
,
'user
'
:
self
.
user_mock
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
}
...
...
@@ -148,3 +160,95 @@ class SubmissionSignalRelayTest(TestCase):
self
.
get_user_mock
=
self
.
setup_patch
(
'lms.djangoapps.grades.signals.user_by_anonymous_id'
,
None
)
submissions_score_reset_handler
(
None
,
**
SUBMISSION_RESET_KWARGS
)
self
.
signal_mock
.
assert_not_called
()
@ddt.ddt
class
ScoreChangedUpdatesSubsectionGradeTest
(
ModuleStoreTestCase
):
"""
Ensures that upon SCORE_CHANGED signals, the handler
initiates an update to the affected subsection grade.
"""
def
setUp
(
self
):
super
(
ScoreChangedUpdatesSubsectionGradeTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
def
set_up_course
(
self
,
enable_subsection_grades
=
True
):
"""
Configures the course for this test.
"""
# pylint: disable=attribute-defined-outside-init,no-member
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
name
=
'course'
,
run
=
'run'
,
metadata
=
{
'enable_subsection_grades_saved'
:
enable_subsection_grades
})
self
.
chapter
=
ItemFactory
.
create
(
parent
=
self
.
course
,
category
=
"chapter"
,
display_name
=
"Chapter"
)
self
.
sequential
=
ItemFactory
.
create
(
parent
=
self
.
chapter
,
category
=
'sequential'
,
display_name
=
"Open Sequential"
)
self
.
problem
=
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem'
)
self
.
score_changed_kwargs
=
{
'points_possible'
:
10
,
'points_earned'
:
5
,
'user'
:
self
.
user
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'usage_id'
:
unicode
(
self
.
problem
.
location
),
}
# this call caches the anonymous id on the user object, saving 4 queries in all happy path tests
_
=
anonymous_id_for_user
(
self
.
user
,
self
.
course
.
id
)
# pylint: enable=attribute-defined-outside-init,no-member
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_subsection_grade_updated_on_signal
(
self
,
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
13
):
recalculate_subsection_grade_handler
(
None
,
**
self
.
score_changed_kwargs
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_query_count_does_not_change_with_more_problems
(
self
,
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem2'
)
ItemFactory
.
create
(
parent
=
self
.
sequential
,
category
=
'problem'
,
display_name
=
'problem3'
)
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
13
):
recalculate_subsection_grade_handler
(
None
,
**
self
.
score_changed_kwargs
)
@ddt.data
(
ModuleStoreEnum
.
Type
.
mongo
,
ModuleStoreEnum
.
Type
.
split
)
def
test_subsection_grades_not_enabled_on_course
(
self
,
default_store
):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
(
enable_subsection_grades
=
False
)
with
check_mongo_calls
(
2
)
and
self
.
assertNumQueries
(
0
):
recalculate_subsection_grade_handler
(
None
,
**
self
.
score_changed_kwargs
)
@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_score_changed_sent_with_feature_flag
(
self
,
default_store
,
feature_flag
):
with
patch
.
dict
(
'django.conf.settings.FEATURES'
,
{
'ENABLE_SUBSECTION_GRADES_SAVED'
:
feature_flag
}):
with
self
.
store
.
default_store
(
default_store
):
self
.
set_up_course
()
with
check_mongo_calls
(
0
)
and
self
.
assertNumQueries
(
19
if
feature_flag
else
1
):
SCORE_CHANGED
.
send
(
sender
=
None
,
**
self
.
score_changed_kwargs
)
@ddt.data
(
(
'points_possible'
,
2
,
13
),
(
'points_earned'
,
2
,
13
),
(
'user'
,
0
,
0
),
(
'course_id'
,
0
,
0
),
(
'usage_id'
,
0
,
0
),
)
@ddt.unpack
def
test_missing_kwargs
(
self
,
kwarg
,
expected_mongo_calls
,
expected_sql_calls
):
self
.
set_up_course
()
del
self
.
score_changed_kwargs
[
kwarg
]
with
patch
(
'lms.djangoapps.grades.signals.log'
)
as
log_mock
:
with
check_mongo_calls
(
expected_mongo_calls
)
and
self
.
assertNumQueries
(
expected_sql_calls
):
recalculate_subsection_grade_handler
(
None
,
**
self
.
score_changed_kwargs
)
self
.
assertEqual
(
log_mock
.
exception
.
called
,
kwarg
not
in
[
'points_possible'
,
'points_earned'
])
lms/djangoapps/lti_provider/tasks.py
View file @
b9dade55
...
...
@@ -27,13 +27,13 @@ def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument
"""
points_possible
=
kwargs
.
get
(
'points_possible'
,
None
)
points_earned
=
kwargs
.
get
(
'points_earned'
,
None
)
user
_id
=
kwargs
.
get
(
'user_id
'
,
None
)
user
=
kwargs
.
get
(
'user
'
,
None
)
course_id
=
kwargs
.
get
(
'course_id'
,
None
)
usage_id
=
kwargs
.
get
(
'usage_id'
,
None
)
if
None
not
in
(
points_earned
,
points_possible
,
user
_id
,
course_id
,
user_
id
):
if
None
not
in
(
points_earned
,
points_possible
,
user
.
id
,
course_id
,
user
.
id
):
course_key
,
usage_key
=
parse_course_and_usage_keys
(
course_id
,
usage_id
)
assignments
=
increment_assignment_versions
(
course_key
,
usage_key
,
user
_
id
)
assignments
=
increment_assignment_versions
(
course_key
,
usage_key
,
user
.
id
)
for
assignment
in
assignments
:
if
assignment
.
usage_key
==
usage_key
:
send_leaf_outcome
.
delay
(
...
...
@@ -41,15 +41,15 @@ def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument
)
else
:
send_composite_outcome
.
apply_async
(
(
user
_
id
,
course_id
,
assignment
.
id
,
assignment
.
version_number
),
(
user
.
id
,
course_id
,
assignment
.
id
,
assignment
.
version_number
),
countdown
=
settings
.
LTI_AGGREGATE_SCORE_PASSBACK_DELAY
)
else
:
log
.
error
(
"Outcome Service: Required signal parameter is None. "
"points_possible:
%
s, points_earned:
%
s, user
_id
:
%
s, "
"points_possible:
%
s, points_earned:
%
s, user:
%
s, "
"course_id:
%
s, usage_id:
%
s"
,
points_possible
,
points_earned
,
u
ser_id
,
course_id
,
usage_id
points_possible
,
points_earned
,
u
nicode
(
user
)
,
course_id
,
usage_id
)
...
...
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