Skip to content

Commit a751e0a

Browse files
committed
🐛(back) ignore XAPI statements from public view without consumer_site
When a video or document is used in a public view and has been created on the website, we have no information at all about the consumer site to use. In that specifice case we want to ignore the XAPI statement.
1 parent d020689 commit a751e0a

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Versioning](https://semver.org/spec/v2.0.0.html).
88

99
## [Unreleased]
1010

11+
### Fixed
12+
13+
- Ignore XAPI statements from public view without consumer_site
14+
1115
## [5.3.0] - 2024-11-04
1216

1317
### Added

src/backend/marsha/core/api/xapi.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
logger = logging.getLogger(__name__)
2525

2626

27+
class NoConsumerSiteError(Exception):
28+
"""Error raised when no consumer site is found."""
29+
30+
2731
class XAPIStatementView(APIViewMixin, APIView):
2832
"""Viewset managing xAPI requests."""
2933

@@ -39,6 +43,13 @@ def _statement_from_lti(
3943
# The resource is used in a LTI context but have been created in the website context
4044
# so the consumer site does not exists on the playlist.
4145
# We have to find it directly from the LTI information we have in the JWT token.
46+
if not request.resource.token.payload.get("consumer_site"):
47+
# If the consumer site is not present in the JWT token, we have no information
48+
# from what context the statement is sent. So we stop here the process.
49+
raise NoConsumerSiteError(
50+
"Consumer site is mandatory in the LTI token."
51+
)
52+
4253
consumer_site = ConsumerSite.objects.get(
4354
pk=request.resource.token.payload.get("consumer_site")
4455
)
@@ -121,16 +132,20 @@ def post(self, request, resource_kind, resource_id):
121132
return Response(partial_xapi_statement.errors, status=400)
122133

123134
if request.resource:
135+
# consumer site in a LTI token is mandatory.
124136
if request.resource.playlist_id != str(object_instance.playlist.id):
125137
return HttpResponseNotFound()
126-
(
127-
statement,
128-
lrs_url,
129-
lrs_auth_token,
130-
lrs_xapi_version,
131-
) = self._statement_from_lti(
132-
request, partial_xapi_statement, statement_class, object_instance
133-
)
138+
try:
139+
(
140+
statement,
141+
lrs_url,
142+
lrs_auth_token,
143+
lrs_xapi_version,
144+
) = self._statement_from_lti(
145+
request, partial_xapi_statement, statement_class, object_instance
146+
)
147+
except NoConsumerSiteError:
148+
return Response(status=400)
134149
else:
135150
statement = self._statement_from_website(
136151
request, partial_xapi_statement, statement_class, object_instance

src/backend/marsha/core/tests/api/xapi/video/test_from_lti.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
PlaylistFactory,
1616
VideoFactory,
1717
)
18-
from marsha.core.simple_jwt.factories import StudentLtiTokenFactory
18+
from marsha.core.simple_jwt.factories import (
19+
PlaylistAccessTokenFactory,
20+
StudentLtiTokenFactory,
21+
)
1922

2023

2124
class XAPIVideoFromLTITest(TestCase):
@@ -188,3 +191,44 @@ def test_send_xapi_statement_from_lti_request_video_no_consumer_site(self):
188191
"objectType": "Activity",
189192
},
190193
)
194+
195+
def test_send_xapi_statement_from_public_video_view_created_from_website(self):
196+
"""
197+
The XAPI statement should be ignored when the video has been created from the website
198+
and used in a public view.
199+
"""
200+
organization = OrganizationFactory()
201+
playlist = PlaylistFactory(
202+
organization=organization, consumer_site=None, lti_id=None
203+
)
204+
video = VideoFactory(
205+
id="7b18195e-e183-4bbf-b8ef-5145ef64ae19",
206+
title="Video 000",
207+
playlist=playlist,
208+
)
209+
# JWT Token used in a public view
210+
jwt_token = PlaylistAccessTokenFactory(
211+
playlist=video.playlist,
212+
)
213+
self.assertIsNotNone(video.playlist.organization)
214+
self.assertIsNone(video.playlist.consumer_site)
215+
216+
data = {
217+
"verb": {
218+
"id": "http://adlnet.gov/expapi/verbs/initialized",
219+
"display": {"en-US": "initialized"},
220+
},
221+
"context": {
222+
"extensions": {"https://w3id.org/xapi/video/extensions/volume": 1}
223+
},
224+
}
225+
226+
response = self.client.post(
227+
f"/xapi/video/{video.id}/",
228+
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
229+
data=json.dumps(data),
230+
content_type="application/json",
231+
)
232+
233+
self.assertEqual(response.status_code, 400)
234+
self.assertEqual(self.log_stream.getvalue(), "")

0 commit comments

Comments
 (0)