Files
edx-platform/lms/djangoapps/lti_provider/tasks.py
Phil McGachey 0fe413744f [LTI Provider] Use LTI Outcome Service to pass back scores
This change allows the LTI provider to pass grades back to the campus LMS platform using the
LTI outcome service. For full details of the outcome service, see:
    http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html

In brief, the LTI 1.1 spec defines an outcome service that can be offered by an LTI consumer.
The consumer determines whether a score should be returned (in Canvas, this means that the LTI
tool is used in an assignment, and the launch was performed by a student). If so, it sends
two additional parameters along with the LTI launch:

lis_outcome_service_url: the endpoint for the outcome service on the consumer;
lis_result_sourcedid: a unique identifier for the row in the gradebook (i.e. the tool/student/assignment combination).

The LTI Provider launch view detects the presence of these optional fields, and creates database
records for the specific Outcome Service and for the graded LTI launch. Later, when a score on
edX changes (identified using the signal mechanism from previous LTI Provider pull requests),
a Celery task is launched to pass the score back to the LTI consumer.
2015-06-02 16:55:50 -04:00

99 lines
3.9 KiB
Python

"""
Asynchronous tasks for the LTI provider app.
"""
from django.dispatch import receiver
import logging
from requests.exceptions import RequestException
from courseware.models import SCORE_CHANGED
from lms import CELERY_APP
from lti_provider.models import GradedAssignment
import lti_provider.outcomes
from lti_provider.views import parse_course_and_usage_keys
log = logging.getLogger("edx.lti_provider")
@receiver(SCORE_CHANGED)
def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument
"""
Consume signals that indicate score changes. See the definition of
courseware.models.SCORE_CHANGED for a description of the signal.
"""
points_possible = kwargs.get('points_possible', None)
points_earned = kwargs.get('points_earned', None)
user_id = kwargs.get('user_id', 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):
send_outcome.delay(
points_possible,
points_earned,
user_id,
course_id,
usage_id
)
else:
log.error(
"Outcome Service: Required signal parameter is None. "
"points_possible: %s, points_earned: %s, user_id: %s, "
"course_id: %s, usage_id: %s",
points_possible, points_earned, user_id, course_id, usage_id
)
@CELERY_APP.task
def send_outcome(points_possible, points_earned, user_id, course_id, usage_id):
"""
Calculate the score for a given user in a problem and send it to the
appropriate LTI consumer's outcome service.
"""
course_key, usage_key = parse_course_and_usage_keys(course_id, usage_id)
assignments = GradedAssignment.objects.filter(
user=user_id, course_key=course_key, usage_key=usage_key
)
# Calculate the user's score, on a scale of 0.0 - 1.0.
score = float(points_earned) / float(points_possible)
# There may be zero or more assignment records. We would expect for there
# to be zero if the user/course/usage combination does not relate to a
# previous graded LTI launch. This can happen if an LTI consumer embeds some
# gradable content in a context that doesn't require a score (maybe by
# including an exercise as a sample that students may complete but don't
# count towards their grade).
# There could be more than one GradedAssignment record if the same content
# is embedded more than once in a single course. This would be a strange
# course design on the consumer's part, but we handle it by sending update
# messages for all launches of the content.
for assignment in assignments:
xml = lti_provider.outcomes.generate_replace_result_xml(
assignment.lis_result_sourcedid, score
)
try:
response = lti_provider.outcomes.sign_and_send_replace_result(assignment, xml)
except RequestException:
# failed to send result. 'response' is None, so more detail will be
# logged at the end of the method.
response = None
log.exception("Outcome Service: Error when sending result.")
# If something went wrong, make sure that we have a complete log record.
# That way we can manually fix things up on the campus system later if
# necessary.
if not (response and lti_provider.outcomes.check_replace_result_response(response)):
log.error(
"Outcome Service: Failed to update score on LTI consumer. "
"User: %s, course: %s, usage: %s, score: %s, possible: %s "
"status: %s, body: %s",
user_id,
course_key,
usage_key,
points_earned,
points_possible,
response,
response.text if response else 'Unknown'
)