Skip to content

Commit

Permalink
feat: Improve score publish code
Browse files Browse the repository at this point in the history
  • Loading branch information
kuipumu committed May 29, 2024
1 parent 456a76a commit f39d0ba
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 214 deletions.
2 changes: 1 addition & 1 deletion openedx_lti_tool_plugin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ def ready(self):
"""
from openedx_lti_tool_plugin import signals
from openedx_lti_tool_plugin.resource_link_launch.ags.signals import (
update_course_score,
publish_course_score,
update_unit_or_problem_score,
)
124 changes: 81 additions & 43 deletions openedx_lti_tool_plugin/resource_link_launch/ags/models.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
"""Django Models."""
from __future__ import annotations

import datetime
import logging
from datetime import datetime, timezone
from typing import Optional, Union

from django.db import models
from django.db.models import QuerySet
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pylti1p3.contrib.django import DjangoDbToolConf, DjangoMessageLaunch
from pylti1p3.exception import LtiException
from pylti1p3.grade import Grade
from requests.exceptions import RequestException

from openedx_lti_tool_plugin.apps import OpenEdxLtiToolPluginConfig as app_config
from openedx_lti_tool_plugin.models import LtiProfile
from openedx_lti_tool_plugin.resource_link_launch.ags.validators import validate_context_key

log = logging.getLogger(__name__)


class LtiGradedResourceManager(models.Manager):
"""A manager for the LtiGradedResource model."""
Expand Down Expand Up @@ -68,27 +74,27 @@ class Meta:
verbose_name_plural = 'LTI graded resources'
unique_together = ['lti_profile', 'context_key', 'lineitem']

def update_score(
self,
given_score: Union[int, float],
max_score: Union[int, float],
timestamp: datetime.datetime,
):
"""
Use LTI's score service to update the LTI platform's gradebook.
def __str__(self) -> str:
"""Model string representation."""
return f'<LtiGradedResource, ID: {self.id}>'

This method sends a request to the LTI platform to update the assignment score.
def save(self, *args: tuple, **kwargs: dict):
"""Model save method.
In this method we run field validators.
Args:
given_score: Score given to the graded resource.
max_score: Graded resource max score.
timestamp: Score timestamp object.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
# Create launch message object and set values.
launch_message = DjangoMessageLaunch(request=None, tool_config=DjangoDbToolConf())
launch_message.set_auto_validation(enable=False)
launch_message.set_jwt({
self.full_clean()
super().save(*args, **kwargs)

@cached_property
def publish_score_jwt(self) -> dict:
"""dict: JWT payload for LTI AGS score publish request."""
return {
'body': {
'iss': self.lti_profile.platform_id,
'aud': self.lti_profile.client_id,
Expand All @@ -100,35 +106,67 @@ def update_score(
},
},
},
})
launch_message.set_restored()
launch_message.validate_registration()
# Get AGS service object.
ags = launch_message.get_ags()
# Create grade object and set grade values.
grade = Grade()
grade.set_score_given(given_score)
grade.set_score_maximum(max_score)
grade.set_timestamp(timestamp.isoformat())
grade.set_activity_progress('Submitted')
grade.set_grading_progress('FullyGraded')
grade.set_user_id(self.lti_profile.subject_id)
# Send grade update.
ags.put_grade(grade)
}

def __str__(self) -> str:
"""Model string representation."""
return f'<LtiGradedResource, ID: {self.id}>'
def publish_score(
self,
given_score: Union[int, float],
score_maximum: Union[int, float],
activity_progress: str = 'Submitted',
grading_progress: str = 'FullyGraded',
timestamp: datetime = datetime.now(tz=timezone.utc),
event_id: str = '',
):
"""
Publish score to the LTI platform.
def save(self, *args: tuple, **kwargs: dict):
"""Model save method.
Args:
given_score: Given score.
score_maximum: Score maximum.
activity_progress: Status of the activity's completion.
grading_progress: Status of the grading process.
timestamp: Score datetime.
event_id: Optional ID for this event.
In this method we run field validators.
Raises:
LtiException: Invalid score data.
RequestException: LTI AGS score publish request failure.
Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
.. _LTI Assignment and Grade Services Specification - Score publish service:
https://www.imsglobal.org/spec/lti-ags/v2p0/#score-publish-service
"""
self.full_clean()
super().save(*args, **kwargs)
log_extra = {
'event_id': event_id,
'given_score': given_score,
'score_maximum': score_maximum,
'activity_progress': activity_progress,
'grading_progress': grading_progress,
'user_id': self.lti_profile.subject_id,
'timestamp': str(timestamp),
'jwt': self.publish_score_jwt,
}

