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
73b854e4
Commit
73b854e4
authored
Apr 28, 2015
by
Phil McGachey
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[LTI Provider] Adding signals for scoring events
parent
38b61fd8
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
315 additions
and
1 deletions
+315
-1
lms/djangoapps/courseware/models.py
+105
-1
lms/djangoapps/courseware/module_render.py
+12
-0
lms/djangoapps/courseware/tests/test_module_render.py
+14
-0
lms/djangoapps/courseware/tests/test_signals.py
+157
-0
lms/djangoapps/lti_provider/models.py
+27
-0
No files found.
lms/djangoapps/courseware/models.py
View file @
73b854e4
...
...
@@ -12,16 +12,22 @@ file and check it in at the same time as your model changes. To do that,
ASSUMPTIONS: modules have unique IDs, even across different module_types
"""
import
logging
from
django.contrib.auth.models
import
User
from
django.conf
import
settings
from
django.db
import
models
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
from
django.dispatch
import
receiver
,
Signal
from
model_utils.models
import
TimeStampedModel
from
student.models
import
user_by_anonymous_id
from
submissions.models
import
score_set
,
score_reset
from
xmodule_django.models
import
CourseKeyField
,
LocationKeyField
,
BlockTypeKeyField
# pylint: disable=import-error
log
=
logging
.
getLogger
(
"edx.courseware"
)
class
StudentModule
(
models
.
Model
):
"""
...
...
@@ -248,3 +254,101 @@ class StudentFieldOverride(TimeStampedModel):
field
=
models
.
CharField
(
max_length
=
255
)
value
=
models
.
TextField
(
default
=
'null'
)
# Signal that indicates that a user's score for a problem has been updated.
# This signal is generated when a scoring event occurs either within the core
# platform or in the Submissions module. Note that this signal will be triggered
# regardless of the new and previous values of the score (i.e. it may be the
# case that this signal is generated when a user re-attempts a problem but
# receives the same score).
SCORE_CHANGED
=
Signal
(
providing_args
=
[
'points_possible'
,
# Maximum score available for the exercise
'points_earned'
,
# Score obtained by the user
'user_id'
,
# Integer User ID
'course_id'
,
# Unicode string representing the course
'usage_id'
# Unicode string indicating the courseware instance
]
)
@receiver
(
score_set
)
def
submissions_score_set_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume the score_set signal defined in the Submissions API, and convert it
to a SCORE_CHANGED signal defined in this module. Converts the unicode keys
for user, course and item into the standard representation for the
SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_set):
- 'points_possible': integer,
- 'points_earned': integer,
- 'anonymous_user_id': unicode,
- 'course_id': unicode,
- 'item_id': unicode
"""
points_possible
=
kwargs
.
get
(
'points_possible'
,
None
)
points_earned
=
kwargs
.
get
(
'points_earned'
,
None
)
course_id
=
kwargs
.
get
(
'course_id'
,
None
)
usage_id
=
kwargs
.
get
(
'item_id'
,
None
)
user
=
None
if
'anonymous_user_id'
in
kwargs
:
user
=
user_by_anonymous_id
(
kwargs
.
get
(
'anonymous_user_id'
))
# If any of the kwargs were missing, at least one of the following values
# will be None.
if
all
((
user
,
points_possible
,
points_earned
,
course_id
,
usage_id
)):
SCORE_CHANGED
.
send
(
sender
=
None
,
points_possible
=
points_possible
,
points_earned
=
points_earned
,
user_id
=
user
.
id
,
course_id
=
course_id
,
usage_id
=
usage_id
)
else
:
log
.
exception
(
u"Failed to process score_set signal from Submissions API. "
"points_possible:
%
s, points_earned:
%
s, user:
%
s, course_id:
%
s, "
"usage_id:
%
s"
,
points_possible
,
points_earned
,
user
,
course_id
,
usage_id
)
@receiver
(
score_reset
)
def
submissions_score_reset_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume the score_reset signal defined in the Submissions API, and convert
it to a SCORE_CHANGED signal indicating that the score has been set to 0/0.
Converts the unicode keys for user, course and item into the standard
representation for the SCORE_CHANGED signal.
This method expects that the kwargs dictionary will contain the following
entries (See the definition of score_reset):
- 'anonymous_user_id': unicode,
- 'course_id': unicode,
- 'item_id': unicode
"""
course_id
=
kwargs
.
get
(
'course_id'
,
None
)
usage_id
=
kwargs
.
get
(
'item_id'
,
None
)
user
=
None
if
'anonymous_user_id'
in
kwargs
:
user
=
user_by_anonymous_id
(
kwargs
.
get
(
'anonymous_user_id'
))
# If any of the kwargs were missing, at least one of the following values
# will be None.
if
all
((
user
,
course_id
,
usage_id
)):
SCORE_CHANGED
.
send
(
sender
=
None
,
points_possible
=
0
,
points_earned
=
0
,
user_id
=
user
.
id
,
course_id
=
course_id
,
usage_id
=
usage_id
)
else
:
log
.
exception
(
u"Failed to process score_reset signal from Submissions API. "
"user:
%
s, course_id:
%
s, usage_id:
%
s"
,
user
,
course_id
,
usage_id
)
lms/djangoapps/courseware/module_render.py
View file @
73b854e4
...
...
@@ -30,6 +30,7 @@ from capa.xqueue_interface import XQueueInterface
from
courseware.access
import
has_access
,
get_user_role
from
courseware.masquerade
import
setup_masquerade
from
courseware.model_data
import
FieldDataCache
,
DjangoKeyValueStore
from
courseware.models
import
SCORE_CHANGED
from
courseware.entrance_exams
import
(
get_entrance_exam_score
,
user_must_complete_entrance_exam
...
...
@@ -450,6 +451,17 @@ def get_module_system_for_user(user, field_data_cache,
descriptor
.
location
,
)
# Send a signal out to any listeners who are waiting for score change
# events.
SCORE_CHANGED
.
send
(
sender
=
None
,
points_possible
=
event
[
'max_value'
],
points_earned
=
event
[
'value'
],
user_id
=
user_id
,
course_id
=
unicode
(
course_id
),
usage_id
=
unicode
(
descriptor
.
location
)
)
def
publish
(
block
,
event_type
,
event
):
"""A function that allows XModules to publish events."""
if
event_type
==
'grade'
:
...
...
lms/djangoapps/courseware/tests/test_module_render.py
View file @
73b854e4
...
...
@@ -1246,6 +1246,20 @@ class TestXmoduleRuntimeEvent(TestSubmittingProblems):
self
.
assertIsNone
(
student_module
.
grade
)
self
.
assertIsNone
(
student_module
.
max_grade
)
@patch
(
'courseware.module_render.SCORE_CHANGED.send'
)
def
test_score_change_signal
(
self
,
send_mock
):
"""Test that a Django signal is generated when a score changes"""
self
.
set_module_grade_using_publish
(
self
.
grade_dict
)
expected_signal_kwargs
=
{
'sender'
:
None
,
'points_possible'
:
self
.
grade_dict
[
'max_value'
],
'points_earned'
:
self
.
grade_dict
[
'value'
],
'user_id'
:
self
.
student_user
.
id
,
'course_id'
:
unicode
(
self
.
course
.
id
),
'usage_id'
:
unicode
(
self
.
problem
.
location
)
}
send_mock
.
assert_called_with
(
**
expected_signal_kwargs
)
@attr
(
'shard_1'
)
class
TestRebindModule
(
TestSubmittingProblems
):
...
...
lms/djangoapps/courseware/tests/test_signals.py
0 → 100644
View file @
73b854e4
"""
Tests for the score change signals defined in the courseware models module.
"""
from
django.test
import
TestCase
from
mock
import
patch
,
MagicMock
from
courseware.models
import
submissions_score_set_handler
,
submissions_score_reset_handler
SUBMISSION_SET_KWARGS
=
{
'points_possible'
:
10
,
'points_earned'
:
5
,
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
'item_id'
:
'i4x://org/course/usage/123456'
}
SUBMISSION_RESET_KWARGS
=
{
'anonymous_user_id'
:
'anonymous_id'
,
'course_id'
:
'CourseID'
,
'item_id'
:
'i4x://org/course/usage/123456'
}
class
SubmissionSignalRelayTest
(
TestCase
):
"""
Tests to ensure that the courseware module correctly catches score_set and
score_reset signals from the Submissions API and recasts them as LMS
signals. This ensures that listeners in the LMS only have to handle one type
of signal for all scoring events.
"""
def
setUp
(
self
):
"""
Configure mocks for all the dependencies of the render method
"""
super
(
SubmissionSignalRelayTest
,
self
)
.
setUp
()
self
.
signal_mock
=
self
.
setup_patch
(
'courseware.models.SCORE_CHANGED.send'
,
None
)
self
.
user_mock
=
MagicMock
()
self
.
user_mock
.
id
=
42
self
.
get_user_mock
=
self
.
setup_patch
(
'courseware.models.user_by_anonymous_id'
,
self
.
user_mock
)
def
setup_patch
(
self
,
function_name
,
return_value
):
"""
Patch a function with a given return value, and return the mock
"""
mock
=
MagicMock
(
return_value
=
return_value
)
new_patch
=
patch
(
function_name
,
new
=
mock
)
new_patch
.
start
()
self
.
addCleanup
(
new_patch
.
stop
)
return
mock
def
setup_patch_with_mock
(
self
,
function_name
,
mock
):
"""
Patch a function with a given mock
"""
new_patch
=
patch
(
function_name
,
new
=
mock
)
new_patch
.
start
()
self
.
addCleanup
(
new_patch
.
stop
)
return
mock
def
test_score_set_signal_handler
(
self
):
"""
Ensure that, on receipt of a score_set signal from the Submissions API,
the courseware model correctly converts it to a score_changed signal
"""
submissions_score_set_handler
(
None
,
**
SUBMISSION_SET_KWARGS
)
expected_set_kwargs
=
{
'sender'
:
None
,
'points_possible'
:
10
,
'points_earned'
:
5
,
'user_id'
:
42
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
}
self
.
signal_mock
.
assert_called_once_with
(
**
expected_set_kwargs
)
def
test_score_set_user_conversion
(
self
):
"""
Ensure that the score_set handler properly calls the
user_by_anonymous_id method to convert from an anonymized ID to a user
object
"""
submissions_score_set_handler
(
None
,
**
SUBMISSION_SET_KWARGS
)
self
.
get_user_mock
.
assert_called_once_with
(
'anonymous_id'
)
def
test_score_set_missing_kwarg
(
self
):
"""
Ensure that, on receipt of a score_set signal from the Submissions API
that does not have the correct kwargs, the courseware model does not
generate a signal.
"""
for
missing
in
SUBMISSION_SET_KWARGS
:
kwargs
=
SUBMISSION_SET_KWARGS
.
copy
()
del
kwargs
[
missing
]
submissions_score_set_handler
(
None
,
**
kwargs
)
self
.
signal_mock
.
assert_not_called
()
def
test_score_set_bad_user
(
self
):
"""
Ensure that, on receipt of a score_set signal from the Submissions API
that has an invalid user ID, the courseware model does not generate a
signal.
"""
self
.
get_user_mock
=
self
.
setup_patch
(
'courseware.models.user_by_anonymous_id'
,
None
)
submissions_score_set_handler
(
None
,
**
SUBMISSION_SET_KWARGS
)
self
.
signal_mock
.
assert_not_called
()
def
test_score_reset_signal_handler
(
self
):
"""
Ensure that, on receipt of a score_reset signal from the Submissions
API, the courseware model correctly converts it to a score_changed
signal
"""
submissions_score_reset_handler
(
None
,
**
SUBMISSION_RESET_KWARGS
)
expected_reset_kwargs
=
{
'sender'
:
None
,
'points_possible'
:
0
,
'points_earned'
:
0
,
'user_id'
:
42
,
'course_id'
:
'CourseID'
,
'usage_id'
:
'i4x://org/course/usage/123456'
}
self
.
signal_mock
.
assert_called_once_with
(
**
expected_reset_kwargs
)
def
test_score_reset_user_conversion
(
self
):
"""
Ensure that the score_reset handler properly calls the
user_by_anonymous_id method to convert from an anonymized ID to a user
object
"""
submissions_score_reset_handler
(
None
,
**
SUBMISSION_RESET_KWARGS
)
self
.
get_user_mock
.
assert_called_once_with
(
'anonymous_id'
)
def
test_score_reset_missing_kwarg
(
self
):
"""
Ensure that, on receipt of a score_reset signal from the Submissions API
that does not have the correct kwargs, the courseware model does not
generate a signal.
"""
for
missing
in
SUBMISSION_RESET_KWARGS
:
kwargs
=
SUBMISSION_RESET_KWARGS
.
copy
()
del
kwargs
[
missing
]
submissions_score_reset_handler
(
None
,
**
kwargs
)
self
.
signal_mock
.
assert_not_called
()
def
test_score_reset_bad_user
(
self
):
"""
Ensure that, on receipt of a score_reset signal from the Submissions API
that has an invalid user ID, the courseware model does not generate a
signal.
"""
self
.
get_user_mock
=
self
.
setup_patch
(
'courseware.models.user_by_anonymous_id'
,
None
)
submissions_score_reset_handler
(
None
,
**
SUBMISSION_RESET_KWARGS
)
self
.
signal_mock
.
assert_not_called
()
lms/djangoapps/lti_provider/models.py
View file @
73b854e4
...
...
@@ -2,6 +2,9 @@
Database models for the LTI provider feature.
"""
from
django.db
import
models
from
django.dispatch
import
receiver
from
courseware.models
import
SCORE_CHANGED
class
LtiConsumer
(
models
.
Model
):
...
...
@@ -12,3 +15,27 @@ class LtiConsumer(models.Model):
"""
key
=
models
.
CharField
(
max_length
=
32
,
unique
=
True
,
db_index
=
True
)
secret
=
models
.
CharField
(
max_length
=
32
,
unique
=
True
)
@receiver
(
SCORE_CHANGED
)
def
score_changed_handler
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""
Consume signals that indicate score changes.
TODO: This function is a placeholder for integration with the LTI 1.1
outcome service, which will follow in a separate change.
"""
message
=
"""LTI Provider got score change event:
points_possible: {}
points_earned: {}
user_id: {}
course_id: {}
usage_id: {}
"""
print
message
.
format
(
kwargs
.
get
(
'points_possible'
,
None
),
kwargs
.
get
(
'points_earned'
,
None
),
kwargs
.
get
(
'user_id'
,
None
),
kwargs
.
get
(
'course_id'
,
None
),
kwargs
.
get
(
'usage_id'
,
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