From 45c9636964105c97c3fe989ff8e4e91329b5dca7 Mon Sep 17 00:00:00 2001 From: Quitterie Lucas Date: Fri, 3 May 2024 16:05:59 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(xapi)=20add=20ralph-malph=20library?= =?UTF-8?q?=20for=20statements=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finally, we have integrated Ralph in marsha to help generate xAPI statements with Pydantic! However the integration is partial as statements are initialized from the frontend. --- CHANGELOG.md | 1 + src/backend/marsha/core/api/xapi.py | 3 + src/backend/marsha/core/lti/__init__.py | 2 +- .../api/xapi/document/test_from_website.py | 1 + .../tests/api/xapi/video/test_from_website.py | 5 +- src/backend/marsha/core/tests/test_xapi.py | 143 ++++++++++----- .../document/test_statement_from_website.py | 12 +- .../xapi/video/test_statement_from_website.py | 36 +++- src/backend/marsha/core/xapi.py | 168 ++++++++++-------- src/backend/setup.cfg | 1 + 10 files changed, 244 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5154493f..a157407578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- Integrate `ralph-malph` library for xAPI statements generation - Add scaleway storage configuration - Add Peertube pipeline to VOD - Celery task queue diff --git a/src/backend/marsha/core/api/xapi.py b/src/backend/marsha/core/api/xapi.py index 12d21a0bf4..b0973f3a97 100644 --- a/src/backend/marsha/core/api/xapi.py +++ b/src/backend/marsha/core/api/xapi.py @@ -17,6 +17,7 @@ from marsha.core import permissions, serializers from marsha.core.api.base import APIViewMixin from marsha.core.defaults import XAPI_STATEMENT_ID_CACHE +from marsha.core.lti import LTI from marsha.core.xapi import XAPI, get_xapi_statement @@ -41,10 +42,12 @@ def _statement_from_lti( xapi_logger = logging.getLogger(f"xapi.{consumer_site.domain}") # xapi statement enriched with video and jwt_token information + lti = LTI(request, object_instance) xapi_statement = statement_class.from_lti( object_instance, partial_xapi_statement.validated_data, request.resource.token, + lti.origin_url ) # Log the statement in the xapi logger diff --git a/src/backend/marsha/core/lti/__init__.py b/src/backend/marsha/core/lti/__init__.py index ebd73dd43d..0dba7d4062 100644 --- a/src/backend/marsha/core/lti/__init__.py +++ b/src/backend/marsha/core/lti/__init__.py @@ -33,7 +33,7 @@ def __init__(self, request, resource_id=None): request : django.http.request.HttpRequest The request that holds the LTI parameters resource_id : uuid.uuid4 - The primary key of the video targetted by the LTI query as a UUID. + The primary key of the video targeted by the LTI query as a UUID. """ self.resource_id = resource_id diff --git a/src/backend/marsha/core/tests/api/xapi/document/test_from_website.py b/src/backend/marsha/core/tests/api/xapi/document/test_from_website.py index 95d55c0ff4..6de5cfa746 100644 --- a/src/backend/marsha/core/tests/api/xapi/document/test_from_website.py +++ b/src/backend/marsha/core/tests/api/xapi/document/test_from_website.py @@ -24,6 +24,7 @@ def test_xapi_statement_document_resource(self): session_id = str(uuid.uuid4()) jwt_token = UserAccessTokenFactory() + print(jwt_token.__dict__) data = { "verb": { diff --git a/src/backend/marsha/core/tests/api/xapi/video/test_from_website.py b/src/backend/marsha/core/tests/api/xapi/video/test_from_website.py index ae3601781c..29cda6f7c2 100644 --- a/src/backend/marsha/core/tests/api/xapi/video/test_from_website.py +++ b/src/backend/marsha/core/tests/api/xapi/video/test_from_website.py @@ -12,7 +12,7 @@ from logging_ldp.formatters import LDPGELFFormatter from marsha.core.defaults import XAPI_STATEMENT_ID_CACHE -from marsha.core.factories import VideoFactory +from marsha.core.factories import VideoFactory, UserFactory from marsha.core.simple_jwt.factories import UserAccessTokenFactory @@ -199,7 +199,8 @@ def test_xapi_statement_with_two_same_event(self): """ video = VideoFactory() jwt_token = UserAccessTokenFactory() - + user=UserFactory(username="johndoe") + print(user) data = { "id": "7b18195e-e183-4bbf-b8ef-5145ef64ae19", "verb": { diff --git a/src/backend/marsha/core/tests/test_xapi.py b/src/backend/marsha/core/tests/test_xapi.py index 5d76034d08..0fc08d9245 100644 --- a/src/backend/marsha/core/tests/test_xapi.py +++ b/src/backend/marsha/core/tests/test_xapi.py @@ -29,9 +29,10 @@ def test_xapi_statement_missing_user(self): title="test video xapi", ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, ) del jwt_token.payload["user"] @@ -59,7 +60,7 @@ def test_xapi_statement_missing_user(self): } xapi_statement = XAPIVideoStatement() - statement = xapi_statement.from_lti(video, base_statement, jwt_token) + statement = xapi_statement.from_lti(video, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( @@ -80,7 +81,6 @@ def test_xapi_statement_missing_user(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -91,11 +91,17 @@ def test_xapi_statement_missing_user(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "id": "https://w3id.org/xapi/video", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -117,10 +123,12 @@ def test_xapi_statement_enrich_statement(self): title="test video xapi", ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, user__id="b2584aa405540758db2a6278521b6478", + user__username="johndoe", ) base_statement = { @@ -147,12 +155,13 @@ def test_xapi_statement_enrich_statement(self): } xapi_statement = XAPIVideoStatement() - statement = xapi_statement.from_lti(video, base_statement, jwt_token) + statement = xapi_statement.from_lti(video, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( statement["actor"], { + "name": "johndoe", "objectType": "Agent", "account": { "name": "b2584aa405540758db2a6278521b6478", @@ -168,7 +177,6 @@ def test_xapi_statement_enrich_statement(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -179,11 +187,17 @@ def test_xapi_statement_enrich_statement(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/video", + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -206,10 +220,12 @@ def test_xapi_statement_live_video(self): live_type=RAW, ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, user__id="b2584aa405540758db2a6278521b6478", + user__username="johndoe", ) base_statement = { @@ -236,12 +252,13 @@ def test_xapi_statement_live_video(self): } xapi_statement = XAPIVideoStatement() - statement = xapi_statement.from_lti(video, base_statement, jwt_token) + statement = xapi_statement.from_lti(video, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( statement["actor"], { + "name": "johndoe", "objectType": "Agent", "account": { "name": "b2584aa405540758db2a6278521b6478", @@ -257,7 +274,6 @@ def test_xapi_statement_live_video(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -268,11 +284,17 @@ def test_xapi_statement_live_video(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "id": "https://w3id.org/xapi/video", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -296,10 +318,12 @@ def test_xapi_statement_live_video_ended(self): upload_state=READY, ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, user__id="b2584aa405540758db2a6278521b6478", + user__username="johndoe", ) base_statement = { @@ -326,12 +350,13 @@ def test_xapi_statement_live_video_ended(self): } xapi_statement = XAPIVideoStatement() - statement = xapi_statement.from_lti(video, base_statement, jwt_token) + statement = xapi_statement.from_lti(video, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( statement["actor"], { + "name": "johndoe", "objectType": "Agent", "account": { "name": "b2584aa405540758db2a6278521b6478", @@ -347,7 +372,6 @@ def test_xapi_statement_live_video_ended(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -358,11 +382,17 @@ def test_xapi_statement_live_video_ended(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "id": "https://w3id.org/xapi/video", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -434,7 +464,6 @@ def test_xapi_statement_missing_context_id(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -445,7 +474,14 @@ def test_xapi_statement_missing_context_id(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}] + "category": [ + { + "id": "https://w3id.org/xapi/video", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ] }, }, ) @@ -466,10 +502,12 @@ def test_xapi_statement_enrich_statement(self): title="test document xapi", ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, user__id="b2584aa405540758db2a6278521b6478", + user__username="johndoe", ) base_statement = { @@ -487,12 +525,13 @@ def test_xapi_statement_enrich_statement(self): } xapi_statement = XAPIDocumentStatement() - statement = xapi_statement.from_lti(document, base_statement, jwt_token) + statement = xapi_statement.from_lti(document, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( statement["actor"], { + "name": "johndoe", "objectType": "Agent", "account": { "name": "b2584aa405540758db2a6278521b6478", @@ -508,7 +547,6 @@ def test_xapi_statement_enrich_statement(self): "name": {"en-US": "test document xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -519,11 +557,17 @@ def test_xapi_statement_enrich_statement(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/lms"}], + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/lms", + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -547,6 +591,7 @@ def test_xapi_statement_missing_context_id(self): jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", user__id="b2584aa405540758db2a6278521b6478", + user__username="johndoe", ) del jwt_token.payload["context_id"] @@ -571,6 +616,7 @@ def test_xapi_statement_missing_context_id(self): self.assertEqual( statement["actor"], { + "name": "johndoe", "objectType": "Agent", "account": { "name": "b2584aa405540758db2a6278521b6478", @@ -586,7 +632,6 @@ def test_xapi_statement_missing_context_id(self): "name": {"en-US": "test document xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -597,7 +642,14 @@ def test_xapi_statement_missing_context_id(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/lms"}] + "category": [ + { + "id": "https://w3id.org/xapi/lms", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ] }, }, ) @@ -613,9 +665,10 @@ def test_xapi_statement_missing_user_id(self): title="test document xapi", ) + context_id="course-v1:ufr+mathematics+0001" jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, ) del jwt_token.payload["user"] @@ -634,7 +687,7 @@ def test_xapi_statement_missing_user_id(self): } xapi_statement = XAPIDocumentStatement() - statement = xapi_statement.from_lti(document, base_statement, jwt_token) + statement = xapi_statement.from_lti(document, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") self.assertIsNotNone(statement["timestamp"]) self.assertEqual( @@ -655,7 +708,6 @@ def test_xapi_statement_missing_user_id(self): "name": {"en-US": "test document xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -666,11 +718,17 @@ def test_xapi_statement_missing_user_id(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/lms"}], + "category": [ + { + "id": "https://w3id.org/xapi/lms", + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + } + ], "parent": [ { - "id": "course-v1:ufr+mathematics+0001", - "objectType": "Activity", + "id": f"http://testserver/course/{context_id}", "definition": { "type": "http://adlnet.gov/expapi/activities/course" }, @@ -700,9 +758,10 @@ def test_xapi_enrich_and_send_statement(self): title="test video xapi", ) + context_id="course-v1:ufr+mathematics+0001", jwt_token = LTIPlaylistAccessTokenFactory( session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f", - context_id="course-v1:ufr+mathematics+0001", + context_id=context_id, user__id="b2584aa405540758db2a6278521b6478", ) @@ -730,7 +789,7 @@ def test_xapi_enrich_and_send_statement(self): } xapi_statement = XAPIVideoStatement() - statement = xapi_statement.from_lti(video, base_statement, jwt_token) + statement = xapi_statement.from_lti(video, base_statement, jwt_token, course_url=f"http://testserver/course/{context_id}") responses.add( responses.POST, diff --git a/src/backend/marsha/core/tests/xapi/document/test_statement_from_website.py b/src/backend/marsha/core/tests/xapi/document/test_statement_from_website.py index 8c91ed768b..23df7d53d6 100644 --- a/src/backend/marsha/core/tests/xapi/document/test_statement_from_website.py +++ b/src/backend/marsha/core/tests/xapi/document/test_statement_from_website.py @@ -41,11 +41,11 @@ def test_xapi_statement_enrich_statement(self): self.assertEqual( statement["actor"], { + "name": f"{user.username}", "objectType": "Agent", "account": { "name": f"{user.id}", "homePage": "http://marsha.education", - "mbox": "mailto:john@example.org", }, }, ) @@ -57,7 +57,6 @@ def test_xapi_statement_enrich_statement(self): "name": {"en-US": "test document xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -68,7 +67,14 @@ def test_xapi_statement_enrich_statement(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/lms"}] + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/lms", + } + ] }, }, ) diff --git a/src/backend/marsha/core/tests/xapi/video/test_statement_from_website.py b/src/backend/marsha/core/tests/xapi/video/test_statement_from_website.py index 1af2b0e0c2..91254cb308 100644 --- a/src/backend/marsha/core/tests/xapi/video/test_statement_from_website.py +++ b/src/backend/marsha/core/tests/xapi/video/test_statement_from_website.py @@ -51,11 +51,11 @@ def test_xapi_statement_enrich_statement(self): self.assertEqual( statement["actor"], { + "name": f"{user.username}", "objectType": "Agent", "account": { "name": f"{user.id}", "homePage": "http://marsha.education", - "mbox": "mailto:john@example.org", }, }, ) @@ -67,7 +67,6 @@ def test_xapi_statement_enrich_statement(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -78,7 +77,14 @@ def test_xapi_statement_enrich_statement(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/video", + } + ], }, }, ) @@ -128,11 +134,11 @@ def test_xapi_statement_live_video(self): self.assertEqual( statement["actor"], { + "name": "john", "objectType": "Agent", "account": { "name": f"{user.id}", "homePage": "http://marsha.education", - "mbox": "mailto:john@example.org", }, }, ) @@ -144,7 +150,6 @@ def test_xapi_statement_live_video(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -155,7 +160,14 @@ def test_xapi_statement_live_video(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/video", + }, + ], }, }, ) @@ -206,11 +218,11 @@ def test_xapi_statement_live_video_ended(self): self.assertEqual( statement["actor"], { + "name": "john", "objectType": "Agent", "account": { "name": f"{user.id}", "homePage": "http://marsha.education", - "mbox": "mailto:john@example.org", }, }, ) @@ -222,7 +234,6 @@ def test_xapi_statement_live_video_ended(self): "name": {"en-US": "test video xapi"}, }, "id": "uuid://68333c45-4b8c-4018-a195-5d5e1706b838", - "objectType": "Activity", }, ) self.assertEqual( @@ -233,7 +244,14 @@ def test_xapi_statement_live_video_ended(self): "43b4-8452-2037fed588df" }, "contextActivities": { - "category": [{"id": "https://w3id.org/xapi/video"}], + "category": [ + { + "definition": { + "type": "http://adlnet.gov/expapi/activities/profile" + }, + "id": "https://w3id.org/xapi/video", + } + ], }, }, ) diff --git a/src/backend/marsha/core/xapi.py b/src/backend/marsha/core/xapi.py index dd019c6ebf..f13fc924c2 100644 --- a/src/backend/marsha/core/xapi.py +++ b/src/backend/marsha/core/xapi.py @@ -7,6 +7,21 @@ from django.utils import timezone from django.utils.translation import to_locale +from ralph.models.xapi.base.agents import BaseXapiAgentWithAccount +from ralph.models.xapi.base.ifi import BaseXapiAccount +from ralph.models.xapi.concepts.activity_types.scorm_profile import CourseActivity +from ralph.models.xapi.concepts.activity_types.tincan_vocabulary import ( + DocumentActivity, + DocumentActivityDefinition, + WebinarActivity, + WebinarActivityDefinition, +) +from ralph.models.xapi.concepts.activity_types.video import ( + VideoActivity, + VideoActivityDefinition, +) +from ralph.models.xapi.lms.contexts import LMSProfileActivity +from ralph.models.xapi.video.contexts import VideoProfileActivity import requests @@ -33,6 +48,15 @@ def get_user_id(jwt_token): else jwt_token.payload["session_id"] ) + @staticmethod + def get_username(jwt_token): + """Return the user name if present in the JWT token or None otherwise.""" + return ( + jwt_token.payload["user"].get("username") + if jwt_token.payload.get("user") + else None + ) + @staticmethod def get_homepage(resource): """Return the domain associated to the playlist consumer site.""" @@ -45,24 +69,19 @@ def get_locale(self): def get_actor_from_website(self, homepage, user): """Return the actor property from a website context""" - return { - "objectType": "Agent", - "account": { - "homePage": homepage, - "mbox": f"mailto:{user.email}", - "name": str(user.id), - }, - } + return BaseXapiAgentWithAccount( + account=BaseXapiAccount(homePage=homepage, name=str(user.id)), + name=user.username, + ).model_dump(exclude_none=True) - def get_actor_from_lti(self, homepage, user_id): + def get_actor_from_lti(self, homepage, user_id, username): """Return the actor property from a LTI context""" - return { - "objectType": "Agent", - "account": {"name": user_id, "homePage": homepage}, - } + return BaseXapiAgentWithAccount( + name=username, account=BaseXapiAccount(homePage=homepage, name=user_id) + ).model_dump(exclude_none=True) def build_common_statement_properties( - self, statement, homepage, user=None, user_id=None + self, statement, homepage, user=None, user_id=None, username=None # pylint: disable=too-many-arguments ): """build statement properties common to all resources.""" if "id" not in statement: @@ -73,7 +92,7 @@ def build_common_statement_properties( statement["actor"] = ( self.get_actor_from_website(homepage, user) if user - else self.get_actor_from_lti(homepage, user_id) + else self.get_actor_from_lti(homepage, user_id, username) ) return statement @@ -83,28 +102,32 @@ class XAPIDocumentStatement(XAPIStatementMixin): """Object managing statement for document objects.""" # pylint: disable=too-many-arguments - def _build_statement(self, document, statement, homepage, user=None, user_id=None): + def _build_statement( + self, document, statement, homepage, user=None, user_id=None, username=None + ): """Build all common properties for a document.""" if re.match(r"^http(s?):\/\/.*", homepage) is None: homepage = f"http://{homepage}" statement = self.build_common_statement_properties( - statement, homepage, user=user, user_id=user_id + statement, homepage, user=user, user_id=user_id, username=username ) statement["context"].update( - {"contextActivities": {"category": [{"id": "https://w3id.org/xapi/lms"}]}} + { + "contextActivities": { + "category": [LMSProfileActivity().model_dump(exclude_none=True)] + } + } ) - statement["object"] = { - "definition": { - "type": "http://id.tincanapi.com/activitytype/document", - "name": {self.get_locale(): document.title}, - }, - "id": f"uuid://{document.id}", - "objectType": "Activity", - } + statement["object"] = DocumentActivity( + definition=DocumentActivityDefinition( + name={self.get_locale(): document.title} + ), + id=f"uuid://{document.id}", + ).model_dump(exclude_none=True) return statement @@ -140,7 +163,7 @@ def from_website(self, document, statement, current_site, user): document, statement, homepage=current_site.domain, user=user ) - def from_lti(self, document, statement, jwt_token): + def from_lti(self, document, statement, jwt_token, course_url=None): """Compute a valid xapi download activity statement.""" statement = self._build_statement( @@ -148,19 +171,16 @@ def from_lti(self, document, statement, jwt_token): statement, homepage=self.get_homepage(document), user_id=self.get_user_id(jwt_token), + username=self.get_username(jwt_token), ) - if jwt_token.payload.get("context_id"): + if course_url: statement["context"]["contextActivities"].update( { "parent": [ - { - "id": jwt_token.payload["context_id"], - "objectType": "Activity", - "definition": { - "type": "http://adlnet.gov/expapi/activities/course" - }, - } + CourseActivity(id=course_url).model_dump( + exclude_none=True + ) ] } ) @@ -171,45 +191,54 @@ def from_lti(self, document, statement, jwt_token): class XAPIVideoStatement(XAPIStatementMixin): """Object managing statement for video objects.""" - def _get_activity_type(self, video): - """Return the activity type for a given video""" - - activity_type = "https://w3id.org/xapi/video/activity-type/video" - + def _get_object(self, video): + """Return the object xAPI instance for a given video""" # When the video is a live we change the activity to webinar if video.is_live: - activity_type = "http://id.tincanapi.com/activitytype/webinar" - - return activity_type + return WebinarActivity( + id=f"uuid://{video.id}", + definition=WebinarActivityDefinition( + name={self.get_locale(): video.title} + ), + ).model_dump(exclude_none=True) + + return VideoActivity( + id=f"uuid://{video.id}", + definition=VideoActivityDefinition(name={self.get_locale(): video.title}), + ).model_dump(exclude_none=True) # pylint: disable=too-many-arguments - def _build_statement(self, video, statement, homepage, user=None, user_id=None): + def _build_statement( + self, video, statement, homepage, user=None, user_id=None, username=None + ): """Build all common properties for a video.""" if re.match(r"^http(s?):\/\/.*", homepage) is None: homepage = f"http://{homepage}" statement = self.build_common_statement_properties( - statement, homepage, user=user, user_id=user_id - ) - - category_id = ( - "https://w3id.org/xapi/lms" - if statement["verb"]["id"] == "http://id.tincanapi.com/verb/downloaded" - else "https://w3id.org/xapi/video" + statement, homepage, user=user, user_id=user_id, username=username ) - statement["context"].update( - {"contextActivities": {"category": [{"id": category_id}]}} - ) + if statement["verb"]["id"] == "http://id.tincanapi.com/verb/downloaded": + statement["context"].update( + { + "contextActivities": { + "category": [LMSProfileActivity().model_dump(exclude_none=True)] + } + } + ) + else: + statement["context"].update( + { + "contextActivities": { + "category": [ + VideoProfileActivity().model_dump(exclude_none=True) + ] + } + } + ) - statement["object"] = { - "definition": { - "type": self._get_activity_type(video), - "name": {self.get_locale(): video.title}, - }, - "id": f"uuid://{video.id}", - "objectType": "Activity", - } + statement["object"] = self._get_object(video) return statement @@ -251,7 +280,7 @@ def from_website(self, video, statement, current_site, user): video, statement, homepage=current_site.domain, user=user ) - def from_lti(self, video, statement, jwt_token): + def from_lti(self, video, statement, jwt_token, course_url=None): """Compute a valid xapi statement in an LTI context. Parameters @@ -286,19 +315,16 @@ def from_lti(self, video, statement, jwt_token): statement, homepage=self.get_homepage(video), user_id=self.get_user_id(jwt_token), + username=self.get_username(jwt_token), ) - if jwt_token.payload.get("context_id"): + if course_url: statement["context"]["contextActivities"].update( { "parent": [ - { - "id": jwt_token.payload["context_id"], - "objectType": "Activity", - "definition": { - "type": "http://adlnet.gov/expapi/activities/course" - }, - } + CourseActivity(id=course_url).model_dump( + exclude_none=True + ) ] } ) diff --git a/src/backend/setup.cfg b/src/backend/setup.cfg index 218cc971c9..7a34dc99be 100644 --- a/src/backend/setup.cfg +++ b/src/backend/setup.cfg @@ -61,6 +61,7 @@ install_requires = pycaption==2.2.5 PyMuPDF==1.23.26 python-dateutil==2.9.0.post0 + ralph-malph==5.0.0 requests==2.31.0 sentry-sdk==1.41.0 social-auth-app-django==5.4.0