Unable to link a programmatic grade
Hello everyone,
I wanted to confirm whether this an issue with pylti1.3 or if I'm missing something, when adding multiple assignment grades through the LTI Assigments and Grades Services with the programmatic mode active, rather than the declarative/default grading mode.
I'm able to register and read back grades (line items) into the LMS. But none of them are registered as the actual score on the LMS side. If my understanding is correct, one of these line items needs to be selected by specifying the resourceLinkId. pylti1.3 does allow setting the resource_id, but not the resource_link_id.
Am I missing something?
I'm using as reference this score() function from the repo
def score(launch_id, earned_score, time_spent):
tool_conf = ToolConfJsonFile(get_lti_config_path())
flask_request = FlaskRequest()
launch_data_storage = get_launch_data_storage()
message_launch = ExtendedFlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf,
launch_data_storage=launch_data_storage)
resource_link_id = message_launch.get_launch_data() \
.get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id')
if not message_launch.has_ags():
raise Forbidden("Don't have grades!")
sub = message_launch.get_launch_data().get('sub')
timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
earned_score = int(earned_score)
time_spent = int(time_spent)
grades = message_launch.get_ags()
sc = Grade()
sc.set_score_given(earned_score) \
.set_score_maximum(100) \
.set_timestamp(timestamp) \
.set_activity_progress('Completed') \
.set_grading_progress('FullyGraded') \
.set_user_id(sub)
sc_line_item = LineItem()
sc_line_item.set_tag('score') \
.set_score_maximum(100) \
.set_label('Score')
if resource_link_id:
sc_line_item.set_resource_id(resource_link_id)
grades.put_grade(sc, sc_line_item)
tm = Grade()
tm.set_score_given(time_spent) \
.set_score_maximum(999) \
.set_timestamp(timestamp) \
.set_activity_progress('Completed') \
.set_grading_progress('FullyGraded') \
.set_user_id(sub)
tm_line_item = LineItem()
tm_line_item.set_tag('time') \
.set_score_maximum(999) \
.set_label('Time Taken')
if resource_link_id:
tm_line_item.set_resource_id(resource_link_id)
result = grades.put_grade(tm, tm_line_item)
return jsonify({'success': True, 'result': result.get('body')})
hi @Jack-2025
To be honest i don't understand what it the difference between "programmatic" and "declarative" grading modes from your message but as i understand you've asked how to send grades into correct gradebook endpoint of the external LMS, right?
To implement this you need only two things: lineitem ID and external user ID. And both of them could be retrieved from the LTI initial launch message.
Here is an example:
- LTI launch. Saving all necessary data in the DB (
process 1):
class Assignment:
def __init__(self, title, score, lti_lineitem: str, lti_jwt_sub: str):
self.title = title
self.score = score
self.lti_lineitem = lti_lineitem
self.lti_jwt_sub = lti_jwt_sub
def save_to_db(self):
pass
@classmethod
def restore_from_db(cls)
pass
message_launch_data = message_launch.get_launch_data()
lti_jwt_sub = message_launch_data.get('sub') # external user id
endpoint = message_launch_data.get('https://purl.imsglobal.org/spec/lti-ags/claim/endpoint', {})
lineitem = endpoint.get('lineitem')
assignment = Assignment('Test', 0.5, lineitem, external_user_id)
assignment.save_to_db()
- Sending score to the existing external gradebook (
process 2):
assignment = Assignment.restore_from_db()
ags = message_launch.get_ags()
line_item = ags.find_lineitem_by_id(assignment.lti_lineitem)
if not line_item:
raise OutcomeServiceSendScoreError("Lineitem not found in the external LMS: " + assignment.lti_lineitem)
timestamp = datetime.datetime.utcnow().isoformat()
# score: 0 <= assignment.score <= 1
gr = Grade()
gr.set_score_given(assignment.score)\
.set_score_maximum(1)\
.set_timestamp(timestamp)\
.set_activity_progress('Submitted')\
.set_grading_progress('FullyGraded')\
.set_user_id(assignment.lti_jwt_sub)
result = ags.put_grade(gr, line_item)
pylti1.3 does allow setting the resource_id, but not the resource_link_id.
In my opinion it doesn't matter. You can find any lineitem manually using built-in method: ags.find_lineitem(prop_name, prop_value) where prop_name could be id / tag / resourceId / anything
If JWT body doesn't contain https://purl.imsglobal.org/spec/lti-ags/claim/endpoint / lineitem keys it means that LMS item is ungraded.
@dmitry-viskov edx assumes that resourceLinkId exists and has a check for it in order to submit grades from LTIAgsScore
https://github.com/openedx/xblock-lti-consumer/blob/582597a6a7fa758be5b1a32511c4f8acd116394b/lti_consumer/signals.py#L30
If we create a PR to fix it, will you consider it?
@Jack-2025 can you check if #62 fixes it for you?
@Alain1405 and @dmitry-viskov, thanks for your support!
@Alain1405 I applied the PR and modified the code in my original post to include this for both grade posts:
if resource_link_id:
line_item.set_resource_id(resource_link_id)
**line_item.set_resource_link_id(resource_link_id)**
I should mention, I'm also using open edx with programmatic grading enabled. @dmitry-viskov by programmatic grading I mean the mode where the tool is in charge of creating the line items, whereas for the declarative mode the LMS will create one line item for you.
edx will take in both grade posts without error but only the last one is shown for that exercise on the grading page. That's one step forward, before none of the grades were being logged. Maybe this current problem is related to edx?
There are two models of interaction to pushing grades to the platform in the LTI AGS services: Declarative: the platform creates a LineItem (equivalent of a gradebook line/grade) and tools can only push results to that item. Programmatic: the tool uses the AGS endpoints to manage its own line items and grades. The tool is responsible for linking each line item to the resourceLinks, which means that a tool might not link a grade to its respective problem.
Here is the explanation of Declarative VS Programmatic Grades from edX Consumer at https://github.com/openedx/xblock-lti-consumer/blob/582597a6a7fa758be5b1a32511c4f8acd116394b/docs/decisions/0005-lti-1p3-score-linking-improved.rst