We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Configure sentry in standalone site
## Feature Request

**Is your feature request related to a problem or unsupported use case? Please describe.**

Sentry is not configured in the standalone site and we have no feedback is there are errors.

**Describe the solution you'd like**

Find a solution to configure sentry. Maybe by fetching th configuration on a dedicated api endpoint



--- 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/urls.py`
Content:
```
1 """Marsha URLs configuration."""
2 import re
3 
4 from django.conf import settings
5 from django.contrib import admin
6 from django.urls import include, path, re_path, register_converter
7 from django.views.decorators.cache import cache_page
8 
9 from rest_framework.routers import DefaultRouter
10 
11 from marsha.core import models
12 from marsha.core.api import (
13     ChallengeAuthenticationView,
14     DocumentViewSet,
15     LiveSessionViewSet,
16     OrganizationViewSet,
17     PlaylistAccessViewSet,
18     PlaylistViewSet,
19     PortabilityResourceViewSet,
20     SharedLiveMediaViewSet,
21     ThumbnailViewSet,
22     TimedTextTrackViewSet,
23     UserViewSet,
24     VideoViewSet,
25     XAPIStatementView,
26     pairing_challenge,
27     recording_slices_manifest,
28     recording_slices_state,
29     update_state,
30 )
31 from marsha.core.api.lti_user_association import LtiUserAssociationViewSet
32 from marsha.core.urls.converters import XAPIResourceKindConverter
33 from marsha.core.utils.lti_select_utils import get_lti_select_resources
34 from marsha.core.views import (
35     DocumentLTIView,
36     DocumentView,
37     LTIConfigView,
38     LTIRespondView,
39     LTISelectView,
40     RemindersCancelView,
41     SiteView,
42     VideoLTIView,
43     VideoView,
44 )
45 from marsha.development.api import local_document_upload, local_video_upload
46 
47 
48 register_converter(XAPIResourceKindConverter, "xapi_resource_kind")
49 
50 LTI_SELECT_ROUTE_PATTERN = (
51     rf"lti/select/((?P<resource_kind>{'|'.join(get_lti_select_resources().keys())})/)?$"
52 )
53 
54 router = DefaultRouter()
55 router.register(models.Video.RESOURCE_NAME, VideoViewSet, basename="videos")
56 router.register(models.Document.RESOURCE_NAME, DocumentViewSet, basename="documents")
57 router.register(
58     models.TimedTextTrack.RESOURCE_NAME,
59     TimedTextTrackViewSet,
60     basename="timed_text_tracks",
61 )
62 router.register(models.Thumbnail.RESOURCE_NAME, ThumbnailViewSet, basename="thumbnails")
63 router.register("organizations", OrganizationViewSet, basename="organizations")
64 router.register("playlists", PlaylistViewSet, basename="playlists")
65 router.register(
66     "playlist-accesses", PlaylistAccessViewSet, basename="playlist_accesses"
67 )
68 router.register(
69     "portability-requests", PortabilityResourceViewSet, basename="portability_requests"
70 )
71 router.register("users", UserViewSet, basename="users")
72 router.register(
73     "lti-user-associations", LtiUserAssociationViewSet, basename="lti_user_associations"
74 )
75 router.register(
76     models.SharedLiveMedia.RESOURCE_NAME,
77     SharedLiveMediaViewSet,
78     basename="sharedlivemedias",
79 )
80 
81 # Old routes to remove
82 router.register(
83     models.LiveSession.RESOURCE_NAME,
84     LiveSessionViewSet,
85     basename="live_sessions",
86 )
87 
88 # Video related resources (for nested routes)
89 video_related_router = DefaultRouter()
90 video_related_router.register(
91     models.LiveSession.RESOURCE_NAME,
92     LiveSessionViewSet,
93     basename="live_sessions",
94 )
95 
96 urlpatterns = [
97     # Account
98     path("account/", include("marsha.account.urls")),
99     # Admin
100     path("admin/", admin.site.urls),
101     # LTI
102     path("lti/config.xml", LTIConfigView.as_view(), name="config_lti_view"),
103     re_path(
104         LTI_SELECT_ROUTE_PATTERN,
105         LTISelectView.as_view(),
106         name="select_lti_view",
107     ),
108     path("lti/respond/", LTIRespondView.as_view(), name="respond_lti_view"),
109     path("lti/videos/<uuid:uuid>", VideoLTIView.as_view(), name="video_lti_view"),
110     path(
111         "lti/documents/<uuid:uuid>", DocumentLTIView.as_view(), name="document_lti_view"
112     ),
113     # Public resources
114     path("videos/<uuid:uuid>", VideoView.as_view(), name="video_direct_access"),
115     path("documents/<uuid:uuid>", DocumentView.as_view(), name="document_public"),
116     # API
117     path(
118         "api/auth/challenge/",
119         ChallengeAuthenticationView.as_view(),
120         name="api_authentication",
121     ),
122     path("api/pairing-challenge", pairing_challenge, name="pairing_challenge"),
123     path("api/update-state", update_state, name="update_state"),
124     path(
125         "api/recording-slices-manifest",
126         recording_slices_manifest,
127         name="recording_slices_manifest",
128     ),
129     path(
130         "api/recording-slices-state",
131         recording_slices_state,
132         name="recording_slices_state",
133     ),
134     path("api/", include(router.urls)),
135     path(
136         f"api/{models.Video.RESOURCE_NAME}/<uuid:video_id>/",
137         include(video_related_router.urls),
138     ),
139     path(
140         "reminders/cancel/<str:pk>/<str:key>",
141         RemindersCancelView.as_view(),
142         name="reminders_cancel",
143     ),
144     path(
145         "xapi/<xapi_resource_kind:resource_kind>/<uuid:resource_id>/",
146         XAPIStatementView.as_view(),
147         name="xapi",
148     ),
149 ]
150 
151 if settings.BBB_ENABLED:
152     urlpatterns += [path("", include("marsha.bbb.urls"))]
153 
154 if settings.DEPOSIT_ENABLED:
155     urlpatterns += [path("", include("marsha.deposit.urls"))]
156 
157 if settings.MARKDOWN_ENABLED:
158     urlpatterns += [path("", include("marsha.markdown.urls"))]
159 
160 if settings.DEBUG:
161     urlpatterns += [path("", include("marsha.development.urls"))]
162 
163 if "dummy" in settings.STORAGE_BACKEND:
164     urlpatterns += [
165         path(
166             "e2e/api/video-upload/<uuid:uuid>",
167             local_video_upload,
168             name="local-video-upload",
169         ),
170         path(
171             "api/document-upload/<uuid:uuid>",
172             local_document_upload,
173             name="local-document-upload",
174         ),
175     ]
176 
177 static_path = re.escape(settings.STATIC_URL.lstrip("/"))
178 media_path = re.escape(settings.MEDIA_URL.lstrip("/"))
179 SITE_IGNORE_PREFIX = "|".join([static_path, media_path])
180 
181 urlpatterns += [
182     re_path(
183         # Catch all URLs that are not handled before
184         # and which do not regard static files or media files
185         rf"^(?!{SITE_IGNORE_PREFIX}).*",
186         cache_page(86400, key_prefix=settings.RELEASE)(SiteView.as_view()),
187         name="site",
188     ),
189 ]
190
```

Path: `src/backend/marsha/core/api/base.py`
Content:
```
1 """Declare API endpoints with Django RestFramework viewsets."""
2 
3 from django.shortcuts import get_object_or_404
4 
5 from rest_framework.decorators import api_view
6 from rest_framework.response import Response
7 
8 from .. import defaults, serializers
9 from ..models import Video
10 from ..simple_jwt.tokens import ResourceAccessToken
11 from ..utils.api_utils import get_uploadable_models_s3_mapping, validate_signature
12 
13 
14 class ObjectPkMixin:
15     """
16     Get the object primary key from the URL path.
17 
18     This is useful to avoid making extra requests using view.get_object() on
19     a ViewSet when we only need the object's id, which is available in the URL.
20     """
21 
22     def get_object_pk(self):
23         """Get the object primary key from the URL path."""
24         lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
25         return self.kwargs.get(lookup_url_kwarg)
26 
27 
28 class ObjectRelatedMixin:
29     """
30     Get the related video belonging to the current object.
31 
32     Using view.get_object is permissions is not possible anymore due to
33     infinite recursion between has_permission and has_object_permission call.
34     """
35 
36     def get_related_object(self):
37         """Get the video related to the current object."""
38         queryset = self.filter_queryset(self.get_queryset())
39         return queryset.get(pk=self.get_object_pk())
40 
41 
42 @api_view(["POST"])
43 def update_state(request):
44     """View handling AWS POST request to update the state of an object by key.
45 
46     Parameters
47     ----------
48     request : Type[django.http.request.HttpRequest]
49         The request on the API endpoint, it should contain a payload with the following fields:
50             - key: the key of an object in the source bucket as delivered in the upload policy,
51             - state: state of the upload, should be either "ready" or "error",
52             - extraParameters: Dict containing arbitrary data sent from AWS Lambda.
53 
54     Returns
55     -------
56     Type[rest_framework.response.Response]
57         HttpResponse acknowledging the success or failure of the state update operation.
58 
59     """
60     msg = request.body
61     serializer = serializers.UpdateStateSerializer(data=request.data)
62 
63     if serializer.is_valid() is not True:
64         return Response(serializer.errors, status=400)
65 
66     # Check if the provided signature is valid against any secret in our list
67     if not validate_signature(request.headers.get("X-Marsha-Signature"), msg):
68         return Response("Forbidden", status=403)
69 
70     # Retrieve the elements from the key
71     key_elements = serializer.get_key_elements()
72 
73     # Update the object targeted by the "object_id" and "resource_id"
74     model_from_s3_identifier = get_uploadable_models_s3_mapping()
75     model = model_from_s3_identifier[key_elements["model_name"]]
76 
77     extra_parameters = serializer.validated_data["extraParameters"]
78     if (
79         serializer.validated_data["state"] == defaults.READY
80         and hasattr(model, "extension")
81         and "extension" not in extra_parameters
82     ):
83         # The extension is part of the s3 key name and added in this key
84         # when generated by the initiate upload
85         extra_parameters["extension"] = key_elements.get("extension")
86 
87     try:
88         object_instance = model.objects.get(id=key_elements["object_id"])
89     except model.DoesNotExist:
90         return Response({"success": False}, status=404)
91 
92     object_instance.update_upload_state(
93         upload_state=serializer.validated_data["state"],
94         uploaded_on=key_elements.get("uploaded_on")
95         if serializer.validated_data["state"] == defaults.READY
96         else None,
97         **extra_parameters,
98     )
99 
100     return Response({"success": True})
101 
102 
103 @api_view(["POST"])
104 def recording_slices_manifest(request):
105     """View handling AWS POST request to set a manifest on a record slice.
106 
107     Parameters
108     ----------
109     request : Type[django.http.request.HttpRequest]
110         The request on the API endpoint, it should contain a payload with the following fields:
111             - video_id: the pk of a video.
112             - harvest_job_id: the id of the harvest job.
113             - manifest_key: the manifest key of the record slice.
114 
115     Returns
116     -------
117     Type[rest_framework.response.Response]
118         HttpResponse containing the current harvest status of all recording slices.
119 
120     """
121     # Check if the provided signature is valid against any secret in our list
122     if not validate_signature(request.headers.get("X-Marsha-Signature"), request.body):
123         return Response("Forbidden", status=403)
124 
125     video = get_object_or_404(Video, pk=request.data["video_id"])
126     video.set_recording_slice_manifest_key(
127         request.data["harvest_job_id"], request.data["manifest_key"]
128     )
129     return Response({"success": True})
130 
131 
132 @api_view(["POST"])
133 def recording_slices_state(request):
134     """View handling AWS POST request to check each record slice harvest status by video pk.
135 
136     Parameters
137     ----------
138     request : Type[django.http.request.HttpRequest]
139         The request on the API endpoint, it should contain a payload with the following fields:
140             - video_id: the pk of a video.
141 
142     Returns
143     -------
144     Type[rest_framework.response.Response]
145         HttpResponse containing the current harvest status of all recording slices.
146 
147     """
148     # Check if the provided signature is valid against any secret in our list
149     if not validate_signature(request.headers.get("X-Marsha-Signature"), request.body):
150         return Response("Forbidden", status=403)
151 
152     video = get_object_or_404(Video, pk=request.data["video_id"])
153     return Response(video.get_recording_slices_state())
154 
155 
156 class APIViewMixin:
157     """
158     Mixin to enhance the base DRF APIView.
159 
160     Must be used on every Marsha API views (APIView, Viewset, ...)
161 
162     This provides a way to separate request information between `request.user`
163     and `request.resource`
164     """
165 
166     def check_permissions(self, request):
167         """
168         Add resource attribute to the request when we are in a resource context.
169 
170         Note, we use the `check_permissions` method to do it because:
171          - we need the authentication to be done to know whether we have a user
172            token or a resource token
173          - AND we also need to be before the call to `check_permissions` and we
174            can't do it in `self.initial`
175          - PLUS we can't be in `perform_authentication` because "metadata" views
176            partially duplicates the request (see `clone_request`) and our
177            `resource` won't be passed along. But `check_permissions`is called
178            after the cloning so this works.
179 
180         Also, we can't alter the `request.user` without messing up with the
181         middleware called with `process_response` so in case of a resource,
182         we keep it as a TokenResource...
183         """
184         request.resource = None
185         if isinstance(request.auth, ResourceAccessToken):  # otherwise, nothing to do
186             request.resource = request.user
187 
188         super().check_permissions(request)
189
```
--- 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/api/base.py b/src/backend/marsha/core/api/base.py
--- a/src/backend/marsha/core/api/base.py
+++ b/src/backend/marsha/core/api/base.py
@@ -1,11 +1,14 @@
 """Declare API endpoints with Django RestFramework viewsets."""
-
+from django.conf import settings
+from django.http import JsonResponse
 from django.shortcuts import get_object_or_404
 
 from rest_framework.decorators import api_view
 from rest_framework.response import Response
+from waffle import switch_is_active
 
 from .. import defaults, serializers
+from ..defaults import SENTRY
 from ..models import Video
 from ..simple_jwt.tokens import ResourceAccessToken
 from ..utils.api_utils import get_uploadable_models_s3_mapping, validate_signature
@@ -153,6 +156,30 @@
     return Response(video.get_recording_slices_state())
 
 
+@api_view(["GET"])
+def get_frontend_configuration(request):
+    """View handling GET request to get the frontend configuration.
+
+    Parameters
+    ----------
+    request : Type[django.http.request.HttpRequest]
+        The request on the API endpoint.
+
+    Returns
+    -------
+    Type[rest_framework.response.Response]
+        HttpResponse containing the frontend configuration.
+
+    """
+    return JsonResponse(
+        {
+            "environment": settings.ENVIRONMENT,
+            "release": settings.RELEASE,
+            "sentry_dsn": settings.SENTRY_DSN if switch_is_active(SENTRY) else None,
+        }
+    )
+
+
 class APIViewMixin:
     """
     Mixin to enhance the base DRF APIView.
diff --git a/src/backend/marsha/urls.py b/src/backend/marsha/urls.py
--- a/src/backend/marsha/urls.py
+++ b/src/backend/marsha/urls.py
@@ -23,6 +23,7 @@
     UserViewSet,
     VideoViewSet,
     XAPIStatementView,
+    get_frontend_configuration,
     pairing_challenge,
     recording_slices_manifest,
     recording_slices_state,
@@ -131,6 +132,7 @@
         recording_slices_state,
         name="recording_slices_state",
     ),
+    path("api/config/", get_frontend_configuration, name="sentry_config"),
     path("api/", include(router.urls)),
     path(
         f"api/{models.Video.RESOURCE_NAME}/<uuid:video_id>/",

{"golden_diff": "diff --git a/src/backend/marsha/core/api/base.py b/src/backend/marsha/core/api/base.py\n--- a/src/backend/marsha/core/api/base.py\n+++ b/src/backend/marsha/core/api/base.py\n@@ -1,11 +1,14 @@\n \"\"\"Declare API endpoints with Django RestFramework viewsets.\"\"\"\n-\n+from django.conf import settings\n+from django.http import JsonResponse\n from django.shortcuts import get_object_or_404\n \n from rest_framework.decorators import api_view\n from rest_framework.response import Response\n+from waffle import switch_is_active\n \n from .. import defaults, serializers\n+from ..defaults import SENTRY\n from ..models import Video\n from ..simple_jwt.tokens import ResourceAccessToken\n from ..utils.api_utils import get_uploadable_models_s3_mapping, validate_signature\n@@ -153,6 +156,30 @@\n     return Response(video.get_recording_slices_state())\n \n \n+@api_view([\"GET\"])\n+def get_frontend_configuration(request):\n+    \"\"\"View handling GET request to get the frontend configuration.\n+\n+    Parameters\n+    ----------\n+    request : Type[django.http.request.HttpRequest]\n+        The request on the API endpoint.\n+\n+    Returns\n+    -------\n+    Type[rest_framework.response.Response]\n+        HttpResponse containing the frontend configuration.\n+\n+    \"\"\"\n+    return JsonResponse(\n+        {\n+            \"environment\": settings.ENVIRONMENT,\n+            \"release\": settings.RELEASE,\n+            \"sentry_dsn\": settings.SENTRY_DSN if switch_is_active(SENTRY) else None,\n+        }\n+    )\n+\n+\n class APIViewMixin:\n     \"\"\"\n     Mixin to enhance the base DRF APIView.\ndiff --git a/src/backend/marsha/urls.py b/src/backend/marsha/urls.py\n--- a/src/backend/marsha/urls.py\n+++ b/src/backend/marsha/urls.py\n@@ -23,6 +23,7 @@\n     UserViewSet,\n     VideoViewSet,\n     XAPIStatementView,\n+    get_frontend_configuration,\n     pairing_challenge,\n     recording_slices_manifest,\n     recording_slices_state,\n@@ -131,6 +132,7 @@\n         recording_slices_state,\n         name=\"recording_slices_state\",\n     ),\n+    path(\"api/config/\", get_frontend_configuration, name=\"sentry_config\"),\n     path(\"api/\", include(router.urls)),\n     path(\n         f\"api/{models.Video.RESOURCE_NAME}/<uuid:video_id>/\",\n", "issue": "Configure sentry in standalone site\n## Feature Request\r\n\r\n**Is your feature request related to a problem or unsupported use case? Please describe.**\r\n\r\nSentry is not configured in the standalone site and we have no feedback is there are errors.\r\n\r\n**Describe the solution you'd like**\r\n\r\nFind a solution to configure sentry. Maybe by fetching th configuration on a dedicated api endpoint\r\n\r\n\n", "before_files": [{"content": "\"\"\"Marsha URLs configuration.\"\"\"\nimport re\n\nfrom django.conf import settings\nfrom django.contrib import admin\nfrom django.urls import include, path, re_path, register_converter\nfrom django.views.decorators.cache import cache_page\n\nfrom rest_framework.routers import DefaultRouter\n\nfrom marsha.core import models\nfrom marsha.core.api import (\n    ChallengeAuthenticationView,\n    DocumentViewSet,\n    LiveSessionViewSet,\n    OrganizationViewSet,\n    PlaylistAccessViewSet,\n    PlaylistViewSet,\n    PortabilityResourceViewSet,\n    SharedLiveMediaViewSet,\n    ThumbnailViewSet,\n    TimedTextTrackViewSet,\n    UserViewSet,\n    VideoViewSet,\n    XAPIStatementView,\n    pairing_challenge,\n    recording_slices_manifest,\n    recording_slices_state,\n    update_state,\n)\nfrom marsha.core.api.lti_user_association import LtiUserAssociationViewSet\nfrom marsha.core.urls.converters import XAPIResourceKindConverter\nfrom marsha.core.utils.lti_select_utils import get_lti_select_resources\nfrom marsha.core.views import (\n    DocumentLTIView,\n    DocumentView,\n    LTIConfigView,\n    LTIRespondView,\n    LTISelectView,\n    RemindersCancelView,\n    SiteView,\n    VideoLTIView,\n    VideoView,\n)\nfrom marsha.development.api import local_document_upload, local_video_upload\n\n\nregister_converter(XAPIResourceKindConverter, \"xapi_resource_kind\")\n\nLTI_SELECT_ROUTE_PATTERN = (\n    rf\"lti/select/((?P<resource_kind>{'|'.join(get_lti_select_resources().keys())})/)?$\"\n)\n\nrouter = DefaultRouter()\nrouter.register(models.Video.RESOURCE_NAME, VideoViewSet, basename=\"videos\")\nrouter.register(models.Document.RESOURCE_NAME, DocumentViewSet, basename=\"documents\")\nrouter.register(\n    models.TimedTextTrack.RESOURCE_NAME,\n    TimedTextTrackViewSet,\n    basename=\"timed_text_tracks\",\n)\nrouter.register(models.Thumbnail.RESOURCE_NAME, ThumbnailViewSet, basename=\"thumbnails\")\nrouter.register(\"organizations\", OrganizationViewSet, basename=\"organizations\")\nrouter.register(\"playlists\", PlaylistViewSet, basename=\"playlists\")\nrouter.register(\n    \"playlist-accesses\", PlaylistAccessViewSet, basename=\"playlist_accesses\"\n)\nrouter.register(\n    \"portability-requests\", PortabilityResourceViewSet, basename=\"portability_requests\"\n)\nrouter.register(\"users\", UserViewSet, basename=\"users\")\nrouter.register(\n    \"lti-user-associations\", LtiUserAssociationViewSet, basename=\"lti_user_associations\"\n)\nrouter.register(\n    models.SharedLiveMedia.RESOURCE_NAME,\n    SharedLiveMediaViewSet,\n    basename=\"sharedlivemedias\",\n)\n\n# Old routes to remove\nrouter.register(\n    models.LiveSession.RESOURCE_NAME,\n    LiveSessionViewSet,\n    basename=\"live_sessions\",\n)\n\n# Video related resources (for nested routes)\nvideo_related_router = DefaultRouter()\nvideo_related_router.register(\n    models.LiveSession.RESOURCE_NAME,\n    LiveSessionViewSet,\n    basename=\"live_sessions\",\n)\n\nurlpatterns = [\n    # Account\n    path(\"account/\", include(\"marsha.account.urls\")),\n    # Admin\n    path(\"admin/\", admin.site.urls),\n    # LTI\n    path(\"lti/config.xml\", LTIConfigView.as_view(), name=\"config_lti_view\"),\n    re_path(\n        LTI_SELECT_ROUTE_PATTERN,\n        LTISelectView.as_view(),\n        name=\"select_lti_view\",\n    ),\n    path(\"lti/respond/\", LTIRespondView.as_view(), name=\"respond_lti_view\"),\n    path(\"lti/videos/<uuid:uuid>\", VideoLTIView.as_view(), name=\"video_lti_view\"),\n    path(\n        \"lti/documents/<uuid:uuid>\", DocumentLTIView.as_view(), name=\"document_lti_view\"\n    ),\n    # Public resources\n    path(\"videos/<uuid:uuid>\", VideoView.as_view(), name=\"video_direct_access\"),\n    path(\"documents/<uuid:uuid>\", DocumentView.as_view(), name=\"document_public\"),\n    # API\n    path(\n        \"api/auth/challenge/\",\n        ChallengeAuthenticationView.as_view(),\n        name=\"api_authentication\",\n    ),\n    path(\"api/pairing-challenge\", pairing_challenge, name=\"pairing_challenge\"),\n    path(\"api/update-state\", update_state, name=\"update_state\"),\n    path(\n        \"api/recording-slices-manifest\",\n        recording_slices_manifest,\n        name=\"recording_slices_manifest\",\n    ),\n    path(\n        \"api/recording-slices-state\",\n        recording_slices_state,\n        name=\"recording_slices_state\",\n    ),\n    path(\"api/\", include(router.urls)),\n    path(\n        f\"api/{models.Video.RESOURCE_NAME}/<uuid:video_id>/\",\n        include(video_related_router.urls),\n    ),\n    path(\n        \"reminders/cancel/<str:pk>/<str:key>\",\n        RemindersCancelView.as_view(),\n        name=\"reminders_cancel\",\n    ),\n    path(\n        \"xapi/<xapi_resource_kind:resource_kind>/<uuid:resource_id>/\",\n        XAPIStatementView.as_view(),\n        name=\"xapi\",\n    ),\n]\n\nif settings.BBB_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.bbb.urls\"))]\n\nif settings.DEPOSIT_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.deposit.urls\"))]\n\nif settings.MARKDOWN_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.markdown.urls\"))]\n\nif settings.DEBUG:\n    urlpatterns += [path(\"\", include(\"marsha.development.urls\"))]\n\nif \"dummy\" in settings.STORAGE_BACKEND:\n    urlpatterns += [\n        path(\n            \"e2e/api/video-upload/<uuid:uuid>\",\n            local_video_upload,\n            name=\"local-video-upload\",\n        ),\n        path(\n            \"api/document-upload/<uuid:uuid>\",\n            local_document_upload,\n            name=\"local-document-upload\",\n        ),\n    ]\n\nstatic_path = re.escape(settings.STATIC_URL.lstrip(\"/\"))\nmedia_path = re.escape(settings.MEDIA_URL.lstrip(\"/\"))\nSITE_IGNORE_PREFIX = \"|\".join([static_path, media_path])\n\nurlpatterns += [\n    re_path(\n        # Catch all URLs that are not handled before\n        # and which do not regard static files or media files\n        rf\"^(?!{SITE_IGNORE_PREFIX}).*\",\n        cache_page(86400, key_prefix=settings.RELEASE)(SiteView.as_view()),\n        name=\"site\",\n    ),\n]\n", "path": "src/backend/marsha/urls.py"}, {"content": "\"\"\"Declare API endpoints with Django RestFramework viewsets.\"\"\"\n\nfrom django.shortcuts import get_object_or_404\n\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\n\nfrom .. import defaults, serializers\nfrom ..models import Video\nfrom ..simple_jwt.tokens import ResourceAccessToken\nfrom ..utils.api_utils import get_uploadable_models_s3_mapping, validate_signature\n\n\nclass ObjectPkMixin:\n    \"\"\"\n    Get the object primary key from the URL path.\n\n    This is useful to avoid making extra requests using view.get_object() on\n    a ViewSet when we only need the object's id, which is available in the URL.\n    \"\"\"\n\n    def get_object_pk(self):\n        \"\"\"Get the object primary key from the URL path.\"\"\"\n        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field\n        return self.kwargs.get(lookup_url_kwarg)\n\n\nclass ObjectRelatedMixin:\n    \"\"\"\n    Get the related video belonging to the current object.\n\n    Using view.get_object is permissions is not possible anymore due to\n    infinite recursion between has_permission and has_object_permission call.\n    \"\"\"\n\n    def get_related_object(self):\n        \"\"\"Get the video related to the current object.\"\"\"\n        queryset = self.filter_queryset(self.get_queryset())\n        return queryset.get(pk=self.get_object_pk())\n\n\n@api_view([\"POST\"])\ndef update_state(request):\n    \"\"\"View handling AWS POST request to update the state of an object by key.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - key: the key of an object in the source bucket as delivered in the upload policy,\n            - state: state of the upload, should be either \"ready\" or \"error\",\n            - extraParameters: Dict containing arbitrary data sent from AWS Lambda.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse acknowledging the success or failure of the state update operation.\n\n    \"\"\"\n    msg = request.body\n    serializer = serializers.UpdateStateSerializer(data=request.data)\n\n    if serializer.is_valid() is not True:\n        return Response(serializer.errors, status=400)\n\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), msg):\n        return Response(\"Forbidden\", status=403)\n\n    # Retrieve the elements from the key\n    key_elements = serializer.get_key_elements()\n\n    # Update the object targeted by the \"object_id\" and \"resource_id\"\n    model_from_s3_identifier = get_uploadable_models_s3_mapping()\n    model = model_from_s3_identifier[key_elements[\"model_name\"]]\n\n    extra_parameters = serializer.validated_data[\"extraParameters\"]\n    if (\n        serializer.validated_data[\"state\"] == defaults.READY\n        and hasattr(model, \"extension\")\n        and \"extension\" not in extra_parameters\n    ):\n        # The extension is part of the s3 key name and added in this key\n        # when generated by the initiate upload\n        extra_parameters[\"extension\"] = key_elements.get(\"extension\")\n\n    try:\n        object_instance = model.objects.get(id=key_elements[\"object_id\"])\n    except model.DoesNotExist:\n        return Response({\"success\": False}, status=404)\n\n    object_instance.update_upload_state(\n        upload_state=serializer.validated_data[\"state\"],\n        uploaded_on=key_elements.get(\"uploaded_on\")\n        if serializer.validated_data[\"state\"] == defaults.READY\n        else None,\n        **extra_parameters,\n    )\n\n    return Response({\"success\": True})\n\n\n@api_view([\"POST\"])\ndef recording_slices_manifest(request):\n    \"\"\"View handling AWS POST request to set a manifest on a record slice.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - video_id: the pk of a video.\n            - harvest_job_id: the id of the harvest job.\n            - manifest_key: the manifest key of the record slice.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse containing the current harvest status of all recording slices.\n\n    \"\"\"\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), request.body):\n        return Response(\"Forbidden\", status=403)\n\n    video = get_object_or_404(Video, pk=request.data[\"video_id\"])\n    video.set_recording_slice_manifest_key(\n        request.data[\"harvest_job_id\"], request.data[\"manifest_key\"]\n    )\n    return Response({\"success\": True})\n\n\n@api_view([\"POST\"])\ndef recording_slices_state(request):\n    \"\"\"View handling AWS POST request to check each record slice harvest status by video pk.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - video_id: the pk of a video.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse containing the current harvest status of all recording slices.\n\n    \"\"\"\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), request.body):\n        return Response(\"Forbidden\", status=403)\n\n    video = get_object_or_404(Video, pk=request.data[\"video_id\"])\n    return Response(video.get_recording_slices_state())\n\n\nclass APIViewMixin:\n    \"\"\"\n    Mixin to enhance the base DRF APIView.\n\n    Must be used on every Marsha API views (APIView, Viewset, ...)\n\n    This provides a way to separate request information between `request.user`\n    and `request.resource`\n    \"\"\"\n\n    def check_permissions(self, request):\n        \"\"\"\n        Add resource attribute to the request when we are in a resource context.\n\n        Note, we use the `check_permissions` method to do it because:\n         - we need the authentication to be done to know whether we have a user\n           token or a resource token\n         - AND we also need to be before the call to `check_permissions` and we\n           can't do it in `self.initial`\n         - PLUS we can't be in `perform_authentication` because \"metadata\" views\n           partially duplicates the request (see `clone_request`) and our\n           `resource` won't be passed along. But `check_permissions`is called\n           after the cloning so this works.\n\n        Also, we can't alter the `request.user` without messing up with the\n        middleware called with `process_response` so in case of a resource,\n        we keep it as a TokenResource...\n        \"\"\"\n        request.resource = None\n        if isinstance(request.auth, ResourceAccessToken):  # otherwise, nothing to do\n            request.resource = request.user\n\n        super().check_permissions(request)\n", "path": "src/backend/marsha/core/api/base.py"}], "after_files": [{"content": "\"\"\"Marsha URLs configuration.\"\"\"\nimport re\n\nfrom django.conf import settings\nfrom django.contrib import admin\nfrom django.urls import include, path, re_path, register_converter\nfrom django.views.decorators.cache import cache_page\n\nfrom rest_framework.routers import DefaultRouter\n\nfrom marsha.core import models\nfrom marsha.core.api import (\n    ChallengeAuthenticationView,\n    DocumentViewSet,\n    LiveSessionViewSet,\n    OrganizationViewSet,\n    PlaylistAccessViewSet,\n    PlaylistViewSet,\n    PortabilityResourceViewSet,\n    SharedLiveMediaViewSet,\n    ThumbnailViewSet,\n    TimedTextTrackViewSet,\n    UserViewSet,\n    VideoViewSet,\n    XAPIStatementView,\n    get_frontend_configuration,\n    pairing_challenge,\n    recording_slices_manifest,\n    recording_slices_state,\n    update_state,\n)\nfrom marsha.core.api.lti_user_association import LtiUserAssociationViewSet\nfrom marsha.core.urls.converters import XAPIResourceKindConverter\nfrom marsha.core.utils.lti_select_utils import get_lti_select_resources\nfrom marsha.core.views import (\n    DocumentLTIView,\n    DocumentView,\n    LTIConfigView,\n    LTIRespondView,\n    LTISelectView,\n    RemindersCancelView,\n    SiteView,\n    VideoLTIView,\n    VideoView,\n)\nfrom marsha.development.api import local_document_upload, local_video_upload\n\n\nregister_converter(XAPIResourceKindConverter, \"xapi_resource_kind\")\n\nLTI_SELECT_ROUTE_PATTERN = (\n    rf\"lti/select/((?P<resource_kind>{'|'.join(get_lti_select_resources().keys())})/)?$\"\n)\n\nrouter = DefaultRouter()\nrouter.register(models.Video.RESOURCE_NAME, VideoViewSet, basename=\"videos\")\nrouter.register(models.Document.RESOURCE_NAME, DocumentViewSet, basename=\"documents\")\nrouter.register(\n    models.TimedTextTrack.RESOURCE_NAME,\n    TimedTextTrackViewSet,\n    basename=\"timed_text_tracks\",\n)\nrouter.register(models.Thumbnail.RESOURCE_NAME, ThumbnailViewSet, basename=\"thumbnails\")\nrouter.register(\"organizations\", OrganizationViewSet, basename=\"organizations\")\nrouter.register(\"playlists\", PlaylistViewSet, basename=\"playlists\")\nrouter.register(\n    \"playlist-accesses\", PlaylistAccessViewSet, basename=\"playlist_accesses\"\n)\nrouter.register(\n    \"portability-requests\", PortabilityResourceViewSet, basename=\"portability_requests\"\n)\nrouter.register(\"users\", UserViewSet, basename=\"users\")\nrouter.register(\n    \"lti-user-associations\", LtiUserAssociationViewSet, basename=\"lti_user_associations\"\n)\nrouter.register(\n    models.SharedLiveMedia.RESOURCE_NAME,\n    SharedLiveMediaViewSet,\n    basename=\"sharedlivemedias\",\n)\n\n# Old routes to remove\nrouter.register(\n    models.LiveSession.RESOURCE_NAME,\n    LiveSessionViewSet,\n    basename=\"live_sessions\",\n)\n\n# Video related resources (for nested routes)\nvideo_related_router = DefaultRouter()\nvideo_related_router.register(\n    models.LiveSession.RESOURCE_NAME,\n    LiveSessionViewSet,\n    basename=\"live_sessions\",\n)\n\nurlpatterns = [\n    # Account\n    path(\"account/\", include(\"marsha.account.urls\")),\n    # Admin\n    path(\"admin/\", admin.site.urls),\n    # LTI\n    path(\"lti/config.xml\", LTIConfigView.as_view(), name=\"config_lti_view\"),\n    re_path(\n        LTI_SELECT_ROUTE_PATTERN,\n        LTISelectView.as_view(),\n        name=\"select_lti_view\",\n    ),\n    path(\"lti/respond/\", LTIRespondView.as_view(), name=\"respond_lti_view\"),\n    path(\"lti/videos/<uuid:uuid>\", VideoLTIView.as_view(), name=\"video_lti_view\"),\n    path(\n        \"lti/documents/<uuid:uuid>\", DocumentLTIView.as_view(), name=\"document_lti_view\"\n    ),\n    # Public resources\n    path(\"videos/<uuid:uuid>\", VideoView.as_view(), name=\"video_direct_access\"),\n    path(\"documents/<uuid:uuid>\", DocumentView.as_view(), name=\"document_public\"),\n    # API\n    path(\n        \"api/auth/challenge/\",\n        ChallengeAuthenticationView.as_view(),\n        name=\"api_authentication\",\n    ),\n    path(\"api/pairing-challenge\", pairing_challenge, name=\"pairing_challenge\"),\n    path(\"api/update-state\", update_state, name=\"update_state\"),\n    path(\n        \"api/recording-slices-manifest\",\n        recording_slices_manifest,\n        name=\"recording_slices_manifest\",\n    ),\n    path(\n        \"api/recording-slices-state\",\n        recording_slices_state,\n        name=\"recording_slices_state\",\n    ),\n    path(\"api/config/\", get_frontend_configuration, name=\"sentry_config\"),\n    path(\"api/\", include(router.urls)),\n    path(\n        f\"api/{models.Video.RESOURCE_NAME}/<uuid:video_id>/\",\n        include(video_related_router.urls),\n    ),\n    path(\n        \"reminders/cancel/<str:pk>/<str:key>\",\n        RemindersCancelView.as_view(),\n        name=\"reminders_cancel\",\n    ),\n    path(\n        \"xapi/<xapi_resource_kind:resource_kind>/<uuid:resource_id>/\",\n        XAPIStatementView.as_view(),\n        name=\"xapi\",\n    ),\n]\n\nif settings.BBB_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.bbb.urls\"))]\n\nif settings.DEPOSIT_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.deposit.urls\"))]\n\nif settings.MARKDOWN_ENABLED:\n    urlpatterns += [path(\"\", include(\"marsha.markdown.urls\"))]\n\nif settings.DEBUG:\n    urlpatterns += [path(\"\", include(\"marsha.development.urls\"))]\n\nif \"dummy\" in settings.STORAGE_BACKEND:\n    urlpatterns += [\n        path(\n            \"e2e/api/video-upload/<uuid:uuid>\",\n            local_video_upload,\n            name=\"local-video-upload\",\n        ),\n        path(\n            \"api/document-upload/<uuid:uuid>\",\n            local_document_upload,\n            name=\"local-document-upload\",\n        ),\n    ]\n\nstatic_path = re.escape(settings.STATIC_URL.lstrip(\"/\"))\nmedia_path = re.escape(settings.MEDIA_URL.lstrip(\"/\"))\nSITE_IGNORE_PREFIX = \"|\".join([static_path, media_path])\n\nurlpatterns += [\n    re_path(\n        # Catch all URLs that are not handled before\n        # and which do not regard static files or media files\n        rf\"^(?!{SITE_IGNORE_PREFIX}).*\",\n        cache_page(86400, key_prefix=settings.RELEASE)(SiteView.as_view()),\n        name=\"site\",\n    ),\n]\n", "path": "src/backend/marsha/urls.py"}, {"content": "\"\"\"Declare API endpoints with Django RestFramework viewsets.\"\"\"\nfrom django.conf import settings\nfrom django.http import JsonResponse\nfrom django.shortcuts import get_object_or_404\n\nfrom rest_framework.decorators import api_view\nfrom rest_framework.response import Response\nfrom waffle import switch_is_active\n\nfrom .. import defaults, serializers\nfrom ..defaults import SENTRY\nfrom ..models import Video\nfrom ..simple_jwt.tokens import ResourceAccessToken\nfrom ..utils.api_utils import get_uploadable_models_s3_mapping, validate_signature\n\n\nclass ObjectPkMixin:\n    \"\"\"\n    Get the object primary key from the URL path.\n\n    This is useful to avoid making extra requests using view.get_object() on\n    a ViewSet when we only need the object's id, which is available in the URL.\n    \"\"\"\n\n    def get_object_pk(self):\n        \"\"\"Get the object primary key from the URL path.\"\"\"\n        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field\n        return self.kwargs.get(lookup_url_kwarg)\n\n\nclass ObjectRelatedMixin:\n    \"\"\"\n    Get the related video belonging to the current object.\n\n    Using view.get_object is permissions is not possible anymore due to\n    infinite recursion between has_permission and has_object_permission call.\n    \"\"\"\n\n    def get_related_object(self):\n        \"\"\"Get the video related to the current object.\"\"\"\n        queryset = self.filter_queryset(self.get_queryset())\n        return queryset.get(pk=self.get_object_pk())\n\n\n@api_view([\"POST\"])\ndef update_state(request):\n    \"\"\"View handling AWS POST request to update the state of an object by key.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - key: the key of an object in the source bucket as delivered in the upload policy,\n            - state: state of the upload, should be either \"ready\" or \"error\",\n            - extraParameters: Dict containing arbitrary data sent from AWS Lambda.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse acknowledging the success or failure of the state update operation.\n\n    \"\"\"\n    msg = request.body\n    serializer = serializers.UpdateStateSerializer(data=request.data)\n\n    if serializer.is_valid() is not True:\n        return Response(serializer.errors, status=400)\n\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), msg):\n        return Response(\"Forbidden\", status=403)\n\n    # Retrieve the elements from the key\n    key_elements = serializer.get_key_elements()\n\n    # Update the object targeted by the \"object_id\" and \"resource_id\"\n    model_from_s3_identifier = get_uploadable_models_s3_mapping()\n    model = model_from_s3_identifier[key_elements[\"model_name\"]]\n\n    extra_parameters = serializer.validated_data[\"extraParameters\"]\n    if (\n        serializer.validated_data[\"state\"] == defaults.READY\n        and hasattr(model, \"extension\")\n        and \"extension\" not in extra_parameters\n    ):\n        # The extension is part of the s3 key name and added in this key\n        # when generated by the initiate upload\n        extra_parameters[\"extension\"] = key_elements.get(\"extension\")\n\n    try:\n        object_instance = model.objects.get(id=key_elements[\"object_id\"])\n    except model.DoesNotExist:\n        return Response({\"success\": False}, status=404)\n\n    object_instance.update_upload_state(\n        upload_state=serializer.validated_data[\"state\"],\n        uploaded_on=key_elements.get(\"uploaded_on\")\n        if serializer.validated_data[\"state\"] == defaults.READY\n        else None,\n        **extra_parameters,\n    )\n\n    return Response({\"success\": True})\n\n\n@api_view([\"POST\"])\ndef recording_slices_manifest(request):\n    \"\"\"View handling AWS POST request to set a manifest on a record slice.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - video_id: the pk of a video.\n            - harvest_job_id: the id of the harvest job.\n            - manifest_key: the manifest key of the record slice.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse containing the current harvest status of all recording slices.\n\n    \"\"\"\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), request.body):\n        return Response(\"Forbidden\", status=403)\n\n    video = get_object_or_404(Video, pk=request.data[\"video_id\"])\n    video.set_recording_slice_manifest_key(\n        request.data[\"harvest_job_id\"], request.data[\"manifest_key\"]\n    )\n    return Response({\"success\": True})\n\n\n@api_view([\"POST\"])\ndef recording_slices_state(request):\n    \"\"\"View handling AWS POST request to check each record slice harvest status by video pk.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint, it should contain a payload with the following fields:\n            - video_id: the pk of a video.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse containing the current harvest status of all recording slices.\n\n    \"\"\"\n    # Check if the provided signature is valid against any secret in our list\n    if not validate_signature(request.headers.get(\"X-Marsha-Signature\"), request.body):\n        return Response(\"Forbidden\", status=403)\n\n    video = get_object_or_404(Video, pk=request.data[\"video_id\"])\n    return Response(video.get_recording_slices_state())\n\n\n@api_view([\"GET\"])\ndef get_frontend_configuration(request):\n    \"\"\"View handling GET request to get the frontend configuration.\n\n    Parameters\n    ----------\n    request : Type[django.http.request.HttpRequest]\n        The request on the API endpoint.\n\n    Returns\n    -------\n    Type[rest_framework.response.Response]\n        HttpResponse containing the frontend configuration.\n\n    \"\"\"\n    return JsonResponse(\n        {\n            \"environment\": settings.ENVIRONMENT,\n            \"release\": settings.RELEASE,\n            \"sentry_dsn\": settings.SENTRY_DSN if switch_is_active(SENTRY) else None,\n        }\n    )\n\n\nclass APIViewMixin:\n    \"\"\"\n    Mixin to enhance the base DRF APIView.\n\n    Must be used on every Marsha API views (APIView, Viewset, ...)\n\n    This provides a way to separate request information between `request.user`\n    and `request.resource`\n    \"\"\"\n\n    def check_permissions(self, request):\n        \"\"\"\n        Add resource attribute to the request when we are in a resource context.\n\n        Note, we use the `check_permissions` method to do it because:\n         - we need the authentication to be done to know whether we have a user\n           token or a resource token\n         - AND we also need to be before the call to `check_permissions` and we\n           can't do it in `self.initial`\n         - PLUS we can't be in `perform_authentication` because \"metadata\" views\n           partially duplicates the request (see `clone_request`) and our\n           `resource` won't be passed along. But `check_permissions`is called\n           after the cloning so this works.\n\n        Also, we can't alter the `request.user` without messing up with the\n        middleware called with `process_response` so in case of a resource,\n        we keep it as a TokenResource...\n        \"\"\"\n        request.resource = None\n        if isinstance(request.auth, ResourceAccessToken):  # otherwise, nothing to do\n            request.resource = request.user\n\n        super().check_permissions(request)\n", "path": "src/backend/marsha/core/api/base.py"}]}