We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Sending a xAPI statement to a LRS not working anymore
## Bug Report

**Problematic Behavior**

When a LRS is configured in a consumer site, sending a xapi statement is failing

https://gip-fun-mooc.sentry.io/share/issue/081e7857e01544d3bd5b5f93d573c428/

**Expected behavior/code**

When a LRS is correctly configured for a given consumer site, the statement should be sent to the LRS.

**Steps to Reproduce**
1. Configure a LRS in a consumer site
2. Navigate on an existing video
3. And then the bug happens!


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/backend/marsha/core/xapi.py`
Content:
```
1 """XAPI module."""
2 
3 import re
4 import uuid
5 
6 from django.conf import settings
7 from django.utils import timezone
8 from django.utils.translation import to_locale
9 
10 import requests
11 
12 
13 def get_xapi_statement(resource):
14     """Return the xapi object statement based on the required resource type."""
15     if resource == "video":
16         return XAPIVideoStatement()
17 
18     if resource == "document":
19         return XAPIDocumentStatement()
20 
21     raise NotImplementedError
22 
23 
24 class XAPIStatementMixin:
25     """Mixin used by xapi statements."""
26 
27     @staticmethod
28     def get_user_id(jwt_token):
29         """Return the user id if present in the JWT token or the session_is otherwise."""
30         return (
31             jwt_token.payload["user"].get("id")
32             if jwt_token.payload.get("user")
33             else jwt_token.payload["session_id"]
34         )
35 
36     @staticmethod
37     def get_homepage(resource):
38         """Return the domain associated to the playlist consumer site."""
39         return resource.playlist.consumer_site.domain
40 
41     def get_locale(self):
42         """Return the locale formatted with a - instead of _"""
43 
44         return to_locale(settings.LANGUAGE_CODE).replace("_", "-")
45 
46     def get_actor_from_website(self, homepage, user):
47         """Return the actor property from a website context"""
48         return {
49             "objectType": "Agent",
50             "account": {
51                 "homePage": homepage,
52                 "mbox": f"mailto:{user.email}",
53                 "name": str(user.id),
54             },
55         }
56 
57     def get_actor_from_lti(self, homepage, user_id):
58         """Return the actor property from a LTI context"""
59         return {
60             "objectType": "Agent",
61             "account": {"name": user_id, "homePage": homepage},
62         }
63 
64     def build_common_statement_properties(
65         self, statement, homepage, user=None, user_id=None
66     ):
67         """build statement properties common to all resources."""
68         if "id" not in statement:
69             statement["id"] = str(uuid.uuid4())
70 
71         statement["timestamp"] = timezone.now().isoformat()
72 
73         statement["actor"] = (
74             self.get_actor_from_website(homepage, user)
75             if user
76             else self.get_actor_from_lti(homepage, user_id)
77         )
78 
79         return statement
80 
81 
82 class XAPIDocumentStatement(XAPIStatementMixin):
83     """Object managing statement for document objects."""
84 
85     # pylint: disable=too-many-arguments
86     def _build_statement(self, document, statement, homepage, user=None, user_id=None):
87         """Build all common properties for a document."""
88 
89         if re.match(r"^http(s?):\/\/.*", homepage) is None:
90             homepage = f"http://{homepage}"
91 
92         statement = self.build_common_statement_properties(
93             statement, homepage, user=user, user_id=user_id
94         )
95 
96         statement["context"].update(
97             {"contextActivities": {"category": [{"id": "https://w3id.org/xapi/lms"}]}}
98         )
99 
100         statement["object"] = {
101             "definition": {
102                 "type": "http://id.tincanapi.com/activitytype/document",
103                 "name": {self.get_locale(): document.title},
104             },
105             "id": f"uuid://{document.id}",
106             "objectType": "Activity",
107         }
108 
109         return statement
110 
111     def from_website(self, document, statement, current_site, user):
112         """Compute a valid xapi statement in a website context.
113 
114         Parameters
115         ----------
116         document : Type[marsha.core.models.Document]
117             The document object used in the xAPI statement
118 
119         statement : dictionary
120             Statement containing base information to send to the LRS
121             An example of expected statement:
122             {
123                 "verb": {
124                     "id": "http://adlnet.gov/expapi/verbs/initialized",
125                     "display": {
126                         "en-US": "initialized"
127                     }
128                 },
129             }
130 
131         current_site : Type[django.contrib.sites.models.Site]
132             The current site used to send the XAPI request
133 
134         user: Type[marsha.core.models.User]
135             The connected user who sent the XAPI request
136 
137         """
138 
139         return self._build_statement(
140             document, statement, homepage=current_site.domain, user=user
141         )
142 
143     def from_lti(self, document, statement, jwt_token):
144         """Compute a valid xapi download activity statement."""
145 
146         statement = self._build_statement(
147             document,
148             statement,
149             homepage=self.get_homepage(document),
150             user_id=self.get_user_id(jwt_token),
151         )
152 
153         if jwt_token.payload.get("context_id"):
154             statement["context"]["contextActivities"].update(
155                 {
156                     "parent": [
157                         {
158                             "id": jwt_token.payload["context_id"],
159                             "objectType": "Activity",
160                             "definition": {
161                                 "type": "http://adlnet.gov/expapi/activities/course"
162                             },
163                         }
164                     ]
165                 }
166             )
167 
168         return statement
169 
170 
171 class XAPIVideoStatement(XAPIStatementMixin):
172     """Object managing statement for video objects."""
173 
174     def _get_activity_type(self, video):
175         """Return the activity type for a given video"""
176 
177         activity_type = "https://w3id.org/xapi/video/activity-type/video"
178 
179         # When the video is a live we change the activity to webinar
180         if video.is_live:
181             activity_type = "http://id.tincanapi.com/activitytype/webinar"
182 
183         return activity_type
184 
185     # pylint: disable=too-many-arguments
186     def _build_statement(self, video, statement, homepage, user=None, user_id=None):
187         """Build all common properties for a video."""
188         if re.match(r"^http(s?):\/\/.*", homepage) is None:
189             homepage = f"http://{homepage}"
190 
191         statement = self.build_common_statement_properties(
192             statement, homepage, user=user, user_id=user_id
193         )
194 
195         category_id = (
196             "https://w3id.org/xapi/lms"
197             if statement["verb"]["id"] == "http://id.tincanapi.com/verb/downloaded"
198             else "https://w3id.org/xapi/video"
199         )
200 
201         statement["context"].update(
202             {"contextActivities": {"category": [{"id": category_id}]}}
203         )
204 
205         statement["object"] = {
206             "definition": {
207                 "type": self._get_activity_type(video),
208                 "name": {self.get_locale(): video.title},
209             },
210             "id": f"uuid://{video.id}",
211             "objectType": "Activity",
212         }
213 
214         return statement
215 
216     def from_website(self, video, statement, current_site, user):
217         """Compute a valid xapi statement in a website context.
218 
219         Parameters
220         ----------
221         video : Type[.models/videos]
222             The video object used in the xAPI statement
223 
224         statement : dictionary
225             Statement containing base information to send to the LRS
226             An example of expected statement:
227             {
228                 "verb": {
229                     "id": "http://adlnet.gov/expapi/verbs/initialized",
230                     "display": {
231                         "en-US": "initialized"
232                     }
233                 },
234                 "context": {
235                     "extensions": {
236                         "https://w3id.org/xapi/video/extensions/volume": 1,
237                         "https://w3id.org/xapi/video/extensions/video-playback-size": "640x264",
238                     }
239                 }
240             }
241 
242         current_site : Type[django.contrib.sites.models.Site]
243             The current site used to send the XAPI request
244 
245         user: Type[marsha.core.models.User]
246             The connected user who sent the XAPI request
247 
248         """
249 
250         return self._build_statement(
251             video, statement, homepage=current_site.domain, user=user
252         )
253 
254     def from_lti(self, video, statement, jwt_token):
255         """Compute a valid xapi statement in an LTI context.
256 
257         Parameters
258         ----------
259         video : Type[.models/videos]
260             The video object used in the xAPI statement
261 
262         statement : dictionary
263             Statement containing base information to send to the LRS
264             An example of expected statement:
265             {
266                 "verb": {
267                     "id": "http://adlnet.gov/expapi/verbs/initialized",
268                     "display": {
269                         "en-US": "initialized"
270                     }
271                 },
272                 "context": {
273                     "extensions": {
274                         "https://w3id.org/xapi/video/extensions/volume": 1,
275                         "https://w3id.org/xapi/video/extensions/video-playback-size": "640x264",
276                     }
277                 }
278             }
279 
280         jwt_token : Type[rest_framework_simplejwt.tokens.AccessToken]
281             A jwt token containing the context used to enrich the xapi statement
282 
283         """
284         statement = self._build_statement(
285             video,
286             statement,
287             homepage=self.get_homepage(video),
288             user_id=self.get_user_id(jwt_token),
289         )
290 
291         if jwt_token.payload.get("context_id"):
292             statement["context"]["contextActivities"].update(
293                 {
294                     "parent": [
295                         {
296                             "id": jwt_token.payload["context_id"],
297                             "objectType": "Activity",
298                             "definition": {
299                                 "type": "http://adlnet.gov/expapi/activities/course"
300                             },
301                         }
302                     ]
303                 }
304             )
305 
306         return statement
307 
308 
309 class XAPI:
310     """The XAPI object compute statements and send them to a LRS."""
311 
312     def __init__(self, url, auth_token, xapi_version="1.0.3"):
313         """Initialize the XAPI module.
314 
315         Parameters
316         ----------
317         url: string
318             The LRS endpoint to fetch
319 
320         auth_token: string
321             The basic_auth token used to authenticate on the LRS
322 
323         xapi_version: string
324             The xAPI version used.
325 
326         """
327         self.url = url
328         self.auth_token = auth_token
329         self.xapi_version = xapi_version
330 
331     def send(self, xapi_statement):
332         """Send the statement to a LRS.
333 
334         Parameters
335         ----------
336         statement : Type[.XAPIStatement]
337 
338         """
339         headers = {
340             "Authorization": self.auth_token,
341             "Content-Type": "application/json",
342             "X-Experience-API-Version": self.xapi_version,
343         }
344 
345         response = requests.post(
346             self.url,
347             json=xapi_statement.get_statement(),
348             headers=headers,
349             timeout=settings.STAT_BACKEND_TIMEOUT,
350         )
351 
352         response.raise_for_status()
353
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/backend/marsha/core/xapi.py b/src/backend/marsha/core/xapi.py
--- a/src/backend/marsha/core/xapi.py
+++ b/src/backend/marsha/core/xapi.py
@@ -344,7 +344,7 @@
 
         response = requests.post(
             self.url,
-            json=xapi_statement.get_statement(),
+            json=xapi_statement,
             headers=headers,
             timeout=settings.STAT_BACKEND_TIMEOUT,
         )