try:
log.info(f'LTI AGS score publish request started: {log_extra}')
# Create pylti1.3 DjangoMessageLaunch object.
message = DjangoMessageLaunch(request=None, tool_config=DjangoDbToolConf())\
.set_auto_validation(enable=False)\
.set_jwt(self.publish_score_jwt)\
.set_restored()\
.validate_registration()
# Create Grade object for pylti1.3 AssignmentsGradeService.
grade = Grade()\
.set_score_given(given_score)\
.set_score_maximum(score_maximum)\
.set_timestamp(timestamp.isoformat())\
.set_activity_progress(activity_progress)\
.set_grading_progress(grading_progress)\
.set_user_id(self.lti_profile.subject_id)
# Send score publish request to LTI platform.
message.get_ags().put_grade(grade)
log.info(f'LTI AGS score publish request success: {log_extra}')
except (LtiException, RequestException) as exc:
log_extra['exception'] = str(exc)
log.error(f'LTI AGS score publish request failure: {log_extra}')
raise
53 changes: 35 additions & 18 deletions openedx_lti_tool_plugin/resource_link_launch/ags/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
edx-platform/lms/djangoapps/grades/course_grade.py.
"""
from datetime import datetime, timezone
import logging
import uuid
from typing import Any

from django.dispatch import receiver
Expand All @@ -22,45 +23,61 @@
from openedx_lti_tool_plugin.resource_link_launch.ags.tasks import send_problem_score_update, send_vertical_score_update
from openedx_lti_tool_plugin.utils import is_plugin_enabled

log = logging.getLogger(__name__)
MAX_SCORE = 1.0


@receiver(course_grade_changed())
def update_course_score(
def publish_course_score(
sender: Any, # pylint: disable=unused-argument
user: UserT,
course_grade: Any,
course_key: CourseKey,
**kwargs: dict,
):
"""Update score for LtiGradedResource with course ID as context key.
"""Publish course score to LTI platform AGS score publish service.
This signal is ignored if the plugin is disabled, the grade is not of
an LtiProfile or the course grade percent is less than 0.0.
This signal receiver will publish the score of all course grade changes
for all users with an LtiProfile and an existing LtiGradedResource(s)
with a context_key value equal to this receiver course_key argument.
This signal receiver is ignored if the plugin is disabled or
the course grade change is not for a user with an LtiProfile.
Args:
sender: Signal sender argument.
user: User instance.
course_grade: Course grade object.
course_key: Course opaque key.
sender: Signal sender.
user: User object.
course_grade (CourseGrade): CourseGrade object.
course_key (CourseKey): CourseKey object.
**kwargs: Arbitrary keyword arguments.
"""
if (
not is_plugin_enabled()
or not getattr(user, 'openedx_lti_tool_plugin_lti_profile', None)
or (getattr(course_grade, 'percent', None) or -0.1) < 0.0
):
log_extra = {
'event_id': str(uuid.uuid4()),
'user': str(user),
'course_key': str(course_key),
'course_grade': str(course_grade),
}

if not is_plugin_enabled():
log.info(f'Plugin is disabled: {log_extra}')
return

for graded_resource in LtiGradedResource.objects.all_from_user_id(
if not getattr(user, 'openedx_lti_tool_plugin_lti_profile', None):
log.info(f'LtiProfile not found for user: {log_extra}')
return

lti_graded_resources = LtiGradedResource.objects.all_from_user_id(
user_id=user.id,
context_key=course_key,
):
graded_resource.update_score(
)
log.info(f'Sending course LTI AGS score publish request(s): {log_extra}')

for lti_graded_resource in lti_graded_resources:
lti_graded_resource.publish_score(
course_grade.percent,
MAX_SCORE,
datetime.now(tz=timezone.utc),
event_id=log_extra.get('event_id'),
)


Expand Down
7 changes: 2 additions & 5 deletions openedx_lti_tool_plugin/resource_link_launch/ags/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""
import logging
from datetime import datetime, timezone

from celery import shared_task
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -47,10 +46,9 @@ def send_problem_score_update(
problem_id,
user_id,
)
graded_resource.update_score(
graded_resource.publish_score(
problem_weighted_earned,
problem_weighted_possible,
datetime.now(tz=timezone.utc),
)


Expand Down Expand Up @@ -95,8 +93,7 @@ def send_vertical_score_update(
str(vertical_key),
user_id,
)
graded_resource.update_score(
graded_resource.publish_score(
earned,
possible,
datetime.now(tz=timezone.utc),
)
Loading

0 comments on commit f39d0ba

Please sign in to comment.