{"golden_diff": "diff --git a/src/backend/marsha/core/xapi.py b/src/backend/marsha/core/xapi.py\n--- a/src/backend/marsha/core/xapi.py\n+++ b/src/backend/marsha/core/xapi.py\n@@ -344,7 +344,7 @@\n \n         response = requests.post(\n             self.url,\n-            json=xapi_statement.get_statement(),\n+            json=xapi_statement,\n             headers=headers,\n             timeout=settings.STAT_BACKEND_TIMEOUT,\n         )\n", "issue": "Sending a xAPI statement to a LRS not working anymore\n## Bug Report\r\n\r\n**Problematic Behavior**\r\n\r\nWhen a LRS is configured in a consumer site, sending a xapi statement is failing\r\n\r\nhttps://gip-fun-mooc.sentry.io/share/issue/081e7857e01544d3bd5b5f93d573c428/\r\n\r\n**Expected behavior/code**\r\n\r\nWhen a LRS is correctly configured for a given consumer site, the statement should be sent to the LRS.\r\n\r\n**Steps to Reproduce**\r\n1. Configure a LRS in a consumer site\r\n2. Navigate on an existing video\r\n3. And then the bug happens!\r\n\n", "before_files": [{"content": "\"\"\"XAPI module.\"\"\"\n\nimport re\nimport uuid\n\nfrom django.conf import settings\nfrom django.utils import timezone\nfrom django.utils.translation import to_locale\n\nimport requests\n\n\ndef get_xapi_statement(resource):\n    \"\"\"Return the xapi object statement based on the required resource type.\"\"\"\n    if resource == \"video\":\n        return XAPIVideoStatement()\n\n    if resource == \"document\":\n        return XAPIDocumentStatement()\n\n    raise NotImplementedError\n\n\nclass XAPIStatementMixin:\n    \"\"\"Mixin used by xapi statements.\"\"\"\n\n    @staticmethod\n    def get_user_id(jwt_token):\n        \"\"\"Return the user id if present in the JWT token or the session_is otherwise.\"\"\"\n        return (\n            jwt_token.payload[\"user\"].get(\"id\")\n            if jwt_token.payload.get(\"user\")\n            else jwt_token.payload[\"session_id\"]\n        )\n\n    @staticmethod\n    def get_homepage(resource):\n        \"\"\"Return the domain associated to the playlist consumer site.\"\"\"\n        return resource.playlist.consumer_site.domain\n\n    def get_locale(self):\n        \"\"\"Return the locale formatted with a - instead of _\"\"\"\n\n        return to_locale(settings.LANGUAGE_CODE).replace(\"_\", \"-\")\n\n    def get_actor_from_website(self, homepage, user):\n        \"\"\"Return the actor property from a website context\"\"\"\n        return {\n            \"objectType\": \"Agent\",\n            \"account\": {\n                \"homePage\": homepage,\n                \"mbox\": f\"mailto:{user.email}\",\n                \"name\": str(user.id),\n            },\n        }\n\n    def get_actor_from_lti(self, homepage, user_id):\n        \"\"\"Return the actor property from a LTI context\"\"\"\n        return {\n            \"objectType\": \"Agent\",\n            \"account\": {\"name\": user_id, \"homePage\": homepage},\n        }\n\n    def build_common_statement_properties(\n        self, statement, homepage, user=None, user_id=None\n    ):\n        \"\"\"build statement properties common to all resources.\"\"\"\n        if \"id\" not in statement:\n            statement[\"id\"] = str(uuid.uuid4())\n\n        statement[\"timestamp\"] = timezone.now().isoformat()\n\n        statement[\"actor\"] = (\n            self.get_actor_from_website(homepage, user)\n            if user\n            else self.get_actor_from_lti(homepage, user_id)\n        )\n\n        return statement\n\n\nclass XAPIDocumentStatement(XAPIStatementMixin):\n    \"\"\"Object managing statement for document objects.\"\"\"\n\n    # pylint: disable=too-many-arguments\n    def _build_statement(self, document, statement, homepage, user=None, user_id=None):\n        \"\"\"Build all common properties for a document.\"\"\"\n\n        if re.match(r\"^http(s?):\\/\\/.*\", homepage) is None:\n            homepage = f\"http://{homepage}\"\n\n        statement = self.build_common_statement_properties(\n            statement, homepage, user=user, user_id=user_id\n        )\n\n        statement[\"context\"].update(\n            {\"contextActivities\": {\"category\": [{\"id\": \"https://w3id.org/xapi/lms\"}]}}\n        )\n\n        statement[\"object\"] = {\n            \"definition\": {\n                \"type\": \"http://id.tincanapi.com/activitytype/document\",\n                \"name\": {self.get_locale(): document.title},\n            },\n            \"id\": f\"uuid://{document.id}\",\n            \"objectType\": \"Activity\",\n        }\n\n        return statement\n\n    def from_website(self, document, statement, current_site, user):\n        \"\"\"Compute a valid xapi statement in a website context.\n\n        Parameters\n        ----------\n        document : Type[marsha.core.models.Document]\n            The document object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n            }\n\n        current_site : Type[django.contrib.sites.models.Site]\n            The current site used to send the XAPI request\n\n        user: Type[marsha.core.models.User]\n            The connected user who sent the XAPI request\n\n        \"\"\"\n\n        return self._build_statement(\n            document, statement, homepage=current_site.domain, user=user\n        )\n\n    def from_lti(self, document, statement, jwt_token):\n        \"\"\"Compute a valid xapi download activity statement.\"\"\"\n\n        statement = self._build_statement(\n            document,\n            statement,\n            homepage=self.get_homepage(document),\n            user_id=self.get_user_id(jwt_token),\n        )\n\n        if jwt_token.payload.get(\"context_id\"):\n            statement[\"context\"][\"contextActivities\"].update(\n                {\n                    \"parent\": [\n                        {\n                            \"id\": jwt_token.payload[\"context_id\"],\n                            \"objectType\": \"Activity\",\n                            \"definition\": {\n                                \"type\": \"http://adlnet.gov/expapi/activities/course\"\n                            },\n                        }\n                    ]\n                }\n            )\n\n        return statement\n\n\nclass XAPIVideoStatement(XAPIStatementMixin):\n    \"\"\"Object managing statement for video objects.\"\"\"\n\n    def _get_activity_type(self, video):\n        \"\"\"Return the activity type for a given video\"\"\"\n\n        activity_type = \"https://w3id.org/xapi/video/activity-type/video\"\n\n        # When the video is a live we change the activity to webinar\n        if video.is_live:\n            activity_type = \"http://id.tincanapi.com/activitytype/webinar\"\n\n        return activity_type\n\n    # pylint: disable=too-many-arguments\n    def _build_statement(self, video, statement, homepage, user=None, user_id=None):\n        \"\"\"Build all common properties for a video.\"\"\"\n        if re.match(r\"^http(s?):\\/\\/.*\", homepage) is None:\n            homepage = f\"http://{homepage}\"\n\n        statement = self.build_common_statement_properties(\n            statement, homepage, user=user, user_id=user_id\n        )\n\n        category_id = (\n            \"https://w3id.org/xapi/lms\"\n            if statement[\"verb\"][\"id\"] == \"http://id.tincanapi.com/verb/downloaded\"\n            else \"https://w3id.org/xapi/video\"\n        )\n\n        statement[\"context\"].update(\n            {\"contextActivities\": {\"category\": [{\"id\": category_id}]}}\n        )\n\n        statement[\"object\"] = {\n            \"definition\": {\n                \"type\": self._get_activity_type(video),\n                \"name\": {self.get_locale(): video.title},\n            },\n            \"id\": f\"uuid://{video.id}\",\n            \"objectType\": \"Activity\",\n        }\n\n        return statement\n\n    def from_website(self, video, statement, current_site, user):\n        \"\"\"Compute a valid xapi statement in a website context.\n\n        Parameters\n        ----------\n        video : Type[.models/videos]\n            The video object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n                \"context\": {\n                    \"extensions\": {\n                        \"https://w3id.org/xapi/video/extensions/volume\": 1,\n                        \"https://w3id.org/xapi/video/extensions/video-playback-size\": \"640x264\",\n                    }\n                }\n            }\n\n        current_site : Type[django.contrib.sites.models.Site]\n            The current site used to send the XAPI request\n\n        user: Type[marsha.core.models.User]\n            The connected user who sent the XAPI request\n\n        \"\"\"\n\n        return self._build_statement(\n            video, statement, homepage=current_site.domain, user=user\n        )\n\n    def from_lti(self, video, statement, jwt_token):\n        \"\"\"Compute a valid xapi statement in an LTI context.\n\n        Parameters\n        ----------\n        video : Type[.models/videos]\n            The video object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n                \"context\": {\n                    \"extensions\": {\n                        \"https://w3id.org/xapi/video/extensions/volume\": 1,\n                        \"https://w3id.org/xapi/video/extensions/video-playback-size\": \"640x264\",\n                    }\n                }\n            }\n\n        jwt_token : Type[rest_framework_simplejwt.tokens.AccessToken]\n            A jwt token containing the context used to enrich the xapi statement\n\n        \"\"\"\n        statement = self._build_statement(\n            video,\n            statement,\n            homepage=self.get_homepage(video),\n            user_id=self.get_user_id(jwt_token),\n        )\n\n        if jwt_token.payload.get(\"context_id\"):\n            statement[\"context\"][\"contextActivities\"].update(\n                {\n                    \"parent\": [\n                        {\n                            \"id\": jwt_token.payload[\"context_id\"],\n                            \"objectType\": \"Activity\",\n                            \"definition\": {\n                                \"type\": \"http://adlnet.gov/expapi/activities/course\"\n                            },\n                        }\n                    ]\n                }\n            )\n\n        return statement\n\n\nclass XAPI:\n    \"\"\"The XAPI object compute statements and send them to a LRS.\"\"\"\n\n    def __init__(self, url, auth_token, xapi_version=\"1.0.3\"):\n        \"\"\"Initialize the XAPI module.\n\n        Parameters\n        ----------\n        url: string\n            The LRS endpoint to fetch\n\n        auth_token: string\n            The basic_auth token used to authenticate on the LRS\n\n        xapi_version: string\n            The xAPI version used.\n\n        \"\"\"\n        self.url = url\n        self.auth_token = auth_token\n        self.xapi_version = xapi_version\n\n    def send(self, xapi_statement):\n        \"\"\"Send the statement to a LRS.\n\n        Parameters\n        ----------\n        statement : Type[.XAPIStatement]\n\n        \"\"\"\n        headers = {\n            \"Authorization\": self.auth_token,\n            \"Content-Type\": \"application/json\",\n            \"X-Experience-API-Version\": self.xapi_version,\n        }\n\n        response = requests.post(\n            self.url,\n            json=xapi_statement.get_statement(),\n            headers=headers,\n            timeout=settings.STAT_BACKEND_TIMEOUT,\n        )\n\n        response.raise_for_status()\n", "path": "src/backend/marsha/core/xapi.py"}], "after_files": [{"content": "\"\"\"XAPI module.\"\"\"\n\nimport re\nimport uuid\n\nfrom django.conf import settings\nfrom django.utils import timezone\nfrom django.utils.translation import to_locale\n\nimport requests\n\n\ndef get_xapi_statement(resource):\n    \"\"\"Return the xapi object statement based on the required resource type.\"\"\"\n    if resource == \"video\":\n        return XAPIVideoStatement()\n\n    if resource == \"document\":\n        return XAPIDocumentStatement()\n\n    raise NotImplementedError\n\n\nclass XAPIStatementMixin:\n    \"\"\"Mixin used by xapi statements.\"\"\"\n\n    @staticmethod\n    def get_user_id(jwt_token):\n        \"\"\"Return the user id if present in the JWT token or the session_is otherwise.\"\"\"\n        return (\n            jwt_token.payload[\"user\"].get(\"id\")\n            if jwt_token.payload.get(\"user\")\n            else jwt_token.payload[\"session_id\"]\n        )\n\n    @staticmethod\n    def get_homepage(resource):\n        \"\"\"Return the domain associated to the playlist consumer site.\"\"\"\n        return resource.playlist.consumer_site.domain\n\n    def get_locale(self):\n        \"\"\"Return the locale formatted with a - instead of _\"\"\"\n\n        return to_locale(settings.LANGUAGE_CODE).replace(\"_\", \"-\")\n\n    def get_actor_from_website(self, homepage, user):\n        \"\"\"Return the actor property from a website context\"\"\"\n        return {\n            \"objectType\": \"Agent\",\n            \"account\": {\n                \"homePage\": homepage,\n                \"mbox\": f\"mailto:{user.email}\",\n                \"name\": str(user.id),\n            },\n        }\n\n    def get_actor_from_lti(self, homepage, user_id):\n        \"\"\"Return the actor property from a LTI context\"\"\"\n        return {\n            \"objectType\": \"Agent\",\n            \"account\": {\"name\": user_id, \"homePage\": homepage},\n        }\n\n    def build_common_statement_properties(\n        self, statement, homepage, user=None, user_id=None\n    ):\n        \"\"\"build statement properties common to all resources.\"\"\"\n        if \"id\" not in statement:\n            statement[\"id\"] = str(uuid.uuid4())\n\n        statement[\"timestamp\"] = timezone.now().isoformat()\n\n        statement[\"actor\"] = (\n            self.get_actor_from_website(homepage, user)\n            if user\n            else self.get_actor_from_lti(homepage, user_id)\n        )\n\n        return statement\n\n\nclass XAPIDocumentStatement(XAPIStatementMixin):\n    \"\"\"Object managing statement for document objects.\"\"\"\n\n    # pylint: disable=too-many-arguments\n    def _build_statement(self, document, statement, homepage, user=None, user_id=None):\n        \"\"\"Build all common properties for a document.\"\"\"\n\n        if re.match(r\"^http(s?):\\/\\/.*\", homepage) is None:\n            homepage = f\"http://{homepage}\"\n\n        statement = self.build_common_statement_properties(\n            statement, homepage, user=user, user_id=user_id\n        )\n\n        statement[\"context\"].update(\n            {\"contextActivities\": {\"category\": [{\"id\": \"https://w3id.org/xapi/lms\"}]}}\n        )\n\n        statement[\"object\"] = {\n            \"definition\": {\n                \"type\": \"http://id.tincanapi.com/activitytype/document\",\n                \"name\": {self.get_locale(): document.title},\n            },\n            \"id\": f\"uuid://{document.id}\",\n            \"objectType\": \"Activity\",\n        }\n\n        return statement\n\n    def from_website(self, document, statement, current_site, user):\n        \"\"\"Compute a valid xapi statement in a website context.\n\n        Parameters\n        ----------\n        document : Type[marsha.core.models.Document]\n            The document object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n            }\n\n        current_site : Type[django.contrib.sites.models.Site]\n            The current site used to send the XAPI request\n\n        user: Type[marsha.core.models.User]\n            The connected user who sent the XAPI request\n\n        \"\"\"\n\n        return self._build_statement(\n            document, statement, homepage=current_site.domain, user=user\n        )\n\n    def from_lti(self, document, statement, jwt_token):\n        \"\"\"Compute a valid xapi download activity statement.\"\"\"\n\n        statement = self._build_statement(\n            document,\n            statement,\n            homepage=self.get_homepage(document),\n            user_id=self.get_user_id(jwt_token),\n        )\n\n        if jwt_token.payload.get(\"context_id\"):\n            statement[\"context\"][\"contextActivities\"].update(\n                {\n                    \"parent\": [\n                        {\n                            \"id\": jwt_token.payload[\"context_id\"],\n                            \"objectType\": \"Activity\",\n                            \"definition\": {\n                                \"type\": \"http://adlnet.gov/expapi/activities/course\"\n                            },\n                        }\n                    ]\n                }\n            )\n\n        return statement\n\n\nclass XAPIVideoStatement(XAPIStatementMixin):\n    \"\"\"Object managing statement for video objects.\"\"\"\n\n    def _get_activity_type(self, video):\n        \"\"\"Return the activity type for a given video\"\"\"\n\n        activity_type = \"https://w3id.org/xapi/video/activity-type/video\"\n\n        # When the video is a live we change the activity to webinar\n        if video.is_live:\n            activity_type = \"http://id.tincanapi.com/activitytype/webinar\"\n\n        return activity_type\n\n    # pylint: disable=too-many-arguments\n    def _build_statement(self, video, statement, homepage, user=None, user_id=None):\n        \"\"\"Build all common properties for a video.\"\"\"\n        if re.match(r\"^http(s?):\\/\\/.*\", homepage) is None:\n            homepage = f\"http://{homepage}\"\n\n        statement = self.build_common_statement_properties(\n            statement, homepage, user=user, user_id=user_id\n        )\n\n        category_id = (\n            \"https://w3id.org/xapi/lms\"\n            if statement[\"verb\"][\"id\"] == \"http://id.tincanapi.com/verb/downloaded\"\n            else \"https://w3id.org/xapi/video\"\n        )\n\n        statement[\"context\"].update(\n            {\"contextActivities\": {\"category\": [{\"id\": category_id}]}}\n        )\n\n        statement[\"object\"] = {\n            \"definition\": {\n                \"type\": self._get_activity_type(video),\n                \"name\": {self.get_locale(): video.title},\n            },\n            \"id\": f\"uuid://{video.id}\",\n            \"objectType\": \"Activity\",\n        }\n\n        return statement\n\n    def from_website(self, video, statement, current_site, user):\n        \"\"\"Compute a valid xapi statement in a website context.\n\n        Parameters\n        ----------\n        video : Type[.models/videos]\n            The video object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n                \"context\": {\n                    \"extensions\": {\n                        \"https://w3id.org/xapi/video/extensions/volume\": 1,\n                        \"https://w3id.org/xapi/video/extensions/video-playback-size\": \"640x264\",\n                    }\n                }\n            }\n\n        current_site : Type[django.contrib.sites.models.Site]\n            The current site used to send the XAPI request\n\n        user: Type[marsha.core.models.User]\n            The connected user who sent the XAPI request\n\n        \"\"\"\n\n        return self._build_statement(\n            video, statement, homepage=current_site.domain, user=user\n        )\n\n    def from_lti(self, video, statement, jwt_token):\n        \"\"\"Compute a valid xapi statement in an LTI context.\n\n        Parameters\n        ----------\n        video : Type[.models/videos]\n            The video object used in the xAPI statement\n\n        statement : dictionary\n            Statement containing base information to send to the LRS\n            An example of expected statement:\n            {\n                \"verb\": {\n                    \"id\": \"http://adlnet.gov/expapi/verbs/initialized\",\n                    \"display\": {\n                        \"en-US\": \"initialized\"\n                    }\n                },\n                \"context\": {\n                    \"extensions\": {\n                        \"https://w3id.org/xapi/video/extensions/volume\": 1,\n                        \"https://w3id.org/xapi/video/extensions/video-playback-size\": \"640x264\",\n                    }\n                }\n            }\n\n        jwt_token : Type[rest_framework_simplejwt.tokens.AccessToken]\n            A jwt token containing the context used to enrich the xapi statement\n\n        \"\"\"\n        statement = self._build_statement(\n            video,\n            statement,\n            homepage=self.get_homepage(video),\n            user_id=self.get_user_id(jwt_token),\n        )\n\n        if jwt_token.payload.get(\"context_id\"):\n            statement[\"context\"][\"contextActivities\"].update(\n                {\n                    \"parent\": [\n                        {\n                            \"id\": jwt_token.payload[\"context_id\"],\n                            \"objectType\": \"Activity\",\n                            \"definition\": {\n                                \"type\": \"http://adlnet.gov/expapi/activities/course\"\n                            },\n                        }\n                    ]\n                }\n            )\n\n        return statement\n\n\nclass XAPI:\n    \"\"\"The XAPI object compute statements and send them to a LRS.\"\"\"\n\n    def __init__(self, url, auth_token, xapi_version=\"1.0.3\"):\n        \"\"\"Initialize the XAPI module.\n\n        Parameters\n        ----------\n        url: string\n            The LRS endpoint to fetch\n\n        auth_token: string\n            The basic_auth token used to authenticate on the LRS\n\n        xapi_version: string\n            The xAPI version used.\n\n        \"\"\"\n        self.url = url\n        self.auth_token = auth_token\n        self.xapi_version = xapi_version\n\n    def send(self, xapi_statement):\n        \"\"\"Send the statement to a LRS.\n\n        Parameters\n        ----------\n        statement : Type[.XAPIStatement]\n\n        \"\"\"\n        headers = {\n            \"Authorization\": self.auth_token,\n            \"Content-Type\": \"application/json\",\n            \"X-Experience-API-Version\": self.xapi_version,\n        }\n\n        response = requests.post(\n            self.url,\n            json=xapi_statement,\n            headers=headers,\n            timeout=settings.STAT_BACKEND_TIMEOUT,\n        )\n\n        response.raise_for_status()\n", "path": "src/backend/marsha/core/xapi.py"}]}