diff --git a/src/backend/marsha/bbb/api.py b/src/backend/marsha/bbb/api.py index e24a0e7358..75866e0f7c 100644 --- a/src/backend/marsha/bbb/api.py +++ b/src/backend/marsha/bbb/api.py @@ -27,7 +27,6 @@ from marsha.core.api import APIViewMixin, BulkDestroyModelMixin, ObjectPkMixin from . import permissions, serializers -from ..core.api.base import ResourceDoesNotMatchParametersException from ..core.defaults import VOD_CONVERT from ..core.models import ADMINISTRATOR, INSTRUCTOR, Video from ..core.utils.convert_lambda_utils import invoke_lambda_convert @@ -52,23 +51,14 @@ class ObjectClassroomRelatedMixin: def get_related_classroom_id(self): """Get the related classroom ID from the request.""" - # The video ID in the URL will be mandatory when old routes are deleted. - classroom_id = ( + # The video ID in the URL is mandatory. + return ( self.kwargs.get("classroom_id") # Backward compatibility with old routes or self.request.data.get("classroom") or self.request.query_params.get("classroom") ) - # Backward compatibility with old routes for LTI context - resource = self.request.resource - if resource is not None: - if resource.id and classroom_id and str(classroom_id) != str(resource.id): - raise ResourceDoesNotMatchParametersException() - return self.request.resource.id - - return classroom_id - class InviteTokenThrottle(AnonRateThrottle): """Throttling class dedicated to classroom invite token endpoint""" @@ -109,7 +99,7 @@ class ClassroomViewSet( permission_classes = [ ( - core_permissions.IsTokenResourceRouteObject + core_permissions.IsPlaylistToken & (core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin) ) | ( @@ -157,7 +147,8 @@ def get_permissions(self): ] elif self.action in ["retrieve", "service_join"]: permission_classes = [ - core_permissions.IsTokenResourceRouteObject + core_permissions.IsPlaylistToken + | core_permissions.IsTokenResourceRouteObject # needed for invite links | ( core_permissions.UserIsAuthenticated # asserts request.resource is None & ( @@ -203,7 +194,7 @@ def get_serializer_context(self): # For LTI | ( core_permissions.ResourceIsAuthenticated - & core_permissions.IsTokenResourceRouteObject + & core_permissions.IsPlaylistToken & ( core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin @@ -289,51 +280,6 @@ def lti_select(self, request): } ) - @action( - methods=["get"], - detail=True, - url_path="classroomdocuments", - permission_classes=[ - core_permissions.IsTokenInstructor - | core_permissions.IsTokenAdmin - | ( - core_permissions.UserIsAuthenticated # asserts request.resource is None - & ( - core_permissions.IsObjectPlaylistAdminOrInstructor - | core_permissions.IsObjectPlaylistOrganizationAdmin - ) - ) - ], - ) - # pylint: disable=unused-argument - def classroomdocuments(self, request, pk=None): - """Get documents from a classroom. - - Calling the endpoint returns a list of classroom documents. - - Parameters - ---------- - request : Type[django.http.request.HttpRequest] - The request on the API endpoint - pk : int - The primary key of the classroom - - Returns - ------- - Type[rest_framework.response.Response] - HttpResponse carrying deposited files as a JSON object. - - """ - classroom = self.get_object() - queryset = classroom.classroom_documents.all().order_by("-created_on") - page = self.paginate_queryset(queryset) - serializer = serializers.ClassroomDocumentSerializer( - page, - many=True, - context={"request": self.request}, - ) - return self.get_paginated_response(serializer.data) - @action( methods=["patch"], detail=True, @@ -546,6 +492,7 @@ class ClassroomDocumentViewSet( APIViewMixin, ObjectPkMixin, ObjectClassroomRelatedMixin, + mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, @@ -627,7 +574,7 @@ def create(self, request, *args, **kwargs): @action(methods=["post"], detail=True, url_path="initiate-upload") # pylint: disable=unused-argument - def initiate_upload(self, request, pk=None): + def initiate_upload(self, request, pk=None, classroom_id=None): """Get an upload policy for a classroom document. Calling the endpoint resets the upload state to `pending` and returns an upload policy to @@ -651,7 +598,6 @@ def initiate_upload(self, request, pk=None): serializer = serializers.ClassroomDocumentInitiateUploadSerializer( data=request.data ) - if serializer.is_valid() is not True: return Response(serializer.errors, status=400) diff --git a/src/backend/marsha/bbb/permissions.py b/src/backend/marsha/bbb/permissions.py index 4b8211526f..5e0f97dccf 100644 --- a/src/backend/marsha/bbb/permissions.py +++ b/src/backend/marsha/bbb/permissions.py @@ -2,6 +2,7 @@ from rest_framework import permissions +from marsha.bbb.models import Classroom from marsha.core import models, permissions as core_permissions @@ -33,7 +34,9 @@ def has_permission(self, request, view): if not request.resource: return False - return str(view.get_related_classroom_id()) == request.resource.id + return Classroom.objects.filter( + pk=view.get_related_classroom_id(), playlist_id=request.resource.id + ).exists() class BaseIsRelatedClassroomPlaylistRoleMixin: diff --git a/src/backend/marsha/bbb/serializers.py b/src/backend/marsha/bbb/serializers.py index 95c1c08e13..c6b80aa02f 100644 --- a/src/backend/marsha/bbb/serializers.py +++ b/src/backend/marsha/bbb/serializers.py @@ -35,7 +35,7 @@ class Meta: # noqa model = ClassroomRecording fields = ( "id", - "classroom", + "classroom_id", "record_id", "started_at", "video_file_url", @@ -43,7 +43,7 @@ class Meta: # noqa ) read_only_fields = ( "id", - "classroom", + "classroom_id", "record_id", "started_at", "video_file_url", @@ -51,7 +51,7 @@ class Meta: # noqa ) # Make sure classroom and vod UUIDs are converted to a string during serialization - classroom = serializers.PrimaryKeyRelatedField( + classroom_id = serializers.PrimaryKeyRelatedField( read_only=True, pk_field=serializers.CharField() ) vod = VideoFromRecordingSerializer(read_only=True) @@ -249,7 +249,7 @@ class ClassroomDocumentSerializer( class Meta: # noqa model = ClassroomDocument fields = ( - "classroom", + "classroom_id", "filename", "id", "is_default", @@ -258,7 +258,7 @@ class Meta: # noqa "url", ) read_only_fields = ( - "classroom", + "classroom_id", "id", "upload_state", "uploaded_on", @@ -267,7 +267,7 @@ class Meta: # noqa url = serializers.SerializerMethodField() # Make sure classroom UUID is converted to a string during serialization - classroom = serializers.PrimaryKeyRelatedField( + classroom_id = serializers.PrimaryKeyRelatedField( read_only=True, pk_field=serializers.CharField() ) @@ -296,13 +296,9 @@ def create(self, validated_data): The "validated_data" dictionary is returned after modification. """ - resource = self.context["request"].resource - classroom_id = self.context["request"].data.get("classroom") + classroom_id = self.context["view"].get_related_classroom_id() if not validated_data.get("classroom_id"): - if resource: - validated_data["classroom_id"] = resource.id - elif classroom_id: - validated_data["classroom_id"] = classroom_id + validated_data["classroom_id"] = classroom_id if not ClassroomDocument.objects.filter( classroom_id=validated_data["classroom_id"] diff --git a/src/backend/marsha/bbb/tests/api/classroom/recordings/test_create_vod.py b/src/backend/marsha/bbb/tests/api/classroom/recordings/test_create_vod.py index ebffbdf1cc..dc2d5ca9ed 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/recordings/test_create_vod.py +++ b/src/backend/marsha/bbb/tests/api/classroom/recordings/test_create_vod.py @@ -125,7 +125,7 @@ def test_api_classroom_recording_create_vod_instructor_or_admin(self): self.assertDictEqual( response.json(), { - "classroom": str(recording.classroom.id), + "classroom_id": str(recording.classroom.id), "id": str(recording.id), "record_id": str(recording.record_id), "started_at": "2019-08-21T15:00:02Z", diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_bulk_destroy.py b/src/backend/marsha/bbb/tests/api/classroom/test_bulk_destroy.py index e9aebbbf89..aaab0d437e 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_bulk_destroy.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_bulk_destroy.py @@ -52,7 +52,7 @@ def test_api_classroom_bulk_delete_student(self): classroom2 = ClassroomFactory() jwt_token = StudentLtiTokenFactory( - resource=classroom1, + resource=classroom1.playlist, permissions__can_update=True, ) @@ -93,7 +93,7 @@ def test_api_classroom_bulk_delete_instructor(self): """LTI Token can't delete a list of classroom.""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.delete( self._api_url(), @@ -113,7 +113,7 @@ def test_api_classroom_bulk_delete_instructor_with_playlist_token(self): playlist = PlaylistFactory() classroom = ClassroomFactory(playlist=playlist) - jwt_token = PlaylistLtiTokenFactory(resource=classroom) + jwt_token = PlaylistLtiTokenFactory(resource=classroom.playlist) self.assertEqual(Classroom.objects.count(), 1) diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_classroomdocuments.py b/src/backend/marsha/bbb/tests/api/classroom/test_classroomdocuments.py index becd715bae..e2f9686321 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_classroomdocuments.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_classroomdocuments.py @@ -49,7 +49,7 @@ def test_api_list_classroom_documents_student(self): """A student should not be able to fetch a list of classroom documents.""" classroom = ClassroomFactory() ClassroomDocumentFactory.create_batch(3, classroom=classroom) - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id}/classroomdocuments/", @@ -63,7 +63,7 @@ def test_api_list_classroom_documents_instructor(self): classroom_documents = ClassroomDocumentFactory.create_batch( 3, classroom=classroom ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id}/classroomdocuments/?limit=2", @@ -79,7 +79,7 @@ def test_api_list_classroom_documents_instructor(self): "previous": None, "results": [ { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[2].filename, "id": str(classroom_documents[2].id), "is_default": False, @@ -88,7 +88,7 @@ def test_api_list_classroom_documents_instructor(self): "url": None, }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[1].filename, "id": str(classroom_documents[1].id), "is_default": False, @@ -118,7 +118,7 @@ def test_api_list_classroom_documents_instructor_urls(self): uploaded_on=now, ) ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id}/classroomdocuments/", @@ -134,7 +134,7 @@ def test_api_list_classroom_documents_instructor_urls(self): "previous": None, "results": [ { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[3].filename, "id": str(classroom_documents[3].id), "is_default": False, @@ -148,7 +148,7 @@ def test_api_list_classroom_documents_instructor_urls(self): ), }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[2].filename, "id": str(classroom_documents[2].id), "is_default": False, @@ -163,7 +163,7 @@ def test_api_list_classroom_documents_instructor_urls(self): ), }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[1].filename, "id": str(classroom_documents[1].id), "is_default": False, @@ -178,7 +178,7 @@ def test_api_list_classroom_documents_instructor_urls(self): ), }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[0].filename, "id": str(classroom_documents[0].id), "is_default": False, @@ -236,7 +236,7 @@ def test_api_list_classroom_documents_user_access_token_organization_admin(self) "previous": None, "results": [ { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[2].filename, "id": str(classroom_documents[2].id), "is_default": False, @@ -245,7 +245,7 @@ def test_api_list_classroom_documents_user_access_token_organization_admin(self) "url": None, }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[1].filename, "id": str(classroom_documents[1].id), "is_default": False, @@ -281,7 +281,7 @@ def test_api_list_classroom_documents_user_access_token_playlist_admin(self): "previous": None, "results": [ { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[2].filename, "id": str(classroom_documents[2].id), "is_default": False, @@ -290,7 +290,7 @@ def test_api_list_classroom_documents_user_access_token_playlist_admin(self): "url": None, }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": classroom_documents[1].filename, "id": str(classroom_documents[1].id), "is_default": False, diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_create.py b/src/backend/marsha/bbb/tests/api/classroom/test_create.py index fe9852004f..998333d922 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_create.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_create.py @@ -51,7 +51,7 @@ def test_api_classroom_create_student(self): classroom = ClassroomFactory() jwt_token = StudentLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, permissions__can_update=True, ) @@ -77,7 +77,7 @@ def test_api_classroom_create_instructor(self): """An instructor without playlist token should not be able to create a classroom.""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.post( "/api/classrooms/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_delete.py b/src/backend/marsha/bbb/tests/api/classroom/test_delete.py index 02a9b19038..fb9e39e247 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_delete.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_delete.py @@ -46,7 +46,7 @@ def test_api_classroom_delete_student(self): classroom = ClassroomFactory() jwt_token = StudentLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, permissions__can_update=True, ) @@ -72,7 +72,7 @@ def test_api_classroom_delete_instructor(self): """An instructor without playlist token should not be able to delete a classroom.""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.delete( f"/api/classrooms/{classroom.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -86,7 +86,7 @@ def test_api_classroom_delete_instructor_with_playlist_token(self): playlist = PlaylistFactory() classroom = ClassroomFactory(playlist=playlist) - jwt_token = PlaylistLtiTokenFactory(resource=classroom) + jwt_token = PlaylistLtiTokenFactory(resource=classroom.playlist) self.assertEqual(Classroom.objects.count(), 1) diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_list.py b/src/backend/marsha/bbb/tests/api/classroom/test_list.py index 6006db3262..bcd9916259 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_list.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_list.py @@ -46,7 +46,7 @@ def test_api_classroom_fetch_list_student(self): classroom = ClassroomFactory() jwt_token = StudentLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, permissions__can_update=True, ) @@ -59,7 +59,7 @@ def test_api_fetch_list_instructor(self): """An instructor should not be able to fetch a classroom list.""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( "/api/classrooms/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_retrieve.py b/src/backend/marsha/bbb/tests/api/classroom/test_retrieve.py index ac4b248586..6293db37d3 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_retrieve.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_retrieve.py @@ -8,6 +8,7 @@ from marsha.bbb import serializers from marsha.bbb.factories import ClassroomFactory, ClassroomRecordingFactory +from marsha.bbb.utils.tokens import create_classroom_stable_invite_jwt from marsha.core.factories import ( OrganizationAccessFactory, PlaylistAccessFactory, @@ -50,7 +51,7 @@ def test_api_classroom_fetch_student(self, mock_get_meeting_infos): "running": "true", } - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -108,7 +109,7 @@ def test_api_classroom_fetch_student_with_recordings(self, mock_get_meeting_info "running": "true", } - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -182,7 +183,7 @@ def test_api_classroom_fetch_student_scheduled(self, mock_get_meeting_infos): "running": "true", } - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -232,7 +233,7 @@ def test_api_classroom_fetch_instructor(self, mock_get_meeting_infos): "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -473,7 +474,7 @@ def test_api_classroom_fetch_with_recordings(self, mock_get_meeting_infos): "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -502,7 +503,7 @@ def test_api_classroom_fetch_with_recordings(self, mock_get_meeting_infos): "estimated_duration": None, "recordings": [ { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "id": str(classroom_recording_2.id), "record_id": str(classroom_recording_2.record_id), "started_at": "2019-08-21T11:00:02Z", @@ -510,7 +511,7 @@ def test_api_classroom_fetch_with_recordings(self, mock_get_meeting_infos): "vod": None, }, { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "id": str(classroom_recording_1.id), "record_id": str(classroom_recording_1.record_id), "started_at": "2019-08-21T15:00:02Z", @@ -545,7 +546,7 @@ def test_api_classroom_fetch_from_LTI_inactive_conversion( "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.get( f"/api/classrooms/{classroom.id!s}/", @@ -584,3 +585,110 @@ def test_api_classroom_fetch_from_standalone_site_inactive_conversion( self.assertEqual(response.status_code, 200) self.assertFalse(response.json()["vod_conversion_enabled"]) + + @mock.patch.object(serializers, "get_meeting_infos") + def test_api_classroom_fetch_public_invite(self, mock_get_meeting_infos): + """A public invited user should be allowed to fetch a classroom.""" + classroom = ClassroomFactory() + mock_get_meeting_infos.return_value = { + "returncode": "SUCCESS", + "running": "true", + } + + jwt_token = create_classroom_stable_invite_jwt(classroom) + + response = self.client.get( + f"/api/classrooms/{classroom.id!s}/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", + ) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content) + self.assertDictEqual( + { + "id": str(classroom.id), + "infos": {"returncode": "SUCCESS", "running": "true"}, + "lti_id": str(classroom.lti_id), + "title": classroom.title, + "description": classroom.description, + "started": False, + "ended": False, + "meeting_id": str(classroom.meeting_id), + "welcome_text": classroom.welcome_text, + "playlist": { + "id": str(classroom.playlist.id), + "title": classroom.playlist.title, + "lti_id": classroom.playlist.lti_id, + }, + "starting_at": None, + "estimated_duration": None, + "public_token": None, + "instructor_token": None, + "recordings": [], + "retention_date": None, + "enable_waiting_room": False, + "enable_chat": True, + "enable_presentation_supports": True, + "enable_recordings": True, + "recording_purpose": classroom.recording_purpose, + "enable_shared_notes": True, + "vod_conversion_enabled": True, + }, + content, + ) + + @mock.patch.object(serializers, "get_meeting_infos") + def test_api_classroom_fetch_moderator_invite(self, mock_get_meeting_infos): + """A moderator invited user should be allowed to fetch a classroom.""" + classroom = ClassroomFactory() + mock_get_meeting_infos.return_value = { + "returncode": "SUCCESS", + "running": "true", + } + + jwt_token = create_classroom_stable_invite_jwt( + classroom, + role=INSTRUCTOR, + permissions={ + "can_update": True, + "can_access_dashboard": True, + }, + ) + + response = self.client.get( + f"/api/classrooms/{classroom.id!s}/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", + ) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content) + self.assertDictEqual( + { + "id": str(classroom.id), + "infos": {"returncode": "SUCCESS", "running": "true"}, + "lti_id": str(classroom.lti_id), + "title": classroom.title, + "description": classroom.description, + "started": False, + "ended": False, + "meeting_id": str(classroom.meeting_id), + "welcome_text": classroom.welcome_text, + "playlist": { + "id": str(classroom.playlist.id), + "title": classroom.playlist.title, + "lti_id": classroom.playlist.lti_id, + }, + "starting_at": None, + "estimated_duration": None, + "public_token": None, + "instructor_token": None, + "recordings": [], + "retention_date": None, + "enable_waiting_room": False, + "enable_chat": True, + "enable_presentation_supports": True, + "enable_recordings": True, + "recording_purpose": classroom.recording_purpose, + "enable_shared_notes": True, + "vod_conversion_enabled": True, + }, + content, + ) diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_service_create.py b/src/backend/marsha/bbb/tests/api/classroom/test_service_create.py index 4229f983a6..463f0143f5 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_service_create.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_service_create.py @@ -67,7 +67,7 @@ def test_api_bbb_create_student(self, mock_create_request): """A student should not be able to create a classroom.""" classroom = ClassroomFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.patch( f"/api/classrooms/{classroom.id}/create/", @@ -86,7 +86,7 @@ def test_api_bbb_create_new_classroom( mock_get_meeting_infos.return_value = {"returncode": "SUCCESS"} mock_create_request.return_value = {"returncode": "SUCCESS"} - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) data = {"title": "new title", "welcome_text": "Hello"} response = self.client.patch( @@ -126,7 +126,7 @@ def test_api_bbb_create_existing_classroom( {"message": "A classroom already exists with that classroom ID."} ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) data = {"title": classroom.title, "welcome_text": classroom.welcome_text} response = self.client.patch( diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_service_end.py b/src/backend/marsha/bbb/tests/api/classroom/test_service_end.py index 94761e8667..db175688e0 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_service_end.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_service_end.py @@ -70,7 +70,7 @@ def test_api_bbb_end_classroom_student(self, mock_end_request): """A student should not be able to end a classroom.""" classroom = ClassroomFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.patch( f"/api/classrooms/{classroom.id}/end/", @@ -92,7 +92,7 @@ def test_api_bbb_end_classroom_instructor(self, mock_end_request): "returncode": "SUCCESS", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.patch( f"/api/classrooms/{classroom.id}/end/", diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_service_join.py b/src/backend/marsha/bbb/tests/api/classroom/test_service_join.py index dd27eb08aa..1fd3bfd79d 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_service_join.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_service_join.py @@ -76,7 +76,7 @@ def test_api_bbb_join_student(self): ) jwt_token = StudentLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, consumer_site="consumer_site", user__id="user_id", ) @@ -106,7 +106,7 @@ def test_api_bbb_join_from_other_classroom(self): other_classroom = ClassroomFactory() jwt_token = StudentLtiTokenFactory( - resource=other_classroom, + resource=other_classroom.playlist, consumer_site="consumer_site", user__id="user_id", ) @@ -127,7 +127,7 @@ def test_api_bbb_join_instructor(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, consumer_site="consumer_site", user__id="user_id", ) @@ -153,7 +153,7 @@ def test_api_bbb_join_instructor_no_fullname(self): title="Classroom 1", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.patch( f"/api/classrooms/{classroom.id}/join/", diff --git a/src/backend/marsha/bbb/tests/api/classroom/test_update.py b/src/backend/marsha/bbb/tests/api/classroom/test_update.py index bf2c098fbe..4318f14420 100644 --- a/src/backend/marsha/bbb/tests/api/classroom/test_update.py +++ b/src/backend/marsha/bbb/tests/api/classroom/test_update.py @@ -64,7 +64,7 @@ def test_api_classroom_update_student(self): """A student user should not be able to update a classroom.""" classroom = ClassroomFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) data = {"title": "new title"} @@ -81,7 +81,7 @@ def test_api_classroom_update_instructor_read_only(self): classroom = ClassroomFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom, + resource=classroom.playlist, permissions__can_update=False, ) data = {"title": "new title"} @@ -104,7 +104,7 @@ def test_api_classroom_update_instructor(self, mock_get_meeting_infos): "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) data = {"title": "new title", "welcome_text": "Hello"} response = self.client.patch( @@ -129,7 +129,7 @@ def test_api_classroom_update_instructor_scheduling(self, mock_get_meeting_infos "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) now = datetime(2018, 8, 8, tzinfo=zoneinfo.ZoneInfo("Europe/Paris")) # set microseconds to 0 to compare date surely as serializer truncate them @@ -203,7 +203,7 @@ def test_api_classroom_update_instructor_scheduling_past_date( "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) now = datetime(2018, 8, 8, tzinfo=zoneinfo.ZoneInfo("Europe/Paris")) # set microseconds to 0 to compare date surely as serializer truncate them @@ -250,7 +250,7 @@ def test_api_classroom_update_put_instructor_scheduling( "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) # set microseconds to 0 to compare date surely as serializer truncate them starting_at = (now + timedelta(hours=1)).replace(microsecond=0) @@ -321,7 +321,7 @@ def test_api_classroom_update_starting_at_ended(self, mock_get_meeting_infos): "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) # set microseconds to 0 to compare date surely as serializer truncate them starting_at = (now + timedelta(hours=1)).replace(microsecond=0) @@ -350,7 +350,7 @@ def test_api_classroom_update_estimated_duration_ended( "running": "true", } - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) estimated_duration = timedelta(seconds=60) data = {"estimated_duration": estimated_duration} diff --git a/src/backend/marsha/bbb/tests/api/classroomdocument/test_create.py b/src/backend/marsha/bbb/tests/api/classroomdocument/test_create.py index 89fbf80727..ac9cc952b9 100644 --- a/src/backend/marsha/bbb/tests/api/classroomdocument/test_create.py +++ b/src/backend/marsha/bbb/tests/api/classroomdocument/test_create.py @@ -43,10 +43,10 @@ def test_api_classroom_document_create_student(self): """ classroom = ClassroomFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom) + jwt_token = StudentLtiTokenFactory(resource=classroom.playlist) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( @@ -69,10 +69,10 @@ def test_api_classroom_document_create_instructor_first_document(self): First created document should be the default one. """ classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( @@ -88,7 +88,7 @@ def test_api_classroom_document_create_instructor_first_document(self): self.assertEqual( response.json(), { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": "test.pdf", "id": str(ClassroomDocument.objects.first().id), "is_default": True, @@ -110,9 +110,9 @@ def test_api_classroom_document_create_instructor_second_document(self): classroom=classroom, is_default=True, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( @@ -128,7 +128,7 @@ def test_api_classroom_document_create_instructor_second_document(self): self.assertEqual( response.json(), { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": "test2.pdf", "id": str(ClassroomDocument.objects.latest("created_on").id), "is_default": False, @@ -152,14 +152,14 @@ def test_api_classroom_document_create_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -181,14 +181,14 @@ def test_api_classroom_document_create_user_access_token_organization_admin(self jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -198,7 +198,7 @@ def test_api_classroom_document_create_user_access_token_organization_admin(self self.assertEqual( response.json(), { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": "test.pdf", "id": str(ClassroomDocument.objects.first().id), "is_default": True, @@ -220,14 +220,14 @@ def test_api_classroom_document_create_user_access_token_playlist_admin(self): jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -237,7 +237,7 @@ def test_api_classroom_document_create_user_access_token_playlist_admin(self): self.assertEqual( response.json(), { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": "test.pdf", "id": str(ClassroomDocument.objects.first().id), "is_default": True, @@ -259,14 +259,14 @@ def test_api_classroom_document_create_user_access_token_playlist_instructor(sel jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -276,7 +276,7 @@ def test_api_classroom_document_create_user_access_token_playlist_instructor(sel self.assertEqual( response.json(), { - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), "filename": "test.pdf", "id": str(ClassroomDocument.objects.first().id), "is_default": True, @@ -296,14 +296,14 @@ def test_api_classroom_document_create_user_access_token_playlist_student(self): jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -326,14 +326,14 @@ def test_api_classroom_document_create_user_access_token_admin_other_playlist(se jwt_token = UserAccessTokenFactory(user=ohter_playlist_access.user) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "filename": "test.pdf", "size": 100, - "classroom": str(classroom.id), + "classroom_id": str(classroom.id), } ), ) @@ -346,10 +346,10 @@ def test_api_classroom_document_create_user_access_token_admin_other_playlist(se def test_api_classroom_document_create_file_too_large(self): """With a file size too large the request should fail""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( @@ -370,10 +370,10 @@ def test_api_classroom_document_create_file_too_large(self): def test_api_classroom_document_create_file_no_size(self): """Without file size the request should fail""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.post( - "/api/classroomdocuments/", + f"/api/classrooms/{classroom.id}/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( diff --git a/src/backend/marsha/bbb/tests/api/classroomdocument/test_delete.py b/src/backend/marsha/bbb/tests/api/classroomdocument/test_delete.py index 3b677a1c53..82139e4c82 100644 --- a/src/backend/marsha/bbb/tests/api/classroomdocument/test_delete.py +++ b/src/backend/marsha/bbb/tests/api/classroomdocument/test_delete.py @@ -41,11 +41,14 @@ def test_api_classroom_document_delete_student(self): """ classroom_document = ClassroomDocumentFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom_document.classroom) + jwt_token = StudentLtiTokenFactory( + resource=classroom_document.classroom.playlist + ) self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -60,12 +63,13 @@ def test_api_classroom_document_delete_instructor_document(self): """ classroom_document = ClassroomDocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -93,11 +97,11 @@ def test_api_classroom_document_delete_instructor_first_document(self): classroom=classroom, is_default=False, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) self.assertEqual(ClassroomDocument.objects.count(), 3) response = self.client.delete( - f"/api/classroomdocuments/{first_document.id}/", + f"/api/classrooms/{classroom.id}/classroomdocuments/{first_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -123,11 +127,11 @@ def test_api_classroom_document_delete_instructor_second_document(self): classroom=classroom, is_default=False, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) self.assertEqual(ClassroomDocument.objects.count(), 2) response = self.client.delete( - f"/api/classroomdocuments/{second_document.id}/", + f"/api/classrooms/{classroom.id}/classroomdocuments/{second_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -149,7 +153,8 @@ def test_api_classroom_document_delete_user_access_token(self): self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -170,7 +175,8 @@ def test_api_classroom_document_delete_user_access_token_organization_admin(self jwt_token = UserAccessTokenFactory(user=organization_access.user) self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -193,7 +199,8 @@ def test_api_classroom_document_delete_user_access_token_playlist_admin(self): self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -216,7 +223,8 @@ def test_api_classroom_document_delete_user_access_token_playlist_instructor(sel self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -237,7 +245,8 @@ def test_api_classroom_document_delete_user_access_token_playlist_student(self): self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -263,7 +272,8 @@ def test_api_classroom_document_delete_user_access_token_admin_other_playlist(se self.assertEqual(ClassroomDocument.objects.count(), 1) response = self.client.delete( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) diff --git a/src/backend/marsha/bbb/tests/api/classroomdocument/test_initiate_upload.py b/src/backend/marsha/bbb/tests/api/classroomdocument/test_initiate_upload.py index 8d2995dde8..d925f1d6e7 100644 --- a/src/backend/marsha/bbb/tests/api/classroomdocument/test_initiate_upload.py +++ b/src/backend/marsha/bbb/tests/api/classroomdocument/test_initiate_upload.py @@ -48,7 +48,7 @@ def test_api_classroom_document_initiate_upload_instructor(self): classroom__id="ed08da34-7447-4141-96ff-5740315d7b99", ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) @@ -57,7 +57,8 @@ def test_api_classroom_document_initiate_upload_instructor(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -106,7 +107,7 @@ def test_api_classroom_document_initiate_upload_instructor_without_extension(sel classroom__id="ed08da34-7447-4141-96ff-5740315d7b99", ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) @@ -115,7 +116,8 @@ def test_api_classroom_document_initiate_upload_instructor_without_extension(sel ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -164,7 +166,7 @@ def test_api_classroom_document_initiate_upload_instructor_without_mimetype(self classroom__id="ed08da34-7447-4141-96ff-5740315d7b99", ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) @@ -173,7 +175,8 @@ def test_api_classroom_document_initiate_upload_instructor_without_mimetype(self ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo", "mimetype": "", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -194,7 +197,7 @@ def test_api_classroom_document_initiate_upload_instructor_wrong_mimetype(self): classroom__id="ed08da34-7447-4141-96ff-5740315d7b99", ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) @@ -203,7 +206,8 @@ def test_api_classroom_document_initiate_upload_instructor_wrong_mimetype(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo", "mimetype": "application/wrong-type", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -235,7 +239,8 @@ def test_api_classroom_document_initiate_upload_user_access_token(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -264,7 +269,8 @@ def test_api_classroom_document_initiate_upload_user_access_token_organization_a ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -325,7 +331,8 @@ def test_api_classroom_document_initiate_upload_user_access_token_playlist_admin ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -386,7 +393,8 @@ def test_api_classroom_document_initiate_upload_user_access_token_playlist_instr ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -448,7 +456,8 @@ def test_api_classroom_document_initiate_upload_user_access_token_playlist_stude ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -467,7 +476,7 @@ def test_api_classroom_document_initiate_upload_file_too_large(self): classroom__id="ed08da34-7447-4141-96ff-5740315d7b99", ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) @@ -476,7 +485,8 @@ def test_api_classroom_document_initiate_upload_file_too_large(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/classroomdocuments/{classroom_document.id}/initiate-upload/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 100}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", diff --git a/src/backend/marsha/bbb/tests/api/classroomdocument/test_options.py b/src/backend/marsha/bbb/tests/api/classroomdocument/test_options.py index 8d823042d1..712172a153 100644 --- a/src/backend/marsha/bbb/tests/api/classroomdocument/test_options.py +++ b/src/backend/marsha/bbb/tests/api/classroomdocument/test_options.py @@ -25,16 +25,20 @@ def setUpClass(cls): def test_api_classroom_document_options_anonymous(self): """Anonymous user can't fetch the classroom document options endpoint""" - - response = self.client.options("/api/classroomdocuments/") + classroom = ClassroomFactory() + response = self.client.options( + f"/api/classrooms/{classroom.id}/classroomdocuments/" + ) self.assertEqual(response.status_code, 401) def test_api_classroom_document_options_as_logged_user(self): """A logged user can fetch the classroom document options endpoint""" jwt_token = UserAccessTokenFactory() + classroom = ClassroomFactory() response = self.client.options( - "/api/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/classrooms/{classroom.id}/classroomdocuments/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 200) @@ -42,9 +46,12 @@ def test_api_classroom_document_options_as_student(self): """A student can fetch the classroom document options endpoint""" classroom_document = ClassroomDocumentFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom_document) + jwt_token = StudentLtiTokenFactory( + resource=classroom_document.classroom.playlist + ) response = self.client.options( - "/api/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/classrooms/{classroom_document.classroom.id}/classroomdocuments/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 200) @@ -53,10 +60,11 @@ def test_api_classroom_document_options_instructor(self): """An instructor can fetch the classroom document options endpoint""" classroom = ClassroomFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) response = self.client.options( - "/api/classroomdocuments/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/classrooms/{classroom.id}/classroomdocuments/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 200) diff --git a/src/backend/marsha/bbb/tests/api/classroomdocument/test_update.py b/src/backend/marsha/bbb/tests/api/classroomdocument/test_update.py index 60bdf505ed..66b2076114 100644 --- a/src/backend/marsha/bbb/tests/api/classroomdocument/test_update.py +++ b/src/backend/marsha/bbb/tests/api/classroomdocument/test_update.py @@ -39,11 +39,14 @@ def setUpClass(cls): def test_api_classroom_document_update_student(self): """A student user should not be able to update a classroom_document.""" classroom_document = ClassroomDocumentFactory() - jwt_token = StudentLtiTokenFactory(resource=classroom_document.classroom) + jwt_token = StudentLtiTokenFactory( + resource=classroom_document.classroom.playlist + ) data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", json.dumps(data), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -54,12 +57,13 @@ def test_api_classroom_document_update_instructor(self): """An instructor should be able to update a classroom_document.""" classroom_document = ClassroomDocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=classroom_document.classroom + resource=classroom_document.classroom.playlist ) data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id!s}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -68,7 +72,7 @@ def test_api_classroom_document_update_instructor(self): self.assertEqual( response.json(), { - "classroom": str(classroom_document.classroom.id), + "classroom_id": str(classroom_document.classroom.id), "filename": "updated_name.pdf", "id": str(classroom_document.id), "is_default": False, @@ -86,11 +90,11 @@ def test_api_classroom_document_update_instructor_default(self): is_default=True, ) second_document = ClassroomDocumentFactory(classroom=classroom) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom.playlist) data = {"is_default": True} response = self.client.patch( - f"/api/classroomdocuments/{second_document.id!s}/", + f"/api/classrooms/{classroom.id}/classroomdocuments/{second_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -99,7 +103,7 @@ def test_api_classroom_document_update_instructor_default(self): self.assertEqual( response.json(), { - "classroom": str(second_document.classroom.id), + "classroom_id": str(second_document.classroom.id), "filename": second_document.filename, "id": str(second_document.id), "is_default": True, @@ -122,7 +126,8 @@ def test_api_classroom_document_update_user_access_token(self): data = {"filename": "updated_name.pdf"} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id}/", json.dumps(data), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -138,7 +143,8 @@ def test_api_classroom_document_update_user_access_token_organization_admin(self data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id!s}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -147,7 +153,7 @@ def test_api_classroom_document_update_user_access_token_organization_admin(self self.assertEqual( response.json(), { - "classroom": str(classroom_document.classroom.id), + "classroom_id": str(classroom_document.classroom.id), "filename": "updated_name.pdf", "id": str(classroom_document.id), "is_default": False, @@ -167,7 +173,8 @@ def test_api_classroom_document_update_user_access_token_playlist_admin(self): data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id!s}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -176,7 +183,7 @@ def test_api_classroom_document_update_user_access_token_playlist_admin(self): self.assertEqual( response.json(), { - "classroom": str(classroom_document.classroom.id), + "classroom_id": str(classroom_document.classroom.id), "filename": "updated_name.pdf", "id": str(classroom_document.id), "is_default": False, @@ -196,7 +203,8 @@ def test_api_classroom_document_update_user_access_token_playlist_instructor(sel data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id!s}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -205,7 +213,7 @@ def test_api_classroom_document_update_user_access_token_playlist_instructor(sel self.assertEqual( response.json(), { - "classroom": str(classroom_document.classroom.id), + "classroom_id": str(classroom_document.classroom.id), "filename": "updated_name.pdf", "id": str(classroom_document.id), "is_default": False, @@ -225,7 +233,8 @@ def test_api_classroom_document_update_user_access_token_playlist_student(self): data = {"filename": "updated_name.pdf", "size": 100} response = self.client.patch( - f"/api/classroomdocuments/{classroom_document.id!s}/", + f"/api/classrooms/{classroom_document.classroom.id}" + f"/classroomdocuments/{classroom_document.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", diff --git a/src/backend/marsha/bbb/tests/test_views_lti.py b/src/backend/marsha/bbb/tests/test_views_lti.py index e96c1461ba..e4b3aeef62 100644 --- a/src/backend/marsha/bbb/tests/test_views_lti.py +++ b/src/backend/marsha/bbb/tests/test_views_lti.py @@ -250,7 +250,7 @@ def test_views_lti_classroom_instructor_same_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(classroom.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(classroom.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -589,7 +589,7 @@ def test_views_lti_classroom_instructor_same_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(classroom.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(classroom.playlist.id)) self.assertEqual( jwt_token.payload["user"], { diff --git a/src/backend/marsha/bbb/urls.py b/src/backend/marsha/bbb/urls.py index cdc211f366..964fb061de 100644 --- a/src/backend/marsha/bbb/urls.py +++ b/src/backend/marsha/bbb/urls.py @@ -13,11 +13,11 @@ router = MarshaDefaultRouter() router.register("classrooms", ClassroomViewSet, basename="classrooms") -router.register( - "classroomdocuments", ClassroomDocumentViewSet, basename="classroom_documents" -) classroom_related_router = MarshaDefaultRouter() +classroom_related_router.register( + "classroomdocuments", ClassroomDocumentViewSet, basename="classroom_documents" +) classroom_related_router.register( "recordings", ClassroomRecordingViewSet, basename="recordings" ) diff --git a/src/backend/marsha/core/api/base.py b/src/backend/marsha/core/api/base.py index ecc1acb12a..7db0d37adf 100644 --- a/src/backend/marsha/core/api/base.py +++ b/src/backend/marsha/core/api/base.py @@ -64,23 +64,14 @@ class ObjectVideoRelatedMixin: def get_related_video_id(self): """Get the related video ID from the request.""" - # The video ID in the URL will be mandatory when old routes are deleted. - video_id = ( + # The video ID in the URL is mandatory + return ( self.kwargs.get("video_id") # Backward compatibility with old routes or self.request.data.get("video") or self.request.query_params.get("video") ) - # Backward compatibility with old routes for LTI context - resource = self.request.resource - if resource is not None: - if resource.id and video_id and str(video_id) != str(resource.id): - raise ResourceDoesNotMatchParametersException() - return self.request.resource.id - - return video_id - def get_serializer_context(self): """Extra context provided to the serializer class.""" context = super().get_serializer_context() diff --git a/src/backend/marsha/core/api/file.py b/src/backend/marsha/core/api/file.py index 1cd8bfe3b0..376d8966a7 100644 --- a/src/backend/marsha/core/api/file.py +++ b/src/backend/marsha/core/api/file.py @@ -26,8 +26,8 @@ class DocumentViewSet( queryset = Document.objects.all() serializer_class = serializers.DocumentSerializer permission_classes = [ - permissions.IsTokenResourceRouteObject & permissions.IsTokenInstructor - | permissions.IsTokenResourceRouteObject & permissions.IsTokenAdmin + permissions.IsPlaylistToken + & (permissions.IsTokenInstructor | permissions.IsTokenAdmin) ] def get_permissions(self): diff --git a/src/backend/marsha/core/api/live_session.py b/src/backend/marsha/core/api/live_session.py index dad783403f..a9423389c9 100644 --- a/src/backend/marsha/core/api/live_session.py +++ b/src/backend/marsha/core/api/live_session.py @@ -296,7 +296,7 @@ def push_attendance(self, request, video_id=None): ): # LTI context token = self.request.resource.token token_user = self.request.resource.user - livesession, _ = get_livesession_from_lti(token) + livesession, _ = get_livesession_from_lti(token, video_id) # Update username only if defined in the token user if token_user.get("username"): diff --git a/src/backend/marsha/core/api/shared_live_media.py b/src/backend/marsha/core/api/shared_live_media.py index cfb8843e2f..a5565574bf 100644 --- a/src/backend/marsha/core/api/shared_live_media.py +++ b/src/backend/marsha/core/api/shared_live_media.py @@ -43,6 +43,7 @@ class SharedLiveMediaViewSet( permission_classes = [permissions.NotAllowed] queryset = SharedLiveMedia.objects.select_related("video") + serializer_class = serializers.SharedLiveMediaSerializer filter_backends = [ filters.OrderingFilter, @@ -102,7 +103,13 @@ def get_queryset(self): queryset = super().get_queryset() if self.action in ["list"]: video_id = self.get_related_video_id() - return queryset.filter(video__id=video_id) + queryset = queryset.filter( + video__id=video_id, + ) + if self.request.resource: + queryset = queryset.filter( + video__playlist__id=self.request.resource.id, + ) return queryset diff --git a/src/backend/marsha/core/api/thumbnail.py b/src/backend/marsha/core/api/thumbnail.py index ef74b8ed40..59ef6c6cfa 100644 --- a/src/backend/marsha/core/api/thumbnail.py +++ b/src/backend/marsha/core/api/thumbnail.py @@ -69,7 +69,7 @@ def get_permissions(self): ] elif self.action in ["retrieve", "destroy", "initiate_upload"]: permission_classes = [ - permissions.IsTokenResourceRouteObjectRelatedVideo + permissions.IsPlaylistToken & (permissions.IsTokenInstructor | permissions.IsTokenAdmin) | permissions.IsRelatedVideoPlaylistAdminOrInstructor | permissions.IsRelatedVideoOrganizationAdmin diff --git a/src/backend/marsha/core/api/video.py b/src/backend/marsha/core/api/video.py index 9e95e8dddc..d07e11fd6d 100644 --- a/src/backend/marsha/core/api/video.py +++ b/src/backend/marsha/core/api/video.py @@ -133,7 +133,7 @@ def get_permissions(self): # `VideoConsumer` websocket consumer. permission_classes = [ # With LTI: anyone with a valid token for the video can access - permissions.IsTokenResourceRouteObject + permissions.IsPlaylistToken # With standalone site, only playlist admin or organization admin can access | permissions.IsObjectPlaylistAdminOrInstructor | permissions.IsObjectPlaylistOrganizationAdmin @@ -167,10 +167,9 @@ def get_permissions(self): permission_classes = [ # With LTI: playlist admin or instructor admin or playlist access can access ( - permissions.IsTokenResourceRouteObject + permissions.IsPlaylistToken & (permissions.IsTokenInstructor | permissions.IsTokenAdmin) ) - | permissions.HasPlaylistToken # With standalone site, only playlist admin or instructor # and organization admin can access | permissions.IsObjectPlaylistAdminOrInstructor @@ -197,7 +196,7 @@ def get_permissions(self): ]: permission_classes = [ # With LTI: playlist admin or instructor admin can access - permissions.IsTokenResourceRouteObject + permissions.IsPlaylistToken & (permissions.IsTokenInstructor | permissions.IsTokenAdmin) # With standalone site, playlist admin or instructor can access | permissions.IsObjectPlaylistAdminOrInstructor diff --git a/src/backend/marsha/core/api/xapi.py b/src/backend/marsha/core/api/xapi.py index 00b9458809..06bb8eb6de 100644 --- a/src/backend/marsha/core/api/xapi.py +++ b/src/backend/marsha/core/api/xapi.py @@ -107,9 +107,8 @@ def post(self, request, resource_kind, resource_id): return Response(partial_xapi_statement.errors, status=400) if request.resource: - if request.resource.resource_id != str(resource_id): + if request.resource.resource_id != str(object_instance.playlist.id): return HttpResponseNotFound() - ( statement, lrs_url, diff --git a/src/backend/marsha/core/models/video.py b/src/backend/marsha/core/models/video.py index fe49ff8706..8774a65fc3 100644 --- a/src/backend/marsha/core/models/video.py +++ b/src/backend/marsha/core/models/video.py @@ -643,6 +643,16 @@ class Thumbnail(AbstractImage): on_delete=models.CASCADE, ) + @property + def playlist(self): + """Return the playlist of the video.""" + return self.video.playlist + + @property + def playlist_id(self): + """Return the playlist id of the video.""" + return self.video.playlist_id + class Meta: """Options for the ``Thumbnail`` model.""" @@ -951,7 +961,7 @@ def video_access_reminder_url(self): """Returns url to access video from mails.""" return ( f"//{Site.objects.get_current()}" - f"{reverse('video_direct_access',kwargs={'uuid': self.video.pk})}" + f"{reverse('video_direct_access', kwargs={'uuid': self.video.pk})}" f"?lrpk={self.pk}&key={self.get_generate_salted_hmac()}" ) diff --git a/src/backend/marsha/core/permissions/token.py b/src/backend/marsha/core/permissions/token.py index 7674144136..55c56dc904 100644 --- a/src/backend/marsha/core/permissions/token.py +++ b/src/backend/marsha/core/permissions/token.py @@ -1,6 +1,5 @@ """Custom permission classes for the Marsha project.""" from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Q from rest_framework import permissions @@ -116,13 +115,8 @@ def has_permission(self, request, view): """ return ( request.resource - and models.Playlist.objects.filter( - Q(pk=view.get_object_pk()) - & ( - Q(videos__id=request.resource.id) - | Q(documents__id=request.resource.id) - ), - ).exists() + and request.resource.id == view.get_object_pk() + and models.Playlist.objects.filter(pk=request.resource.id).exists() ) @@ -153,12 +147,48 @@ def has_permission(self, request, view): try: return ( request.resource - and str(view.get_related_object().video.id) == request.resource.id + and str(view.get_related_object().video.playlist.id) + == request.resource.id ) except ObjectDoesNotExist: return False +class IsPlaylistToken(permissions.BasePermission): + """ + Allow a request to proceed. Permission class. + + Only if the user has a playlist token payload. + """ + + def has_permission(self, request, view): + """ + Allow the request. + + Only if the playlist exists. + """ + if request.resource: + playlist_id = request.resource.id + try: + return models.Playlist.objects.filter(id=playlist_id).exists() and ( + str(view.get_queryset().get(id=view.get_object_pk()).playlist_id) + == playlist_id + ) + except (AttributeError, ObjectDoesNotExist): + try: + return models.Playlist.objects.filter(id=playlist_id).exists() and ( + str( + view.get_queryset() + .get(id=view.get_object_pk()) + .video.playlist_id + ) + == playlist_id + ) + except ObjectDoesNotExist: + return False + return False + + class HasPlaylistToken(permissions.BasePermission): """ Allow a request to proceed. Permission class. diff --git a/src/backend/marsha/core/services/live_session.py b/src/backend/marsha/core/services/live_session.py index d76b3abc2b..e7bf12db31 100644 --- a/src/backend/marsha/core/services/live_session.py +++ b/src/backend/marsha/core/services/live_session.py @@ -33,12 +33,12 @@ def is_public_token(token): ) -def get_livesession_from_lti(token): +def get_livesession_from_lti(token, video_id=None): """Get or create livesession for a LTI connection.""" if not is_lti_token(token): raise NotLtiTokenException() - video = Video.objects.get(pk=token.payload["resource_id"]) + video = Video.objects.get(pk=video_id) token_user = token.payload.get("user") consumer_site = ConsumerSite.objects.get(pk=token.payload["consumer_site"]) diff --git a/src/backend/marsha/core/simple_jwt/factories.py b/src/backend/marsha/core/simple_jwt/factories.py index b77d0152a6..2aef7541a8 100644 --- a/src/backend/marsha/core/simple_jwt/factories.py +++ b/src/backend/marsha/core/simple_jwt/factories.py @@ -132,6 +132,31 @@ class BaseResourceTokenFactory(BaseTokenFactory): to forge the JWT, or nothing to use a random UUID. """ + # Should we force the resource to be a playlist? + # + # @staticmethod + # def get_playlist_id(o): + # """Get the playlist id from the resource.""" + # # print("woot") + # # breakpoint() + # resource_id = uuid.uuid4() + # if not o.resource: + # print("no resource") + # pass + # elif o.resource.__class__.__name__ == "Playlist": + # print("playlist resource") + # resource_id = o.resource.id + # elif o.resource.playlist: + # print("resource has playlist") + # resource_id = o.resource.playlist.id + # elif o.resource.video.playlist.id: + # print("resource has video") + # resource_id = o.resource.video.playlist.id + # + # return str(resource_id) + # + # resource_id = factory.LazyAttribute(get_playlist_id) + resource_id = factory.LazyAttribute( lambda o: str(o.resource.id if o.resource else uuid.uuid4()) ) @@ -242,7 +267,7 @@ class LiveSessionLtiTokenFactory(LTIResourceAccessTokenFactory): but this one allows to deeply customize the final JWT. """ - resource_id = factory.LazyAttribute(lambda o: str(o.live_session.video.id)) + resource_id = factory.LazyAttribute(lambda o: str(o.live_session.video.playlist.id)) roles = factory.fuzzy.FuzzyChoice([STUDENT, NONE], getter=lambda x: [x]) consumer_site = factory.LazyAttribute( diff --git a/src/backend/marsha/core/simple_jwt/tokens.py b/src/backend/marsha/core/simple_jwt/tokens.py index c9613f7e96..817bcba93a 100644 --- a/src/backend/marsha/core/simple_jwt/tokens.py +++ b/src/backend/marsha/core/simple_jwt/tokens.py @@ -127,7 +127,7 @@ def for_lti( lti, permissions, session_id, - playlist_id=None, + playlist_id, ): """ Returns an authorization token for the resource in the LTI request that will be provided @@ -167,7 +167,7 @@ def for_lti( - user_fullname """ token = cls.for_resource_id( - str(lti.resource_id), + str(playlist_id), session_id, permissions=permissions, roles=lti.roles, @@ -180,9 +180,7 @@ def for_lti( } ) - if playlist_id: - assert lti.is_instructor or lti.is_admin # nosec - token.payload["playlist_id"] = playlist_id + token.payload["playlist_id"] = playlist_id user_id = getattr(lti, "user_id", None) if user_id: @@ -350,7 +348,7 @@ def for_live_session(cls, live_session, session_id): - anonymous_id (if not from LTI connection) """ token = cls.for_resource_id( - str(live_session.video.id), + str(live_session.video.playlist.id), session_id, locale=react_locale(live_session.language), ) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_create.py b/src/backend/marsha/core/tests/api/live_sessions/test_create.py index 41cd2ad4dd..651c745fd7 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_create.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_create.py @@ -1518,30 +1518,3 @@ def test_api_livesession_send_mail_i18n(self): f"{key_access}]", email_content, ) - - -# Old routes to remove -class LiveSessionCreateApiOldTest(LiveSessionCreateApiTest): - """Test the create API of the liveSession object with old URLs.""" - - def _post_url(self, video): - """Return the url to use to create a live session.""" - return "/api/livesessions/" - - def assert_user_can_create(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_create(user, video) - - def test_api_livesession_create_with_unknown_video(self): - """Token with wrong resource_id should render a 404.""" - video = VideoFactory() - - # token with no user information - jwt_token = ResourceAccessTokenFactory() - response = self.client.post( - self._post_url(video), - {"email": "salome@test-fun-mooc.fr", "should_send_reminders": True}, - content_type="application/json", - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - ) - self.assertEqual(response.status_code, 404) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_delete.py b/src/backend/marsha/core/tests/api/live_sessions/test_delete.py index d7902a5571..4a8708c9be 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_delete.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_delete.py @@ -34,12 +34,3 @@ def test_api_livesession_delete_token_lti(self): HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 405) - - -# Old routes to remove -class LiveSessionDeleteApiOldTest(LiveSessionDeleteApiTest): - """Test the delete API of the liveSession object with old URLs.""" - - def _delete_url(self, video, live_session): - """Return the url to use to delete a live session.""" - return f"/api/livesessions/{live_session.pk}/" diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_display_name.py b/src/backend/marsha/core/tests/api/live_sessions/test_display_name.py index 10858468ea..6a42d8ef63 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_display_name.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_display_name.py @@ -525,16 +525,3 @@ def test_api_livesession_put_username_lti_already_exists( response.json(), {"display_name": "User with that display_name already exists!"}, ) - - -# Old routes to remove -class LiveSessionDisplayNameApiOldTest(LiveSessionDisplayNameApiTest): - """Test the display_name API of the liveSession object with old URLs.""" - - def _put_url(self, video): - """Return the url to use in tests.""" - return "/api/livesessions/display_name/" - - def assert_user_can_set_display_name(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_set_display_name(user, video) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_list.py b/src/backend/marsha/core/tests/api/live_sessions/test_list.py index 0d75458097..6865b096fa 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_list.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_list.py @@ -648,16 +648,3 @@ def test_list_livesession_token_lti_wrong_is_registered_field(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["count"], 0) - - -# Old routes to remove -class LiveSessionListApiOldTest(LiveSessionListApiTest): - """Test the list API of the liveSession object with old URLs.""" - - def _get_url(self, video): - """Return the url to use in tests.""" - return "/api/livesessions/" - - def assert_user_can_list(self, user, video, awaited_results=1): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_list(user, video) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_list_attendances.py b/src/backend/marsha/core/tests/api/live_sessions/test_list_attendances.py index 619d39e3bf..af0758affb 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_list_attendances.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_list_attendances.py @@ -1743,16 +1743,3 @@ def test_api_livesession_read_attendances_admin_live_attendance_key_string(self) self.assertEqual( response.json(), {"live_attendance": "keys in fields should be timestamps"} ) - - -# Old routes to remove -class LiveSessionListAttendancesApiOldTest(LiveSessionListAttendancesApiTest): - """Test the list-attendances API of the liveSession object with old URLs.""" - - def _get_url(self, video): - """Return the url to use in tests.""" - return "/api/livesessions/list_attendances/" - - def assert_user_can_read_attendances(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_read_attendances(user, video) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_push_attendance.py b/src/backend/marsha/core/tests/api/live_sessions/test_push_attendance.py index 4d3125cda4..2de44fb8de 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_push_attendance.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_push_attendance.py @@ -184,7 +184,7 @@ def test_api_livesession_post_attendance_no_attendance(self): """Request without attendance should raise an error.""" video = VideoFactory() jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -205,7 +205,7 @@ def test_api_livesession_post_attendance_token_lti_consumer_site_not_existing( """Pushing an attendance on a not existing video should fail.""" video = VideoFactory() jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), user__email=None, ) @@ -227,7 +227,7 @@ def test_api_livesession_post_attendance_token_lti_email_none_previous_none( """Endpoint push_attendance works with no email and no previous record.""" video = VideoFactory() jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), user__email=None, @@ -287,7 +287,7 @@ def test_api_livesession_post_attendance_token_lti_existing_record(self): ) self.assertEqual(LiveSession.objects.count(), 1) jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, context_id=str(livesession.lti_id), consumer_site=str(video.playlist.consumer_site.id), user__email="chantal@aol.com", @@ -346,7 +346,7 @@ def test_api_livesession_post_new_attendance_token_public_unexisting_video( self.assertEqual(LiveSession.objects.count(), 0) jwt_token = ResourceAccessTokenFactory() response = self.client.post( - f"/api/livesessions/push_attendance/?anonymous_id={anonymous_id}", + f"/api/videos/unexisting/livesessions/push_attendance/?anonymous_id={anonymous_id}", {"live_attendance": {}}, content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", @@ -359,9 +359,9 @@ def test_api_livesession_post_new_attendance_token_public(self): video = VideoFactory() anonymous_id = uuid.uuid4() self.assertEqual(LiveSession.objects.count(), 0) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) response = self.client.post( - f"/api/livesessions/push_attendance/?anonymous_id={anonymous_id}", + f"/api/videos/{video.id}/livesessions/push_attendance/?anonymous_id={anonymous_id}", {"language": "fr", "live_attendance": {}}, content_type="application/json", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", @@ -395,9 +395,10 @@ def test_api_livesession_post_attendance_existing_token_public(self): self.assertEqual(LiveSession.objects.count(), 1) timestamp = to_timestamp(timezone.now()) - jwt_token = ResourceAccessTokenFactory(resource=livesession.video) + jwt_token = ResourceAccessTokenFactory(resource=livesession.video.playlist) response = self.client.post( - f"/api/livesessions/push_attendance/?anonymous_id={livesession.anonymous_id}", + f"/api/videos/{livesession.video_id}/livesessions/push_attendance/" + f"?anonymous_id={livesession.anonymous_id}", { "live_attendance": { timestamp: {"sound": "ON", "tabs": "OFF"}, @@ -446,7 +447,7 @@ def test_api_livesession_post_attendance_token_public_missing_anonymous_id(self) self.assertEqual(LiveSession.objects.count(), 0) timestamp = to_timestamp(timezone.now()) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), { @@ -799,7 +800,7 @@ def test_api_livesession_post_attendance_token_with_could_match_other_records( self.assertEqual(LiveSession.objects.count(), nb_created) # token with same email jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.playlist.consumer_site.id), context_id="Maths", user__id="55555", @@ -856,39 +857,3 @@ def test_api_livesession_post_attendance_token_with_could_match_other_records( }, ) self.assertEqual(livesession.consumer_site, video.playlist.consumer_site) - - -# Old routes to remove -class LiveSessionPushAttendanceApiOldTest(LiveSessionPushAttendanceApiTest): - """Test the push_attendance API of the liveSession object with old URLs.""" - - def _post_url(self, video): - """Return the url to use in tests.""" - return "/api/livesessions/push_attendance/" - - def assert_user_can_push_attendance(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_push_attendance(user, video) - - def test_api_livesession_post_attendance_token_lti_video_not_existing( - self, - ): - """Pushing an attendance on a non existing video should fail.""" - video = VideoFactory() - jwt_token = LTIResourceAccessTokenFactory( - context_id=str(video.playlist.lti_id), - consumer_site=str(video.playlist.consumer_site.id), - user__email=None, - ) - response = self.client.post( - self._post_url(video), - { - "live_attendance": { - to_timestamp(timezone.now()): {"sound": "ON", "tabs": "OFF"} - } - }, - content_type="application/json", - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - ) - - self.assertEqual(response.status_code, 404) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_retrieve.py b/src/backend/marsha/core/tests/api/live_sessions/test_retrieve.py index 779911bda3..f6625bfddc 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_retrieve.py @@ -43,10 +43,10 @@ def setUpTestData(cls): def assert_response_resource_not_accessible(self, response): """Assert response resource not the same as video_id""" - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 403) self.assertEqual( response.json(), - {"detail": "Resource from token does not match given parameters."}, + {"detail": "You do not have permission to perform this action."}, ) def assert_user_cannot_read(self, user, video): @@ -181,7 +181,7 @@ def test_api_livesession_read_token_public(self): video = VideoFactory() livesession = AnonymousLiveSessionFactory(video=video) # token has no consumer_site, no context_id and no user's info - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(livesession.video, livesession), @@ -200,7 +200,7 @@ def test_api_livesession_read_token_public_with_anonymous(self): """ livesession = AnonymousLiveSessionFactory() # token has no consumer_site, no context_id and no user's info - jwt_token = ResourceAccessTokenFactory(resource=livesession.video) + jwt_token = ResourceAccessTokenFactory(resource=livesession.video.playlist) response = self.client.get( f"{self._get_url(livesession.video, livesession)}" @@ -366,7 +366,7 @@ def test_api_livesession_read_token_lti_record_consumer_none(self): # token has context_id so different consumer_site jwt_token = LTIResourceAccessTokenFactory( - resource=livesession.video, # as usual + resource=livesession.video.playlist, # as usual roles=[random.choice([STUDENT, NONE])], user__email=livesession.email, # as usual # below arguments are not usual for anonymous live session @@ -541,7 +541,7 @@ def test_api_livesession_read_token_lti_admin_instruct_token_email_ok(self): username="Sam", # explicit to be found in response ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=livesession.video, + resource=livesession.video.playlist, context_id=str(livesession.video.playlist.lti_id), consumer_site=str(livesession.consumer_site.id), ) @@ -586,7 +586,7 @@ def test_api_livesession_read_token_lti_admin_instruct_token_email_none(self): ) # token with right context_id jwt_token = InstructorOrAdminLtiTokenFactory( - resource=livesession.video, + resource=livesession.video.playlist, context_id=str(livesession.video.playlist.lti_id), consumer_site=str(livesession.consumer_site.id), user__email=None, @@ -627,7 +627,7 @@ def test_api_livesession_read_token_lti_admin_instruct_email_diff(self): jwt_token = InstructorOrAdminLtiTokenFactory( # context_id and consumer_site are not determinant (random uuid here) - resource=livesession.video, + resource=livesession.video.playlist, ) response = self.client.get( @@ -667,7 +667,7 @@ def test_api_livesession_read_token_lti_admin_instruct_record_consumer_diff(self # token with context_id leading to another consumer site jwt_token = InstructorOrAdminLtiTokenFactory( - resource=livesession.video, + resource=livesession.video.playlist, context_id=str(livesession.video.playlist.lti_id), # consumer_site is not other_consumer_site consumer_site=str(livesession.video.playlist.consumer_site.id), @@ -710,7 +710,7 @@ def test_api_livesession_read_token_lti_admin_instruct_record_course_diff(self): # token with context_id leading to another consumer site jwt_token = InstructorOrAdminLtiTokenFactory( - resource=livesession.video, + resource=livesession.video.playlist, context_id=f"{livesession.video.playlist.lti_id}_diff", # consumer_site is not other_consumer_site consumer_site=str(livesession.video.playlist.consumer_site.id), @@ -779,7 +779,7 @@ def test_api_livesession_read_token_public_other_video_context_none_role(self): livesession = AnonymousLiveSessionFactory() # token with no context_id leading to the same undefined consumer_site - jwt_token = ResourceAccessTokenFactory(resource=VideoFactory()) + jwt_token = ResourceAccessTokenFactory(resource=VideoFactory().playlist) response = self.client.get( self._get_url(livesession.video, livesession), @@ -793,7 +793,7 @@ def test_api_livesession_read_token_lti_other_video_context_none_role(self): livesession = LiveSessionFactory(is_from_lti_connection=True) jwt_token = LTIResourceAccessTokenFactory( - resource=VideoFactory(), # other video + resource=VideoFactory().playlist, # other video context_id=str(livesession.video.playlist.lti_id), consumer_site=str(livesession.video.playlist.consumer_site.id), user__email=None, @@ -807,7 +807,7 @@ def test_api_livesession_read_token_lti_other_video_context_none_role(self): self.assert_response_resource_not_accessible(response) def test_api_livesession_read_detail_unknown_video(self): - """Token with wrong resource_id should render a 404.""" + """Token with wrong resource_id should render a 403.""" starting_at = timezone.now() + timedelta(days=5) video = VideoFactory(live_state=IDLE, live_type=RAW, starting_at=starting_at) livesession = AnonymousLiveSessionFactory(video=video) @@ -819,20 +819,3 @@ def test_api_livesession_read_detail_unknown_video(self): HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assert_response_resource_not_accessible(response) - - -# Old routes to remove -class LiveSessionRetrieveApiOldTest(LiveSessionRetrieveApiTest): - """Test the retrieve API of the liveSession object using old URLs.""" - - def _get_url(self, video, live_session): - """Return the url to use in tests.""" - return f"/api/livesessions/{live_session.pk}/" - - def assert_user_can_read(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_read(user, video) - - def assert_response_resource_not_accessible(self, response): - """Assert response resource not accessible""" - self.assertEqual(response.status_code, 403) diff --git a/src/backend/marsha/core/tests/api/live_sessions/test_update.py b/src/backend/marsha/core/tests/api/live_sessions/test_update.py index 7ff4d1115a..02de338b31 100644 --- a/src/backend/marsha/core/tests/api/live_sessions/test_update.py +++ b/src/backend/marsha/core/tests/api/live_sessions/test_update.py @@ -45,10 +45,10 @@ def setUpTestData(cls): def assert_response_resource_not_accessible(self, response): """Assert response resource not the same as video_id""" - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 403) self.assertEqual( response.json(), - {"detail": "Resource from token does not match given parameters."}, + {"detail": "You do not have permission to perform this action."}, ) def assert_user_cannot_patch(self, user, video): @@ -210,7 +210,7 @@ def test_api_livesession_update_put_with_token_not_allowed(self): starting_at=timezone.now() + timedelta(days=100), ) live_session = AnonymousLiveSessionFactory(video=video) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) response = self.client.put( self._update_url(video, live_session), @@ -234,7 +234,7 @@ def test_api_livesession_put_not_allowed(self): ) jwt_token = LTIResourceAccessTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.playlist.consumer_site.id), context_id=live_session.lti_id, user__id=live_session.lti_user_id, @@ -631,7 +631,7 @@ def test_api_livesession_admin_can_patch_any_record_from_the_same_consumer_site( ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.playlist.consumer_site.id), context_id="Maths", ) @@ -699,7 +699,7 @@ def test_api_live_session_admin_using_existing_email(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.playlist.consumer_site.id), context_id="Maths", ) @@ -732,7 +732,7 @@ def test_api_livesession_patch_email_from_anonymous_livesession(self): ) self.assertIsNone(live_session.registered_at) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) now = datetime(2022, 4, 7, tzinfo=baseTimezone.utc) with mock.patch.object(LiveSessionTimezone, "now", return_value=now): @@ -782,7 +782,7 @@ def test_api_livesession_update_email_with_another_anonymous_id(self): video=video, ) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) other_anonymous_id = uuid.uuid4() response = self.client.patch( @@ -813,7 +813,7 @@ def test_api_livesession_patch_language(self): self.assertIsNone(live_session.registered_at) self.assertEqual(live_session.language, "en") - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) # if a wrong language is set response = self.client.patch( @@ -880,41 +880,3 @@ def test_api_livesession_patch_language(self): "video": str(video.id), }, ) - - -# Old routes to remove -class LiveSessionUpdateApiOldTest(LiveSessionUpdateApiTest): - """Test the update API of the liveSession object with old URLs.""" - - def _update_url(self, video, live_session): - """Return the url to use in tests.""" - return f"/api/livesessions/{live_session.pk}/" - - def test_api_livesession_update_with_token_patch_not_allowed(self): - """Patch update is not allowed.""" - video = VideoFactory( - live_state=IDLE, - live_type=RAW, - starting_at=timezone.now() + timedelta(days=100), - ) - AnonymousLiveSessionFactory(video=video) - jwt_token = ResourceAccessTokenFactory(resource=video) - - # This is not clear why this is tested as this is not an expected URL - # still we keep the test for old URLs - response = self.client.patch( - "/api/livesessions/", - {"email": "salome@test-fun-mooc.fr", "should_send_reminders": False}, - content_type="application/json", - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - ) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), {"detail": 'Method "PATCH" not allowed.'}) - - def assert_user_can_patch(self, user, video): - """Defuse original assertion for old URLs""" - self.assert_user_cannot_patch(user, video) - - def assert_response_resource_not_accessible(self, response): - """Assert response resource not accessible""" - self.assertEqual(response.status_code, 403) diff --git a/src/backend/marsha/core/tests/api/playlists/test_retrieve.py b/src/backend/marsha/core/tests/api/playlists/test_retrieve.py index b93299c287..0daf0fd827 100644 --- a/src/backend/marsha/core/tests/api/playlists/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/playlists/test_retrieve.py @@ -54,7 +54,7 @@ def test_retrieve_playlist_through_video_token_instructor(self): """Playlist instructors can retrieve playlists through video token.""" video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/playlists/{video.playlist.id}/", @@ -67,7 +67,7 @@ def test_retrieve_playlist_through_document_token_instructor(self): """Playlist instructors can retrieve playlists through document token.""" document = factories.DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.get( f"/api/playlists/{document.playlist.id}/", diff --git a/src/backend/marsha/core/tests/api/playlists/test_update.py b/src/backend/marsha/core/tests/api/playlists/test_update.py index 302983c450..bfd6198a90 100644 --- a/src/backend/marsha/core/tests/api/playlists/test_update.py +++ b/src/backend/marsha/core/tests/api/playlists/test_update.py @@ -139,7 +139,7 @@ def test_update_playlist_through_video_token_instructor(self): """Playlist instructors or admins can update playlists through video token.""" video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/playlists/{video.playlist.id}/", @@ -163,7 +163,7 @@ def test_update_playlist_through_document_token_instructor(self): """Playlist instructors or admins can update playlists with document token.""" document = factories.DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.get( f"/api/playlists/{document.playlist.id}/", @@ -187,7 +187,7 @@ def test_partial_update_playlist_through_video_token_instructor(self): """Playlist instructors or admins can partially update playlists.""" video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.patch( f"/api/playlists/{video.playlist.id}/", diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_create.py b/src/backend/marsha/core/tests/api/shared_live_media/test_create.py index 602baa03ad..7aa643b3db 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_create.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_create.py @@ -40,7 +40,7 @@ def test_api_shared_live_media_create_instructor(self): """An instructor should be able to create a shared live media for an existing video.""" video = VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), @@ -71,7 +71,7 @@ def test_api_shared_live_media_create_instructor_in_read_only(self): """An instructor in read only should not be able to create a shared live media.""" video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -86,7 +86,7 @@ def test_api_shared_live_media_create_instructor_in_read_only(self): def test_api_shared_live_media_create_student(self): """A student should not be able to create a shared live media.""" video = VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), @@ -276,11 +276,3 @@ def test_api_shared_live_media_create_by_video_organization_admin(self): "video": str(video.id), }, ) - - -class SharedLiveMediaCreateAPIOldTest(SharedLiveMediaCreateAPITest): - """Test the create API of the shared live media object.""" - - def _post_url(self, video): - """Return the url to use in tests.""" - return "/api/sharedlivemedias/" diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_delete.py b/src/backend/marsha/core/tests/api/shared_live_media/test_delete.py index 7403908929..3e2c1ec468 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_delete.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_delete.py @@ -45,7 +45,7 @@ def test_api_shared_live_media_delete_student(self): """A student can not delete a shared live media.""" shared_live_media = SharedLiveMediaFactory() - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) response = self.client.delete( self._delete_url(shared_live_media.video, shared_live_media), @@ -60,7 +60,9 @@ def test_api_shared_live_media_delete_instructor(self): video = VideoFactory() video.shared_live_medias.set([shared_live_media]) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) self.assertTrue(SharedLiveMedia.objects.exists()) @@ -247,7 +249,9 @@ def test_api_shared_live_media_delete_active(self): ) video.shared_live_medias.set([shared_live_media]) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) self.assertTrue(SharedLiveMedia.objects.exists()) @@ -265,11 +269,3 @@ def test_api_shared_live_media_delete_active(self): video.refresh_from_db() self.assertIsNone(video.active_shared_live_media) self.assertIsNone(video.active_shared_live_media_page) - - -class SharedLiveMediaDeleteAPIOldTest(SharedLiveMediaDeleteAPITest): - """Test the delete API of the shared live media object.""" - - def _delete_url(self, video, shared_live_media): - """Return the url to use in tests.""" - return f"/api/sharedlivemedias/{shared_live_media.id}/" diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_initiate_upload.py b/src/backend/marsha/core/tests/api/shared_live_media/test_initiate_upload.py index 538e5906b0..3825a6c0d9 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_initiate_upload.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_initiate_upload.py @@ -56,7 +56,7 @@ def test_api_shared_live_media_initiate_upload_student(self): shared_live_media = SharedLiveMediaFactory() - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) response = self.client.post( self._post_url(shared_live_media.video, shared_live_media), @@ -79,7 +79,9 @@ def test_api_shared_live_media_initiate_upload_instructor(self): video__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) now = datetime(2021, 12, 2, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -141,7 +143,9 @@ def test_api_shared_live_media_initiate_upload_file_without_extension(self): video__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) now = datetime(2021, 12, 2, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -203,7 +207,9 @@ def test_api_shared_live_media_initiate_upload_file_without_mimetype(self): video__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) now = datetime(2021, 12, 2, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -232,7 +238,9 @@ def test_api_shared_live_media_initiate_upload_file_wrong_mimetype(self): video__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) now = datetime(2021, 12, 2, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -566,11 +574,3 @@ def test_api_shared_live_media_initiate_upload_by_video_organization_admin(self) shared_live_media.refresh_from_db() self.assertEqual(shared_live_media.upload_state, defaults.PENDING) - - -class SharedLiveMediaInitiateUploadAPIOldTest(SharedLiveMediaInitiateUploadAPITest): - """Test the update API of the shared live media object.""" - - def _post_url(self, video, shared_live_media): - """Return the url to use in tests.""" - return f"/api/sharedlivemedias/{shared_live_media.id}/initiate-upload/" diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_list.py b/src/backend/marsha/core/tests/api/shared_live_media/test_list.py index c5f0d4ed1d..74fb32a025 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_list.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_list.py @@ -62,7 +62,7 @@ def test_api_shared_live_media_list_student(self): nb_pages=3, ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(video), @@ -94,7 +94,7 @@ def test_api_shared_live_media_list_instructor(self): nb_pages=3, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(video), @@ -180,7 +180,7 @@ def test_api_shared_live_media_list_instructor_other_video(self): video=other_video, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(other_video), @@ -188,7 +188,11 @@ def test_api_shared_live_media_list_instructor_other_video(self): HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) - self.assertEqual(response.status_code, 400) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + {"count": 0, "next": None, "previous": None, "results": []}, + ) @override_settings( CLOUDFRONT_SIGNED_URLS_ACTIVE=True, @@ -220,7 +224,7 @@ def test_api_shared_live_media_list_instructor_ready_to_show_and_signed_url_acti nb_pages=3, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # fix the time so that the url signature is deterministic and can be checked now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) @@ -625,11 +629,3 @@ def test_api_shared_live_media_list_by_video_organization_admin_other_video(self HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) - - -class SharedLiveMediaListAPIOldTest(SharedLiveMediaListAPITest): - """Test the list API of the shared live media object.""" - - def _get_url(self, video): - """Return the url to use in tests.""" - return f"/api/sharedlivemedias/?video={video.id}" diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_retrieve.py b/src/backend/marsha/core/tests/api/shared_live_media/test_retrieve.py index 57b7face4e..a94c50ceeb 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_retrieve.py @@ -54,7 +54,7 @@ def test_api_shared_live_media_read_detail_student_not_ready_to_show(self): nb_pages=None, ) - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) response = self.client.get( self._get_url(shared_live_media.video, shared_live_media), @@ -92,7 +92,7 @@ def test_api_shared_live_media_read_detail_student_ready_to_show(self): video__id="d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb", ) - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) response = self.client.get( self._get_url(shared_live_media.video, shared_live_media), @@ -154,7 +154,7 @@ def test_api_shared_live_media_read_detail_student_ready_to_show_and_signed_url_ video__id="d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb", ) - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) # fix the time so that the url signature is deterministic and can be checked now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) @@ -244,7 +244,7 @@ def test_api_shared_live_media_read_detail_student_ready_to_show_and_show_downlo video__id="d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb", ) - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) # fix the time so that the url signature is deterministic and can be checked now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) @@ -315,7 +315,7 @@ def test_api_shared_live_media_read_detail_instructor_not_ready_to_show(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, permissions__can_update=False, ) @@ -356,7 +356,7 @@ def test_api_shared_live_media_read_detail_instructor_ready_to_show(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, permissions__can_update=False, ) @@ -421,7 +421,7 @@ def test_api_shared_live_media_read_detail_instructor_ready_to_show_and_signed_u ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, permissions__can_update=False, ) @@ -497,7 +497,7 @@ def test_api_shared_live_media_read_detail_other_video(self): other_video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=other_video, + resource=other_video.playlist, roles=[random.choice(["instructor", "administrator", "student"])], ) @@ -513,7 +513,10 @@ def test_api_shared_live_media_read_detail_staff_or_user(self): shared_live_media = SharedLiveMediaFactory() for user in [UserFactory(), UserFactory(is_staff=True)]: self.client.login(username=user.username, password="test") - response = self.client.get(f"/api/sharedlivemedias/{shared_live_media.id}/") + response = self.client.get( + f"/api/videos/{shared_live_media.video.id}/" + f"sharedlivemedias/{shared_live_media.id}/" + ) self.assertEqual(response.status_code, 401) def test_api_shared_live_media_read_detail_by_user_with_no_access(self): @@ -675,11 +678,3 @@ def test_api_shared_live_media_read_detail_by_video_organization_admin(self): "video": str(video.id), }, ) - - -class SharedLiveMediaRetrieveAPIOldTest(SharedLiveMediaRetrieveAPITest): - """Test the retrieve API of the shared live media object.""" - - def _get_url(self, video, shared_live_media): - """Return the url to use in tests.""" - return f"/api/sharedlivemedias/{shared_live_media.id}/" diff --git a/src/backend/marsha/core/tests/api/shared_live_media/test_update.py b/src/backend/marsha/core/tests/api/shared_live_media/test_update.py index f2a301147e..ae735b411a 100644 --- a/src/backend/marsha/core/tests/api/shared_live_media/test_update.py +++ b/src/backend/marsha/core/tests/api/shared_live_media/test_update.py @@ -47,7 +47,7 @@ def test_api_shared_live_media_update_student(self): """A student can not update a shared live media.""" shared_live_media = SharedLiveMediaFactory() - jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video) + jwt_token = StudentLtiTokenFactory(resource=shared_live_media.video.playlist) response = self.client.put( self._update_url(shared_live_media.video, shared_live_media), @@ -62,7 +62,9 @@ def test_api_shared_live_media_update_instructor(self): """An instructor can update a shared live media.""" shared_live_media = SharedLiveMediaFactory(title="update me!") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=shared_live_media.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=shared_live_media.video.playlist + ) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_shared_live_media" @@ -306,11 +308,3 @@ def test_api_shared_live_media_update_by_video_organization_admin(self): "video": str(video.id), }, ) - - -class SharedLiveMediaUpdateAPIOldTest(SharedLiveMediaUpdateAPITest): - """Test the update API of the shared live media object.""" - - def _update_url(self, video, shared_live_media): - """Return the url to use in tests.""" - return f"/api/sharedlivemedias/{shared_live_media.id}/" diff --git a/src/backend/marsha/core/tests/api/thumbnails/test_create.py b/src/backend/marsha/core/tests/api/thumbnails/test_create.py index 965f37bd5c..5c985dc82e 100644 --- a/src/backend/marsha/core/tests/api/thumbnails/test_create.py +++ b/src/backend/marsha/core/tests/api/thumbnails/test_create.py @@ -159,10 +159,10 @@ def test_api_thumbnail_create_student(self): """Student users should not be able to create a thumbnail.""" video = VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( - "/api/thumbnails/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + self._post_url(video), HTTP_AUTHORIZATION=f"Bearer {jwt_token}" ) self.assertEqual(response.status_code, 403) @@ -171,7 +171,7 @@ def test_api_thumbnail_create_instructor_or_admin(self): """LTI instructor or admin should be able to create a thumbnail.""" video = VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), @@ -199,7 +199,7 @@ def test_api_thumbnail_create_instructor_or_admin(self): def test_api_thumbnail_create_instructor_file_too_large(self): """Instructor users should not be able to create a thumbnail if file is too large""" video = VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), @@ -215,7 +215,7 @@ def test_api_thumbnail_create_instructor_file_too_large(self): def test_api_thumbnail_create_instructor_no_size_parameter_provided(self): """Instructor users shouldn't be able to create a thumbnail without a file size""" video = VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -232,7 +232,7 @@ def test_api_thumbnail_create_already_existing_instructor(self): video = VideoFactory() ThumbnailFactory(video=video) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( self._post_url(video), @@ -252,20 +252,12 @@ def test_api_thumbnail_instructor_create_in_read_only(self): thumbnail = ThumbnailFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, permissions__can_update=False, ) response = self.client.post( - "/api/thumbnails/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + self._post_url(thumbnail.video), HTTP_AUTHORIZATION=f"Bearer {jwt_token}" ) self.assertEqual(response.status_code, 403) - - -class ThumbnailCreateApiOldTest(ThumbnailCreateApiTest): - """Test the create API of the thumbnail object.""" - - def _post_url(self, video): - """Return the url to use to create a thumbnail.""" - return "/api/thumbnails/" diff --git a/src/backend/marsha/core/tests/api/thumbnails/test_delete.py b/src/backend/marsha/core/tests/api/thumbnails/test_delete.py index a3b4a7b3dd..cb96f8d2ee 100644 --- a/src/backend/marsha/core/tests/api/thumbnails/test_delete.py +++ b/src/backend/marsha/core/tests/api/thumbnails/test_delete.py @@ -155,7 +155,7 @@ def test_api_thumbnail_delete_student(self): video = VideoFactory() thumbnail = ThumbnailFactory(video=video) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.delete( self._delete_url(video, thumbnail), @@ -165,7 +165,7 @@ def test_api_thumbnail_delete_student(self): def test_api_thumbnail_delete_instructor(self): """Instructor should be able to delete a thumbnail for its video.""" - jwt_token = InstructorOrAdminLtiTokenFactory(resource=self.some_video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=self.some_video.playlist) self.assertEqual(Thumbnail.objects.count(), 1) @@ -187,7 +187,9 @@ def test_api_thumbnail_delete_instructor(self): # Creating a new thumbnail should be allowed. response = self.client.post( - "/api/thumbnails/", {"size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/videos/{self.some_video.pk}/thumbnails/", + {"size": 10}, + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 201) @@ -197,7 +199,7 @@ def test_api_thumbnail_delete_instructor_in_read_only(self): thumbnail = ThumbnailFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, permissions__can_update=False, ) @@ -214,18 +216,10 @@ def test_api_thumbnail_delete_instructor_other_video(self): video_other = VideoFactory() thumbnail = ThumbnailFactory(video=video_other) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video_token) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video_token.playlist) response = self.client.delete( self._delete_url(video_other, thumbnail), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) - - -class ThumbnailDeleteApiOldTest(ThumbnailDeleteApiTest): - """Test the delete API of the thumbnail object.""" - - def _delete_url(self, video, thumbnail): - """Return the url to use to delete a thumbnail.""" - return f"/api/thumbnails/{thumbnail.id}/" diff --git a/src/backend/marsha/core/tests/api/thumbnails/test_initiate_upload.py b/src/backend/marsha/core/tests/api/thumbnails/test_initiate_upload.py index ec0abb66f4..c826021cc0 100644 --- a/src/backend/marsha/core/tests/api/thumbnails/test_initiate_upload.py +++ b/src/backend/marsha/core/tests/api/thumbnails/test_initiate_upload.py @@ -31,7 +31,7 @@ def test_api_thumbnail_initiate_upload_anonymous(self): def test_api_thumbnail_initiate_upload_student(self): """Student users should not be allowed to initiate an upload.""" thumbnail = ThumbnailFactory() - jwt_token = StudentLtiTokenFactory(resource=thumbnail.video) + jwt_token = StudentLtiTokenFactory(resource=thumbnail.video.playlist) response = self.client.post( self._post_url(thumbnail.video, thumbnail), @@ -47,7 +47,7 @@ def test_api_thumbnail_initiate_upload_instructor(self): thumbnail = ThumbnailFactory( id="4ab8079e-ff4d-4d06-9922-4929e4f7a6eb", video=video, upload_state="ready" ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the upload policy for this thumbnail # It should generate a key file with the Unix timestamp of the present time @@ -101,7 +101,7 @@ def test_api_thumbnail_initiate_upload_instructor_read_only(self): thumbnail = ThumbnailFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, permissions__can_update=False, ) @@ -117,7 +117,7 @@ def test_api_thumbnail_initiate_upload_file_too_large(self): """It should not be possible to upload a thumbnail if its size is too large.""" video = VideoFactory(upload_state="ready") thumbnail = ThumbnailFactory(video=video, upload_state="ready") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the upload policy for this thumbnail # It should generate a key file with the Unix timestamp of the present time @@ -140,10 +140,3 @@ def test_api_thumbnail_initiate_upload_file_too_large(self): response.json(), {"size": ["File too large, max size allowed is 10 Bytes"]}, ) - - -class ThumbnailInitiateUploadApiOldTest(ThumbnailInitiateUploadApiTest): - """Test the initiate-upload API of the thumbnail object.""" - - def _post_url(self, video, thumbnail): - return f"/api/thumbnails/{thumbnail.id}/initiate-upload/" diff --git a/src/backend/marsha/core/tests/api/thumbnails/test_options.py b/src/backend/marsha/core/tests/api/thumbnails/test_options.py index f54e447602..3a10e0c2a6 100644 --- a/src/backend/marsha/core/tests/api/thumbnails/test_options.py +++ b/src/backend/marsha/core/tests/api/thumbnails/test_options.py @@ -61,7 +61,7 @@ def test_api_thumbnail_options_as_instructor(self): can query the thumbnail options' endpoint. """ video = VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.options( self._options_url(video), HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -76,7 +76,7 @@ def test_api_thumbnail_options_as_student(self): can query the thumbnail options' endpoint. """ video = VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.options( self._options_url(video), HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -84,11 +84,3 @@ def test_api_thumbnail_options_as_student(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["upload_max_size_bytes"], 10) - - -class ThumbnailOptionsApiOldTest(ThumbnailOptionsApiTest): - """Test the options API of the thumbnail object.""" - - def _options_url(self, video): - """Return the url to use to create a live session.""" - return "/api/thumbnails/" diff --git a/src/backend/marsha/core/tests/api/thumbnails/test_retrieve.py b/src/backend/marsha/core/tests/api/thumbnails/test_retrieve.py index e12963b4b0..1a33a14f56 100644 --- a/src/backend/marsha/core/tests/api/thumbnails/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/thumbnails/test_retrieve.py @@ -172,7 +172,7 @@ def test_api_thumbnail_read_detail_student(self): video = VideoFactory() thumbnail = ThumbnailFactory(video=video) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(video, thumbnail), @@ -186,7 +186,7 @@ def test_api_thumbnail_instructor_read_detail_in_read_only(self): thumbnail = ThumbnailFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, permissions__can_update=False, ) @@ -205,7 +205,7 @@ def test_api_thumbnail_read_detail_token_user(self): ) thumbnail = ThumbnailFactory(video=video, upload_state="pending") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(thumbnail.video, thumbnail), @@ -232,7 +232,7 @@ def test_api_thumbnail_administrator_read_detail_in_read_only(self): thumbnail = ThumbnailFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, permissions__can_update=False, ) @@ -252,7 +252,7 @@ def test_api_thumbnail_read_detail_admin_user(self): thumbnail = ThumbnailFactory(video=video, upload_state="pending") jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, roles=["administrator"], ) @@ -289,7 +289,7 @@ def test_api_thumbnail_read_ready_thumbnail(self): upload_state="ready", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( self._get_url(video, thumbnail), @@ -321,11 +321,3 @@ def test_api_thumbnail_read_ready_thumbnail(self): "video": str(video.id), }, ) - - -class ThumbnailRetrieveApiOldTest(ThumbnailRetrieveApiTest): - """Test the retrieve API of the thumbnail object.""" - - def _get_url(self, video, thumbnail): - """Return the url to use to create a live session.""" - return f"/api/thumbnails/{thumbnail.id}/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_create.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_create.py index b4f4c22f26..7d65356a67 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_create.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_create.py @@ -288,12 +288,3 @@ def test_api_timed_text_track_create_by_video_organization_admin(self): "video": str(video.id), }, ) - - -# Old routes to remove -class TimedTextTrackCreateAPIOldTest(TimedTextTrackCreateAPITest): - """Test the create API of the timed text track object with old URLs.""" - - def _post_url(self, video): - """Return the url to use to create a timed text track.""" - return "/api/timedtexttracks/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_delete.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_delete.py index c007b84d59..141d92eb59 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_delete.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_delete.py @@ -74,7 +74,7 @@ def test_api_timed_text_track_delete_detail_token_user(self): # Delete the timed text tracks using the JWT token for timed_text_track in timed_text_tracks: jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video + resource=timed_text_track.video.playlist, ) response = self.client.delete( self._delete_url(timed_text_track.video, timed_text_track), @@ -111,7 +111,7 @@ def test_api_timed_text_track_delete_instructor_in_read_only(self): timed_text_track = TimedTextTrackFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, permissions__can_update=False, ) @@ -256,11 +256,3 @@ def test_api_timed_text_track_delete_by_video_organization_admin(self): self.assertEqual(response.status_code, 204) self.assertEqual(TimedTextTrack.objects.count(), 0) - - -class TimedTextTrackDeleteAPIOldTest(TimedTextTrackDeleteAPITest): - """Test the delete API of the timed text track object with old URLs.""" - - def _delete_url(self, video, track): - """Return the url to delete a timed text track.""" - return f"/api/timedtexttracks/{track.id}/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_initiate_upload.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_initiate_upload.py index d2d63b9239..413b4c7676 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_initiate_upload.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_initiate_upload.py @@ -46,7 +46,9 @@ def test_api_timed_text_track_initiate_upload_token_user(self): upload_state=random.choice(["ready", "error"]), mode="cc", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Create other timed text tracks to check that their upload state are unaffected # Make sure we avoid unicty constraints by setting a different language @@ -149,7 +151,7 @@ def test_api_timed_text_track_instructor_initiate_upload_in_read_only(self): timed_text_track = TimedTextTrackFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, permissions__can_update=False, ) @@ -465,7 +467,7 @@ def test_api_timed_text_track_initiate_upload_file_too_large(self): upload_state=random.choice(["ready", "error"]), mode="cc", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=track.video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=track.video.playlist) # Get the upload policy for this timed text track # It should generate a key file with the Unix timestamp of the present time @@ -488,10 +490,3 @@ def test_api_timed_text_track_initiate_upload_file_too_large(self): response.json(), {"size": ["file too large, max size allowed is 10 Bytes"]}, ) - - -class TimedTextTrackInitiateUploadAPIOldTest(TimedTextTrackInitiateUploadAPITest): - """Test the create API of the liveSession object with old URLs.""" - - def _post_url(self, video, track): - return f"/api/timedtexttracks/{track.id}/initiate-upload/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_list.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_list.py index f0d6b338dc..714900072a 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_list.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_list.py @@ -38,7 +38,7 @@ def test_api_timed_text_track_read_list_token_user(self): TimedTextTrackFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track_one.video + resource=timed_text_track_one.video.playlist ) response = self.client.get( @@ -62,8 +62,10 @@ def test_api_timed_text_track_read_list_staff_or_user(self): """Users authenticated via a session shouldn't be able to read timed text tracks.""" for user in [UserFactory(), UserFactory(is_staff=True)]: self.client.login(username=user.username, password="test") - TimedTextTrackFactory() - response = self.client.get("/api/timedtexttracks/") + timed_text_track = TimedTextTrackFactory() + response = self.client.get( + f"/api/videos/{timed_text_track.video.id}/timedtexttracks/" + ) self.assertEqual(response.status_code, 401) def test_api_timed_text_track_read_list_by_user_with_no_access(self): @@ -239,43 +241,3 @@ def test_api_timed_text_track_read_list_by_video_organization_admin(self): str(timed_text_track_two.id) in (ttt["id"] for ttt in timed_text_track_list["results"]) ) - - -class TimedTextTrackListAPIOldTest(TimedTextTrackListAPITest): - """Test the create API of the timed text track object with old URLs.""" - - def _get_url(self, video=None): - """Return the url to delete a timed text track.""" - if video: - return f"/api/timedtexttracks/?video={video.id}" - return "/api/timedtexttracks/" - - def test_api_timed_text_track_read_list_by_admin_without_video_filter(self): - """ - Token user with organization access lists timed text tracks without the video filter. - - A user with a user token, with an organization access, cannot list timed text - tracks without a filter, as they have no basis to have permission to do so. - """ - user = factories.UserFactory() - # An organization where the user has access, with a playlist with a video - organization = factories.OrganizationFactory() - playlist = factories.PlaylistFactory(organization=organization) - video = factories.VideoFactory(playlist=playlist) - factories.OrganizationAccessFactory( - user=user, organization=organization, role=models.ADMINISTRATOR - ) - - TimedTextTrackFactory(mode="st", video=video) - TimedTextTrackFactory(mode="cc", video=video) - # Add a timed text track for another video - TimedTextTrackFactory() - - jwt_token = UserAccessTokenFactory(user=user) - - response = self.client.get( - self._get_url(), - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - ) - - self.assertEqual(response.status_code, 403) diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_options.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_options.py index 5a95d53661..c0c846447e 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_options.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_options.py @@ -108,11 +108,3 @@ def test_api_timed_text_track_options_authenticated(self): jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) self.assert_jwt_can_query_options(jwt_token, timed_text_track) - - -class TimedTextTrackOptionsAPIOldTest(TimedTextTrackOptionsAPITest): - """Test the create API of the timed text track object with old URLs.""" - - def _options_url(self, video): - """Return the url to use options on a timed text track.""" - return "/api/timedtexttracks/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_retrieve.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_retrieve.py index c08954bc8e..0aac2a9a76 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_retrieve.py @@ -41,7 +41,7 @@ def test_api_timed_text_track_read_detail_anonymous(self): def test_api_timed_text_track_read_detail_student(self): """Student users should not be allowed to read a timed text track detail.""" timed_text_track = TimedTextTrackFactory() - jwt_token = StudentLtiTokenFactory(resource=timed_text_track.video) + jwt_token = StudentLtiTokenFactory(resource=timed_text_track.video.playlist) # Get the timed text track using the JWT token response = self.client.get( self._get_url(timed_text_track.video, timed_text_track), @@ -66,7 +66,9 @@ def test_api_timed_text_track_read_detail_token_user(self): extension="srt", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Get the timed text track using the JWT token response = self.client.get( @@ -101,7 +103,8 @@ def test_api_timed_text_track_read_detail_token_user(self): # Try getting another timed_text_track other_timed_text_track = TimedTextTrackFactory() response = self.client.get( - f"/api/timedtexttracks/{other_timed_text_track.id}/", + f"/api/videos/{other_timed_text_track.video.id}/" + f"timedtexttracks/{other_timed_text_track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -122,7 +125,9 @@ def test_api_timed_text_track_without_extension_read_detail_token_user(self): upload_state="ready", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Get the timed text track using the JWT token response = self.client.get( @@ -153,7 +158,8 @@ def test_api_timed_text_track_without_extension_read_detail_token_user(self): # Try getting another timed_text_track other_timed_text_track = TimedTextTrackFactory() response = self.client.get( - f"/api/timedtexttracks/{other_timed_text_track.id}/", + f"/api/videos/{other_timed_text_track.video.id}/" + f"timedtexttracks/{other_timed_text_track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -176,7 +182,7 @@ def test_api_timed_text_track_read_detail_admin_user(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, roles=["administrator"], ) @@ -213,7 +219,8 @@ def test_api_timed_text_track_read_detail_admin_user(self): # Try getting another timed_text_track other_timed_text_track = TimedTextTrackFactory() response = self.client.get( - f"/api/timedtexttracks/{other_timed_text_track.id}/", + f"/api/videos/{other_timed_text_track.video.id}/" + f"timedtexttracks/{other_timed_text_track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -227,7 +234,7 @@ def test_api_timed_text_track_read_instructor_in_read_only(self): timed_text_track = TimedTextTrackFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, permissions__can_update=False, ) @@ -244,7 +251,9 @@ def test_api_timed_text_track_read_detail_token_user_no_active_stamp(self): Its "url" field should be set to None. """ timed_text_track = TimedTextTrackFactory(uploaded_on=None) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Get the timed text track using the JWT token response = self.client.get( @@ -262,7 +271,9 @@ def test_api_timed_text_track_read_detail_token_user_not_ready(self): timed_text_track = TimedTextTrackFactory( uploaded_on=None, upload_state=random.choice(["pending", "error", "ready"]) ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Get the timed_text_track linked to the JWT token response = self.client.get( @@ -290,7 +301,9 @@ def test_api_timed_text_track_read_detail_token_user_signed_urls(self, _mock_ope upload_state="ready", extension="srt", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) # Get the timed_text_track via the API using the JWT token # fix the time so that the url signature is deterministic and can be checked @@ -508,11 +521,3 @@ def test_api_timed_text_track_read_detail_by_video_organization_admin(self): "video": str(video.id), }, ) - - -class TimedTextTrackRetrieveAPIOldTest(TimedTextTrackRetrieveAPITest): - """Test the retrieve API of the timed text track object with old URLs.""" - - def _get_url(self, video, track): - """Return the url to retrieve a timed text track.""" - return f"/api/timedtexttracks/{track.id}/" diff --git a/src/backend/marsha/core/tests/api/timed_text_tracks/test_update.py b/src/backend/marsha/core/tests/api/timed_text_tracks/test_update.py index 4076cab143..9639ffa81a 100644 --- a/src/backend/marsha/core/tests/api/timed_text_tracks/test_update.py +++ b/src/backend/marsha/core/tests/api/timed_text_tracks/test_update.py @@ -36,7 +36,9 @@ def test_api_timed_text_track_update_detail_anonymous(self): def test_api_timed_text_track_update_detail_token_user_language(self): """Token users should be able to update the language of their timed_text_track.""" timed_text_track = TimedTextTrackFactory(language="fr") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( self._update_url(timed_text_track.video, timed_text_track), @@ -57,7 +59,9 @@ def test_api_timed_text_track_update_detail_token_user_language(self): def test_api_timed_text_track_update_detail_token_user_closed_captioning(self): """Token users should be able to update the mode flag through the API.""" timed_text_track = TimedTextTrackFactory(mode="cc") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( self._update_url(timed_text_track.video, timed_text_track), @@ -79,7 +83,9 @@ def test_api_timed_text_track_update_detail_token_user_closed_captioning(self): def test_api_timed_text_track_update_detail_token_user_active_stamp(self): """Token users trying to update "active_stamp" through the API should be ignored.""" timed_text_track = TimedTextTrackFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( self._update_url(timed_text_track.video, timed_text_track), @@ -102,7 +108,9 @@ def test_api_timed_text_track_update_detail_token_user_active_stamp(self): def test_api_timed_text_track_update_detail_token_user_upload_state(self): """Token users trying to update "upload_state" through the API should be ignored.""" timed_text_track = TimedTextTrackFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( self._update_url(timed_text_track.video, timed_text_track), @@ -127,7 +135,7 @@ def test_api_timed_text_track_update_instructor_in_read_only(self): timed_text_track = TimedTextTrackFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, permissions__can_update=False, ) @@ -149,7 +157,7 @@ def test_api_timed_text_track_update_by_user_with_no_access(self): jwt_token = UserAccessTokenFactory() response = self.client.get( - f"/api/timedtexttracks/{track.id}/", + f"/api/videos/{track.video.id}/timedtexttracks/{track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = json.loads(response.content) @@ -184,7 +192,7 @@ def test_api_timed_text_track_update_by_video_playlist_instructor(self): jwt_token = UserAccessTokenFactory(user=user) response = self.client.get( - f"/api/timedtexttracks/{track.id}/", + f"/api/videos/{track.video.id}/timedtexttracks/{track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = response.json() @@ -219,7 +227,7 @@ def test_api_timed_text_track_update_by_video_playlist_admin(self): jwt_token = UserAccessTokenFactory(user=user) response = self.client.get( - f"/api/timedtexttracks/{track.id}/", + f"/api/videos/{track.video.id}/timedtexttracks/{track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = response.json() @@ -255,7 +263,7 @@ def test_api_timed_text_track_update_by_video_organization_instructor(self): jwt_token = UserAccessTokenFactory(user=user) response = self.client.get( - f"/api/timedtexttracks/{track.id}/", + f"/api/videos/{track.video.id}/timedtexttracks/{track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = json.loads(response.content) @@ -298,7 +306,7 @@ def test_api_timed_text_track_update_by_video_organization_admin(self): data["language"] = "en" response = self.client.put( - f"/api/timedtexttracks/{track.id}/", + f"/api/videos/{track.video.id}/timedtexttracks/{track.id}/", json.dumps(data), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -313,7 +321,9 @@ def test_api_timed_text_track_patch_detail_token_user_stamp_and_state(self): These 2 fields can only be updated by AWS via the separate update-state API endpoint. """ timed_text_track = TimedTextTrackFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) self.assertEqual(timed_text_track.upload_state, "pending") self.assertIsNone(timed_text_track.uploaded_on) @@ -335,10 +345,12 @@ def test_api_timed_text_track_update_detail_token_id(self): """Token users trying to update the ID of a timed text track they own should be ignored.""" timed_text_track = TimedTextTrackFactory() original_id = timed_text_track.id - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( - f"/api/timedtexttracks/{timed_text_track.id}/", + f"/api/videos/{timed_text_track.video.id}/timedtexttracks/{timed_text_track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = json.loads(response.content) @@ -358,10 +370,12 @@ def test_api_timed_text_track_update_detail_token_video(self): """Token users trying to update the video of a timed text track should be ignored.""" timed_text_track = TimedTextTrackFactory() original_video = timed_text_track.video - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=timed_text_track.video.playlist + ) response = self.client.get( - f"/api/timedtexttracks/{timed_text_track.id}/", + f"/api/videos/{timed_text_track.video.id}/timedtexttracks/{timed_text_track.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) data = json.loads(response.content) @@ -381,7 +395,7 @@ def test_api_timed_text_track_update_detail_token_user_other_video(self): """Token users are not allowed to update a timed text track related to another video.""" other_video = VideoFactory() timed_text_track_update = TimedTextTrackFactory(language="en") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=other_video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=other_video.playlist) data = {"language": "fr", "size": 10} response = self.client.put( @@ -393,34 +407,3 @@ def test_api_timed_text_track_update_detail_token_user_other_video(self): self.assertEqual(response.status_code, 403) timed_text_track_update.refresh_from_db() self.assertEqual(timed_text_track_update.language, "en") - - -class TimedTextTrackUpdateAPIOldTest(TimedTextTrackUpdateAPITest): - """Test the update API of the liveSession object with old URLs.""" - - def _update_url(self, video, track): - """Return the url to update a timed text track.""" - return f"/api/timedtexttracks/{track.id}/" - - def test_api_timed_text_track_update_detail_token_video(self): - """Token users trying to update the video of a timed text return an error.""" - timed_text_track = TimedTextTrackFactory() - original_video = timed_text_track.video - jwt_token = InstructorOrAdminLtiTokenFactory(resource=timed_text_track.video) - - response = self.client.get( - f"/api/timedtexttracks/{timed_text_track.id}/", - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - ) - data = json.loads(response.content) - data["video"] = str(VideoFactory().id) - - response = self.client.put( - self._update_url(timed_text_track.video, timed_text_track), - json.dumps(data), - HTTP_AUTHORIZATION=f"Bearer {jwt_token}", - content_type="application/json", - ) - self.assertEqual(response.status_code, 400) - timed_text_track.refresh_from_db() - self.assertEqual(timed_text_track.video, original_video) diff --git a/src/backend/marsha/core/tests/api/video/test_bulk_destroy.py b/src/backend/marsha/core/tests/api/video/test_bulk_destroy.py index 4aca08fdd8..e4483885f4 100644 --- a/src/backend/marsha/core/tests/api/video/test_bulk_destroy.py +++ b/src/backend/marsha/core/tests/api/video/test_bulk_destroy.py @@ -34,7 +34,7 @@ def test_api_video_bulk_delete_detail_anonymous(self): def test_api_video_bulk_delete_detail_token_user(self): """A token user associated to a video should not be able to delete it or any other.""" video1 = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video1) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video1.playlist) # Try deleting the video linked to the JWT token and the other one response = self.client.delete( @@ -50,7 +50,7 @@ def test_api_video_bulk_delete_detail_token_user(self): def test_api_video_bulk_delete_detail_student(self): """Student users should not be able to delete a video.""" video1 = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video1) + jwt_token = StudentLtiTokenFactory(resource=video1.playlist) response = self.client.delete( self._api_url(), @@ -241,7 +241,7 @@ def test_api_video_instructor_bulk_delete_video_in_read_only(self): video1 = factories.VideoFactory() video2 = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video1, + resource=video1.playlist, permissions__can_update=False, ) diff --git a/src/backend/marsha/core/tests/api/video/test_create.py b/src/backend/marsha/core/tests/api/video/test_create.py index 73fb0eea61..81e6b22a9e 100644 --- a/src/backend/marsha/core/tests/api/video/test_create.py +++ b/src/backend/marsha/core/tests/api/video/test_create.py @@ -39,7 +39,7 @@ def test_api_video_create_token_user_playlist_preexists(self): def test_api_video_create_student(self): """Student users should not be able to create videos.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( "/api/videos/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", diff --git a/src/backend/marsha/core/tests/api/video/test_destroy.py b/src/backend/marsha/core/tests/api/video/test_destroy.py index 04d1facfad..5ba977c630 100644 --- a/src/backend/marsha/core/tests/api/video/test_destroy.py +++ b/src/backend/marsha/core/tests/api/video/test_destroy.py @@ -30,7 +30,7 @@ def test_api_video_delete_detail_anonymous(self): def test_api_video_delete_detail_token_user(self): """A token user associated to a video should not be able to delete it or any other.""" videos = factories.VideoFactory.create_batch(2) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=videos[0]) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=videos[0].playlist) # Try deleting the video linked to the JWT token and the other one for video in videos: @@ -44,7 +44,7 @@ def test_api_video_delete_detail_token_user(self): def test_api_video_delete_detail_student(self): """Student users should not be able to delete a video.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.delete( f"/api/videos/{video.id}/", @@ -180,7 +180,7 @@ def test_api_video_instructor_delete_video_in_read_only(self): """An instructor with read_only set to true should not be able to delete the video.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) diff --git a/src/backend/marsha/core/tests/api/video/test_harvest_live.py b/src/backend/marsha/core/tests/api/video/test_harvest_live.py index cd01e23b17..789434b265 100644 --- a/src/backend/marsha/core/tests/api/video/test_harvest_live.py +++ b/src/backend/marsha/core/tests/api/video/test_harvest_live.py @@ -180,7 +180,7 @@ def test_api_video_instructor_harvest_live_in_read_only(self): """An instructor with read_only set to true should not be able to harvest a live.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -193,7 +193,7 @@ def test_api_video_instructor_harvest_live_in_read_only(self): def test_api_video_student_harvest_live(self): """A student should not be able to harvest a live.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( f"/api/videos/{video.id}/harvest-live/", @@ -227,7 +227,7 @@ def test_api_video_instructor_harvest_idle_live(self): live_type=JITSI, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.post( f"/api/videos/{video.id}/harvest-live/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", @@ -280,7 +280,7 @@ def test_api_video_instructor_harvest_paused_live_recording_slice(self): }, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch.object(timezone, "now", return_value=stop), mock.patch.object( api.video, "delete_aws_element_stack" @@ -431,7 +431,7 @@ def test_api_video_instructor_harvest_paused_live_no_recording_slice(self): }, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch.object(timezone, "now", return_value=stop), mock.patch.object( api.video, "delete_aws_element_stack" @@ -495,7 +495,7 @@ def test_api_video_instructor_harvest_paused_live_missing_manifest(self): }, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch.object( api.video, "delete_aws_element_stack" @@ -595,7 +595,7 @@ def test_api_video_instructor_harvest_live_wrong_live_state(self): ), live_type=JITSI, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups" diff --git a/src/backend/marsha/core/tests/api/video/test_initiate_live.py b/src/backend/marsha/core/tests/api/video/test_initiate_live.py index 707ac44c4e..8d197f48fc 100644 --- a/src/backend/marsha/core/tests/api/video/test_initiate_live.py +++ b/src/backend/marsha/core/tests/api/video/test_initiate_live.py @@ -36,7 +36,7 @@ def test_api_video_instructor_initiate_live_in_read_only(self): """An instructor with read_only set to true should not be able to initiate a live.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -49,7 +49,7 @@ def test_api_video_instructor_initiate_live_in_read_only(self): def test_api_video_student_initiate_live(self): """A student should not be able to initiate a live.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( f"/api/videos/{video.id}/initiate-live/", @@ -82,7 +82,7 @@ def test_api_video_instructor_initiate_live(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video" ) as mock_dispatch_video: @@ -155,7 +155,9 @@ def test_api_video_instructor_initiate_live_with_playlist_token(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = PlaylistLtiTokenFactory(playlist=video.playlist) + jwt_token = PlaylistLtiTokenFactory( + resource=video.playlist, playlist=video.playlist + ) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video" ) as mock_dispatch_video: @@ -167,6 +169,7 @@ def test_api_video_instructor_initiate_live_with_playlist_token(self): video.refresh_from_db() mock_dispatch_video.assert_called_with(video, to_admin=True) + # breakpoint() self.assertEqual(response.status_code, 200) content = json.loads(response.content) @@ -224,7 +227,7 @@ def test_api_video_instructor_initiate_jitsi_live(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video" @@ -305,7 +308,7 @@ def test_api_video_instructor_initiate_jitsi_live_with_token(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) now = datetime(2022, 5, 4, tzinfo=baseTimezone.utc) with mock.patch( diff --git a/src/backend/marsha/core/tests/api/video/test_initiate_upload.py b/src/backend/marsha/core/tests/api/video/test_initiate_upload.py index 39da5c04bd..083c637763 100644 --- a/src/backend/marsha/core/tests/api/video/test_initiate_upload.py +++ b/src/backend/marsha/core/tests/api/video/test_initiate_upload.py @@ -37,7 +37,7 @@ def test_api_video_instructor_initiate_upload_in_read_only(self): """An instructor with read_only set to true should not be able to initiate an upload.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -54,7 +54,7 @@ def test_api_video_initiate_upload_token_user(self): id="27a23f52-3379-46a2-94fa-697b59cfe3c7", upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Create another video to check that its upload state is unaffected other_video = factories.VideoFactory( @@ -374,7 +374,7 @@ def test_api_video_initiate_upload_file_without_size(self): id="27a23f52-3379-46a2-94fa-697b59cfe3c7", upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -399,7 +399,7 @@ def test_api_video_initiate_upload_file_too_large(self): id="27a23f52-3379-46a2-94fa-697b59cfe3c7", upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( diff --git a/src/backend/marsha/core/tests/api/video/test_jitsi_info.py b/src/backend/marsha/core/tests/api/video/test_jitsi_info.py index 98043b4b13..bc7a77b3f4 100644 --- a/src/backend/marsha/core/tests/api/video/test_jitsi_info.py +++ b/src/backend/marsha/core/tests/api/video/test_jitsi_info.py @@ -33,7 +33,7 @@ def test_jitsi_info_anonymous(self): def test_jitsi_info_for_a_student(self): """A student user can not fetch jitsi info.""" video = VideoFactory(live_state=RUNNING, live_type=JITSI) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/", @@ -46,7 +46,7 @@ def test_jitsi_info_for_an_admin(self): An instructor or an adminstrator with a resource token should be able to fetch jitsi info. """ video = VideoFactory(live_state=RUNNING, live_type=JITSI) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/", @@ -75,7 +75,7 @@ def test_jitsi_info_for_an_admin_moderator_not_specified(self): When the moderator query string is not specified, the False value should be used. """ video = VideoFactory(live_state=RUNNING, live_type=JITSI) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/", @@ -117,7 +117,7 @@ def test_jitsi_info_admin_user_moderator_false_specified(self): not be in the response. """ video = VideoFactory(live_state=RUNNING, live_type=JITSI) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/?moderator=false", @@ -159,7 +159,7 @@ def test_jitsi_info_admin_user_moderator_true_specified(self): be in the response. """ video = VideoFactory(live_state=RUNNING, live_type=JITSI) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/?moderator=true", @@ -277,7 +277,7 @@ def test_jitsi_info_on_non_live_video(self): """ video = VideoFactory(live_state=None, live_type=None) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/", @@ -291,7 +291,7 @@ def test_jitsi_info_on_live_not_jitsi(self): """ video = VideoFactory(live_state=RUNNING, live_type=RAW) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/jitsi/", diff --git a/src/backend/marsha/core/tests/api/video/test_list.py b/src/backend/marsha/core/tests/api/video/test_list.py index 8a8ebb5ab6..4642eafb7a 100644 --- a/src/backend/marsha/core/tests/api/video/test_list.py +++ b/src/backend/marsha/core/tests/api/video/test_list.py @@ -31,7 +31,7 @@ def test_api_video_read_list_token_user(self): """ video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) diff --git a/src/backend/marsha/core/tests/api/video/test_live_pairing.py b/src/backend/marsha/core/tests/api/video/test_live_pairing.py index ce0dabc03b..964ae687b8 100644 --- a/src/backend/marsha/core/tests/api/video/test_live_pairing.py +++ b/src/backend/marsha/core/tests/api/video/test_live_pairing.py @@ -170,7 +170,7 @@ def test_pairing_secret_by_playlist_admin(self): def test_api_video_student_pairing_secret(self): """A student should not be able to request a live pairing secret.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/pairing-secret/", @@ -199,7 +199,7 @@ def test_api_video_pairing_secret_staff_or_user(self): def test_api_video_instructor_pairing_secret_non_jitsi(self): """A request related to a non jitsi video should raise a 400 error.""" video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/pairing-secret/", @@ -215,7 +215,7 @@ def test_api_video_instructor_pairing_secret_non_jitsi(self): def test_api_video_instructor_pairing_secret_1st_request(self): """An instructor should be able to request a live pairing secret.""" video = factories.VideoFactory(live_state=IDLE, live_type=JITSI) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/pairing-secret/", @@ -235,7 +235,7 @@ def test_api_video_instructor_pairing_secret_2nd_request(self): live_pairing = LivePairingFactory(video=video) previous_secret = live_pairing.secret - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/pairing-secret/", @@ -256,7 +256,7 @@ def test_api_video_pairing_secret_delete_expired(self): ) with mock.patch.object(timezone, "now", return_value=expired_date): video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) self.client.get( f"/api/videos/{video.id}/pairing-secret/", diff --git a/src/backend/marsha/core/tests/api/video/test_live_participants_asking_to_join.py b/src/backend/marsha/core/tests/api/video/test_live_participants_asking_to_join.py index 1c6b0611e2..0b844e7fc3 100644 --- a/src/backend/marsha/core/tests/api/video/test_live_participants_asking_to_join.py +++ b/src/backend/marsha/core/tests/api/video/test_live_participants_asking_to_join.py @@ -218,7 +218,7 @@ def test_api_video_participants_post_asking_to_join_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -267,7 +267,7 @@ def test_api_video_participants_post_asking_to_join_instructor(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -322,7 +322,7 @@ def test_api_video_participants_post_asking_to_join_invalid_participants(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -361,7 +361,7 @@ def test_api_video_participants_post_asking_to_join_invalid_empty(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -399,7 +399,7 @@ def test_api_video_participants_post_asking_to_join_extra_data(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -458,7 +458,7 @@ def test_api_video_participants_post_asking_to_join_no_change(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -496,7 +496,7 @@ def test_api_video_participants_post_asking_to_join_join_mode_denied(self): video = VideoFactory(join_mode=DENIED) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -543,7 +543,7 @@ def test_api_video_participants_delete_asking_to_join_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -592,7 +592,7 @@ def test_api_video_participants_delete_asking_to_join_instructor(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -639,7 +639,7 @@ def test_api_video_participants_delete_asking_to_join_invalid_participants(self) video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -678,7 +678,7 @@ def test_api_video_participants_delete_asking_to_join_invalid_empty(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -723,7 +723,7 @@ def test_api_video_participants_delete_asking_to_join_extra_data(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -777,7 +777,7 @@ def test_api_video_participants_delete_asking_to_join_no_change(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) diff --git a/src/backend/marsha/core/tests/api/video/test_live_participants_joined.py b/src/backend/marsha/core/tests/api/video/test_live_participants_joined.py index fbaaf1c0fe..d6c8244ae1 100644 --- a/src/backend/marsha/core/tests/api/video/test_live_participants_joined.py +++ b/src/backend/marsha/core/tests/api/video/test_live_participants_joined.py @@ -224,7 +224,7 @@ def test_api_video_participants_post_joined_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -273,7 +273,7 @@ def test_api_video_participants_post_joined_instructor(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -325,7 +325,7 @@ def test_api_video_participants_post_joined_invalid_participants(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -376,7 +376,7 @@ def test_api_video_participants_post_joined_instructor_join_mode_denied(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -423,7 +423,7 @@ def test_api_video_participants_delete_joined_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -472,7 +472,7 @@ def test_api_video_participants_delete_joined_instructor(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -519,7 +519,7 @@ def test_api_video_participants_delete_joined_invalid_participants(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -558,7 +558,7 @@ def test_api_video_participants_delete_joined_invalid_empty(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -603,7 +603,7 @@ def test_api_video_participants_delete_joined_extra_data(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -657,7 +657,7 @@ def test_api_video_participants_delete_joined_no_change(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) diff --git a/src/backend/marsha/core/tests/api/video/test_live_to_vod.py b/src/backend/marsha/core/tests/api/video/test_live_to_vod.py index 7ebb931448..3117549688 100644 --- a/src/backend/marsha/core/tests/api/video/test_live_to_vod.py +++ b/src/backend/marsha/core/tests/api/video/test_live_to_vod.py @@ -165,7 +165,7 @@ def test_api_video_instructor_harvested_live_to_vod(self): uploaded_on="2019-09-24 07:24:40+00", resolutions=[240, 480, 720], ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups" @@ -277,7 +277,7 @@ def test_api_video_instructor_non_harvested_live_to_vod(self): uploaded_on="2019-09-24 07:24:40+00", resolutions=[240, 480, 720], ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups" diff --git a/src/backend/marsha/core/tests/api/video/test_metadata.py b/src/backend/marsha/core/tests/api/video/test_metadata.py index e0499a843d..5ef34af42d 100644 --- a/src/backend/marsha/core/tests/api/video/test_metadata.py +++ b/src/backend/marsha/core/tests/api/video/test_metadata.py @@ -20,7 +20,7 @@ def test_api_video_options_as_student(self): """A student can fetch the video options endpoint""" video = VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.options( "/api/videos/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" ) @@ -61,7 +61,7 @@ def test_api_video_options_as_instructor(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) diff --git a/src/backend/marsha/core/tests/api/video/test_retrieve.py b/src/backend/marsha/core/tests/api/video/test_retrieve.py index e6e2518715..c5a75d86ed 100644 --- a/src/backend/marsha/core/tests/api/video/test_retrieve.py +++ b/src/backend/marsha/core/tests/api/video/test_retrieve.py @@ -48,7 +48,7 @@ def test_api_video_read_detail_anonymous(self): def test_api_video_read_detail_student(self): """Student users should be allowed to read a video detail.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( f"/api/videos/{video.id}/", @@ -63,7 +63,7 @@ def test_api_video_read_detail_scheduled_video_student(self): live_state=IDLE, live_type=RAW, starting_at=starting_at ) self.assertTrue(video.is_scheduled) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( f"/api/videos/{video.id}/", @@ -83,7 +83,7 @@ def test_api_video_read_detail_scheduled_past_video_student(self): with mock.patch.object(timezone, "now", return_value=now): self.assertFalse(video.is_scheduled) self.assertEqual(video.live_state, IDLE) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( f"/api/videos/{video.id}/", @@ -95,7 +95,7 @@ def test_api_video_read_detail_student_other_video(self): """Student users should not be allowed to read an other video detail.""" video = factories.VideoFactory() other_video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( f"/api/videos/{other_video.id}/", @@ -111,7 +111,7 @@ def test_api_video_read_detail_student_join_modes(self): live_state=RUNNING, live_type=JITSI, ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( f"/api/videos/{video.id}/", @@ -126,7 +126,7 @@ def test_api_video_read_detail_admin_token_user(self): video = factories.VideoFactory(upload_state="pending") jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, roles=["administrator"], ) @@ -173,7 +173,7 @@ def test_api_video_read_detail_token_user(self): video=video, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( @@ -369,7 +369,7 @@ def test_api_video_read_detail_token_user_nested_shared_live_media_urls_signed( video=video, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # fix the time so that the url signature is deterministic and can be checked now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) @@ -588,7 +588,7 @@ def test_api_video_read_detail_token_student_user_nested_shared_live_media_urls_ video=video, ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) # fix the time so that the url signature is deterministic and can be checked now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) @@ -779,7 +779,7 @@ def test_api_video_read_detail_xmpp_enabled_live_state_idle(self): ) jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id="Maths", consumer_site=str(video.playlist.consumer_site.id), ) @@ -842,7 +842,7 @@ def test_api_video_read_detail_as_instructor_in_read_only(self): video = factories.VideoFactory(upload_state="ready") jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -861,7 +861,7 @@ def test_api_video_read_detail_token_user_no_active_stamp(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( @@ -929,7 +929,7 @@ def test_api_video_read_detail_token_user_not_sucessfully_uploaded(self): playlist__title="foo bar", playlist__lti_id="course-v1:ufr+mathematics+00001", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( @@ -1000,7 +1000,7 @@ def test_api_video_read_detail_token_user_signed_urls(self, _mock_open): resolutions=[144], playlist__title="foo", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token # fix the time so that the url signature is deterministic and can be checked @@ -1275,7 +1275,7 @@ def test_api_video_with_a_thumbnail(self): upload_state="ready", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Get the video linked to the JWT token response = self.client.get( diff --git a/src/backend/marsha/core/tests/api/video/test_shared_live_media.py b/src/backend/marsha/core/tests/api/video/test_shared_live_media.py index 36d1afed19..e92283c357 100644 --- a/src/backend/marsha/core/tests/api/video/test_shared_live_media.py +++ b/src/backend/marsha/core/tests/api/video/test_shared_live_media.py @@ -547,7 +547,7 @@ def test_api_video_shared_live_media_start_student(self): shared_live_media = SharedLiveMediaFactory() jwt_token = StudentLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, context_id=str(shared_live_media.video.playlist.lti_id), consumer_site=str(shared_live_media.video.playlist.consumer_site.id), ) @@ -569,7 +569,7 @@ def test_api_video_shared_live_media_navigate_student(self): shared_live_media = SharedLiveMediaFactory() jwt_token = StudentLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) response = self.client.patch( @@ -589,7 +589,7 @@ def test_api_video_shared_live_media_end_student(self): shared_live_media = SharedLiveMediaFactory() jwt_token = StudentLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) response = self.client.patch( @@ -692,7 +692,7 @@ def test_api_video_shared_live_media_start_instructor_ready(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( channel_layers_utils, "dispatch_video_to_groups" @@ -906,7 +906,7 @@ def test_api_video_shared_live_media_start_not_ready(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -959,7 +959,7 @@ def test_api_video_shared_live_media_start_wrong_sharedlivemedia_id(self): other_shared_live_media = SharedLiveMediaFactory(upload_state=READY) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1016,7 +1016,7 @@ def test_api_video_shared_live_media_start_already_started(self): shared_live_media = SharedLiveMediaFactory(video=video) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1075,7 +1075,7 @@ def test_api_video_shared_live_media_navigate_instructor(self): video.shared_live_medias.set([shared_live_media]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1234,7 +1234,7 @@ def test_api_video_shared_live_media_navigate_no_active(self): ) video.shared_live_medias.set([shared_live_media]) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch.object( channel_layers_utils, "dispatch_video_to_groups" @@ -1295,7 +1295,7 @@ def test_api_video_shared_live_media_navigate_unexisting_page(self): video.shared_live_medias.set([shared_live_media]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1357,7 +1357,7 @@ def test_api_video_shared_live_media_navigate_undefined_page(self): video.shared_live_medias.set([shared_live_media]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1419,7 +1419,7 @@ def test_api_video_shared_live_media_navigate_missing_page(self): video.shared_live_medias.set([shared_live_media]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1480,7 +1480,7 @@ def test_api_video_shared_live_media_end_instructor(self): video.shared_live_medias.set([shared_live_media]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=shared_live_media.video, + resource=shared_live_media.video.playlist, ) with mock.patch.object( @@ -1626,7 +1626,7 @@ def test_api_video_shared_live_media_end_no_active(self): ) video.shared_live_medias.set([shared_live_media]) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch.object( channel_layers_utils, "dispatch_video_to_groups" diff --git a/src/backend/marsha/core/tests/api/video/test_start_live.py b/src/backend/marsha/core/tests/api/video/test_start_live.py index 6e642c66aa..fd30440d3a 100644 --- a/src/backend/marsha/core/tests/api/video/test_start_live.py +++ b/src/backend/marsha/core/tests/api/video/test_start_live.py @@ -203,7 +203,7 @@ def test_api_instructor_start_non_created_live(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -372,7 +372,7 @@ def test_api_instructor_start_already_created_live(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -527,7 +527,7 @@ def test_api_instructor_start_stopped_live(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -686,7 +686,7 @@ def test_api_instructor_start_harvested_live(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -867,7 +867,7 @@ def test_api_instructor_start_non_live_video(self): id="27a23f52-3379-46a2-94fa-697b59cfe3c7", upload_state=random.choice([s[0] for s in STATE_CHOICES]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # start a live video, with mock.patch.object(api.video, "start_live_channel"), mock.patch( @@ -894,7 +894,7 @@ def test_api_instructor_start_aws_raising_exception(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -944,7 +944,7 @@ def test_api_instructor_start_non_idle_or_stopped_live(self): ), live_type=RAW, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) with mock.patch( "marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups" diff --git a/src/backend/marsha/core/tests/api/video/test_start_stop_recording.py b/src/backend/marsha/core/tests/api/video/test_start_stop_recording.py index be78a17c7c..b734f6d921 100644 --- a/src/backend/marsha/core/tests/api/video/test_start_stop_recording.py +++ b/src/backend/marsha/core/tests/api/video/test_start_stop_recording.py @@ -319,7 +319,7 @@ def test_api_video_recording_start_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -341,7 +341,7 @@ def test_api_video_recording_stop_student(self): video = VideoFactory() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, context_id=str(video.playlist.lti_id), consumer_site=str(video.playlist.consumer_site.id), ) @@ -419,7 +419,7 @@ def test_api_video_recording_start_instructor(self): self.assertEqual(video.recording_slices, []) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -553,7 +553,7 @@ def test_api_video_recording_start_not_allowed(self): self.assertEqual(video.recording_slices, []) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -618,7 +618,7 @@ def test_api_video_recording_start_live_not_running(self): self.assertEqual(video.recording_slices, []) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) @@ -685,7 +685,7 @@ def test_api_video_recording_stop_instructor_started_slice(self): self.assertEqual(video.recording_slices, [{"start": to_timestamp(start)}]) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, user__id="56255f3807599c377bf0e5bf072359fd", ) diff --git a/src/backend/marsha/core/tests/api/video/test_stats.py b/src/backend/marsha/core/tests/api/video/test_stats.py index 59846a85ff..78de6c658f 100644 --- a/src/backend/marsha/core/tests/api/video/test_stats.py +++ b/src/backend/marsha/core/tests/api/video/test_stats.py @@ -218,7 +218,7 @@ def test_api_video_stats_instructor(self): video = VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, ) with mock.patch( diff --git a/src/backend/marsha/core/tests/api/video/test_stop_live.py b/src/backend/marsha/core/tests/api/video/test_stop_live.py index 678a6d0a72..2b4076757a 100644 --- a/src/backend/marsha/core/tests/api/video/test_stop_live.py +++ b/src/backend/marsha/core/tests/api/video/test_stop_live.py @@ -178,7 +178,7 @@ def test_api_video_instructor_stop_live_in_read_only(self): """An instructor with read_only set to true should not be able to stop a live.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -191,7 +191,7 @@ def test_api_video_instructor_stop_live_in_read_only(self): def test_api_video_student_stop_live(self): """A student should not be able to stop a live.""" video = factories.VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) response = self.client.post( f"/api/videos/{video.id}/stop-live/", @@ -249,7 +249,7 @@ def test_api_video_instructor_stop_live(self): }, live_type=RAW, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # stop a live video, now = datetime(2021, 11, 16, tzinfo=baseTimezone.utc) @@ -334,7 +334,7 @@ def test_api_instructor_stop_non_live_video(self): id="27a23f52-3379-46a2-94fa-697b59cfe3c7", upload_state=random.choice([s[0] for s in STATE_CHOICES]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # stop a live video, with mock.patch.object( @@ -359,7 +359,7 @@ def test_api_instructor_stop_non_running_live(self): live_state=random.choice([s[0] for s in LIVE_CHOICES if s[0] != "running"]), live_type=RAW, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # stop a live video, with mock.patch.object( @@ -410,7 +410,7 @@ def test_api_video_instructor_stop_live_recording_slice(self): }, live_type=RAW, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # stop a live video, with mock.patch.object(timezone, "now", return_value=stop), mock.patch.object( diff --git a/src/backend/marsha/core/tests/api/video/test_update.py b/src/backend/marsha/core/tests/api/video/test_update.py index f7be09af35..2fd877a773 100644 --- a/src/backend/marsha/core/tests/api/video/test_update.py +++ b/src/backend/marsha/core/tests/api/video/test_update.py @@ -52,7 +52,7 @@ def test_api_video_update_detail_anonymous(self): def test_api_video_update_detail_student(self): """Student users should not be allowed to update a video through the API.""" video = factories.VideoFactory(title="my title") - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = {"title": "my new title"} response = self.client.put( @@ -72,7 +72,7 @@ def test_api_video_update_detail_student(self): def test_api_video_update_detail_token_user_title(self): """Token users should be able to update the title of their video through the API.""" video = factories.VideoFactory(title="my title") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"title": "my new title"} response = self.client.put( f"/api/videos/{video.id}/", @@ -87,7 +87,7 @@ def test_api_video_update_detail_token_user_title(self): def test_api_video_update_detail_token_user_title_null(self): """Token users can not set a null title.""" video = factories.VideoFactory(title="my title") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"title": None} response = self.client.put( f"/api/videos/{video.id}/", @@ -103,7 +103,7 @@ def test_api_video_update_detail_token_user_title_null(self): def test_api_video_update_detail_token_user_title_empty(self): """Token users can not set an empty title.""" video = factories.VideoFactory(title="my title") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"title": " "} response = self.client.put( f"/api/videos/{video.id}/", @@ -128,7 +128,7 @@ def test_api_video_update_detail_token_scheduled_date_future(self): live_state=IDLE, live_type=JITSI, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # set microseconds to 0 to compare date surely as serializer truncate them starting_at = (timezone.now() + timedelta(hours=1)).replace(microsecond=0) data = { @@ -220,7 +220,7 @@ def test_api_video_update_detail_token_scheduled_date_future_live_session(self): should_send_reminders=True, video=video, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # we only change the title response = self.client.put( f"/api/videos/{video.id}/", @@ -292,7 +292,7 @@ def test_api_video_update_detail_token_scheduled_date_past(self): title="my title", ) self.assertTrue(video.is_scheduled) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # try to set a date in the past # set microseconds to 0 to compare date surely as serializer truncate them @@ -330,7 +330,7 @@ def test_api_video_update_detail_token_scheduled_date_to_none(self): live_state=IDLE, live_type=JITSI, starting_at=starting_at, title="my title" ) self.assertTrue(video.is_scheduled) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"title": "title required", "starting_at": None} response = self.client.put( @@ -352,7 +352,7 @@ def test_api_video_update_detail_token_scheduled_with_previous_starting_at_alrea video = factories.VideoFactory( live_state=IDLE, live_type=RAW, starting_at=intial_starting_at ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # now is set to after initial starting_at now = intial_starting_at + timedelta(days=10) with mock.patch.object(timezone, "now", return_value=now): @@ -382,7 +382,7 @@ def test_api_video_update_detail_token_scheduled_with_previous_starting_at_alrea def test_api_video_update_detail_token_user_description(self): """Token users should be able to update the description of their video through the API.""" video = factories.VideoFactory(description="my description") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/", @@ -403,7 +403,7 @@ def test_api_video_update_detail_token_user_description(self): def test_api_video_update_detail_token_user_uploaded_on(self): """Token users trying to update "uploaded_on" through the API should be ignored.""" video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/", @@ -438,7 +438,7 @@ def test_api_video_update_detail_token_user_upload_state(self): uploaded_on="2019-09-24 07:24:40+00", resolutions=[240, 480, 720], ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/", @@ -540,7 +540,7 @@ def test_api_video_update_detail_token_user_join_mode(self): description="my description", join_mode=APPROVAL, ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/", @@ -564,7 +564,7 @@ def test_api_video_instructor_update_video_in_read_only(self): """An instructor with read_only set to true should not be able to update the video.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -594,7 +594,7 @@ def test_api_video_patch_video_anonymous(self): def test_api_video_patch_video_student(self): """Student users should not be allowed to patch a video through the API.""" video = factories.VideoFactory(title="my title") - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = {"title": "my new title"} response = self.client.patch( @@ -615,7 +615,7 @@ def test_api_video_instructor_patch_video_in_read_only(self): """An instructor with read_only set to true should not be able to patch the video.""" video = factories.VideoFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, permissions__can_update=False, ) @@ -635,7 +635,7 @@ def test_api_video_patch_detail_token_user_stamp(self): this field can only be updated by AWS via the separate update-state API endpoint. """ video = factories.VideoFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) self.assertIsNone(video.uploaded_on) data = {"active_stamp": "1533686400"} @@ -655,7 +655,7 @@ def test_api_video_update_detail_token_user_id(self): """Token users trying to update the ID of a video they own should be ignored.""" video = factories.VideoFactory() original_id = video.id - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) response = self.client.get( f"/api/videos/{video.id}/", @@ -678,7 +678,7 @@ def test_api_video_update_detail_token_user_other_video(self): """Token users should not be allowed to update another video through the API.""" video_token = factories.VideoFactory() video_update = factories.VideoFactory(title="my title") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video_token) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video_token.playlist) data = {"title": "my new title"} response = self.client.put( @@ -695,7 +695,7 @@ def test_api_video_update_detail_token_user_other_video(self): def test_api_video_patch_detail_token_user_description(self): """Token users should be able to patch fields on their video through the API.""" video = factories.VideoFactory(description="my description") - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"description": "my new description"} @@ -713,7 +713,7 @@ def test_api_video_patch_detail_token_user_is_public(self): """Instructors and administrators should be able to patch the public flag of their video through the API.""" video = factories.VideoFactory(is_public=False) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"is_public": True} response = self.client.patch( f"/api/videos/{video.id}/", @@ -731,7 +731,7 @@ def test_api_video_patch_detail_token_user_allow_recording(self): video = factories.VideoFactory() self.assertTrue(video.allow_recording) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"allow_recording": False} response = self.client.patch( f"/api/videos/{video.id}/", @@ -749,7 +749,7 @@ def test_api_video_patch_detail_token_user_tags(self): video = factories.VideoFactory() self.assertEqual(video.tags, []) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"tags": ["foo", "bar"]} response = self.client.patch( f"/api/videos/{video.id}/", @@ -767,7 +767,7 @@ def test_api_video_patch_detail_token_user_license(self): video = factories.VideoFactory() self.assertIsNone(video.license) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"license": CC_BY_SA} response = self.client.patch( f"/api/videos/{video.id}/", @@ -785,7 +785,7 @@ def test_api_video_patch_detail_token_user_estimated_duration(self): video = factories.VideoFactory() self.assertIsNone(video.estimated_duration) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) estimated_duration = timedelta(seconds=2100) data = {"estimated_duration": estimated_duration} response = self.client.patch( @@ -804,7 +804,7 @@ def test_api_video_patch_detail_token_user_estimated_duration_negative(self): video = factories.VideoFactory() self.assertIsNone(video.estimated_duration) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # from -7 days to -1 second estimated_duration = timedelta(seconds=random.randint(-604800, -1)) data = {"estimated_duration": estimated_duration} @@ -833,7 +833,7 @@ def test_api_video_patch_detail_token_user_estimated_duration_negative_one_secon video = factories.VideoFactory() self.assertIsNone(video.estimated_duration) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) estimated_duration = timedelta(seconds=-1) data = {"estimated_duration": estimated_duration} @@ -861,7 +861,7 @@ def test_api_video_patch_detail_token_user_has_chat(self): video = factories.VideoFactory() self.assertTrue(video.has_chat) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"has_chat": False} response = self.client.patch( f"/api/videos/{video.id}/", @@ -879,7 +879,7 @@ def test_api_video_patch_detail_token_user_has_live_media(self): video = factories.VideoFactory() self.assertTrue(video.has_live_media) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) data = {"has_live_media": False} response = self.client.patch( f"/api/videos/{video.id}/", @@ -928,7 +928,7 @@ def test_api_video_patch_by_instructor_scheduling_date_future(self): # starting_at is None there is no event scheduled self.assertFalse(video.is_scheduled) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # starting_at gets updated to a date in the future # set microseconds to 0 to compare date surely as serializer truncate them @@ -991,7 +991,7 @@ def test_api_patch_video_with_previous_starting_at_already_past(self): video = factories.VideoFactory( live_state=IDLE, live_type=RAW, starting_at=intial_starting_at ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # now is set after video.starting_at now = intial_starting_at + timedelta(days=10) with mock.patch.object(timezone, "now", return_value=now): @@ -1028,7 +1028,7 @@ def test_api_patch_video_with_live_state_set(self): ) self.assertFalse(video.is_scheduled) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) starting_at = timezone.now() + timedelta(days=10) response = self.client.patch( @@ -1221,7 +1221,7 @@ def test_api_update_video_with_live_state_set(self): ) self.assertFalse(video.is_scheduled) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) starting_at = timezone.now() + timedelta(hours=1) response = self.client.put( f"/api/videos/{video.id}/", @@ -1259,7 +1259,7 @@ def test_api_update_video_with_starting_at_past(self): self.assertTrue(video.starting_at > timezone.now()) self.assertTrue(video.is_scheduled) self.assertEqual(video.live_state, IDLE) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) # Mock now to the future to check video gets set to not scheduled future = timezone.now() + timedelta(hours=1) with mock.patch.object(timezone, "now", return_value=future): diff --git a/src/backend/marsha/core/tests/management_commands/test_send_reminders.py b/src/backend/marsha/core/tests/management_commands/test_send_reminders.py index 1a10e1de0c..8af0693ec2 100644 --- a/src/backend/marsha/core/tests/management_commands/test_send_reminders.py +++ b/src/backend/marsha/core/tests/management_commands/test_send_reminders.py @@ -1201,7 +1201,7 @@ def test_scenario_video_date_has_changed(self): video = VideoFactory( title="my title", live_state=IDLE, live_type=JITSI, starting_at=None ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=video.playlist) self.assertFalse(video.is_scheduled) livesession = LiveSessionFactory( anonymous_id=uuid.uuid4(), diff --git a/src/backend/marsha/core/tests/simple_jwt/test_factories.py b/src/backend/marsha/core/tests/simple_jwt/test_factories.py index b8455f8611..889f1172a1 100644 --- a/src/backend/marsha/core/tests/simple_jwt/test_factories.py +++ b/src/backend/marsha/core/tests/simple_jwt/test_factories.py @@ -33,6 +33,7 @@ def test_payload_keys(self): "lis_person_contact_email_primary": "jane@test-mooc.fr", }, ) + playlist_id = uuid.uuid4() request = RequestFactory().post( url, @@ -43,9 +44,9 @@ def test_payload_keys(self): self.assertTrue(lti.verify()) session_id = uuid.uuid4() - jwt = ResourceAccessToken.for_lti(lti, {}, session_id) + jwt = ResourceAccessToken.for_lti(lti, {}, session_id, playlist_id) - jwt_from_factory = LTIResourceAccessTokenFactory() + jwt_from_factory = LTIResourceAccessTokenFactory(playlist_id=playlist_id) self.assertSetEqual( set(jwt.payload.keys()), diff --git a/src/backend/marsha/core/tests/simple_jwt/test_tokens.py b/src/backend/marsha/core/tests/simple_jwt/test_tokens.py index 3774978d3b..aab360cd93 100644 --- a/src/backend/marsha/core/tests/simple_jwt/test_tokens.py +++ b/src/backend/marsha/core/tests/simple_jwt/test_tokens.py @@ -141,8 +141,14 @@ def test_for_lti(self): session_id = str(uuid.uuid4()) resource_id = str(uuid.uuid4()) lti, passport = self.make_lti_instance(resource_id=resource_id) + playlist_id = str(uuid.uuid4()) - refresh_token = ResourceRefreshToken.for_lti(lti, permissions, session_id) + refresh_token = ResourceRefreshToken.for_lti( + lti, + permissions, + session_id, + playlist_id=playlist_id, + ) token = refresh_token.access_token refresh_token.verify() # Must not raise @@ -150,7 +156,7 @@ def test_for_lti(self): self.assertEqual(refresh_token.payload["access_token_type"], "resource_access") self.assertEqual(token.payload["token_type"], "resource_access") self.assertEqual(token.payload["session_id"], session_id) - self.assertEqual(token.payload["resource_id"], resource_id) + self.assertEqual(token.payload["resource_id"], playlist_id) self.assertEqual(token.payload["roles"], [INSTRUCTOR]) self.assertEqual(token.payload["locale"], "en_US") self.assertDictEqual(token.payload["permissions"], permissions) @@ -189,7 +195,7 @@ def test_for_lti_with_playlist(self): self.assertEqual(refresh_token.payload["access_token_type"], "resource_access") self.assertEqual(token.payload["token_type"], "resource_access") self.assertEqual(token.payload["session_id"], session_id) - self.assertEqual(token.payload["resource_id"], resource_id) + self.assertEqual(token.payload["resource_id"], playlist_id) self.assertEqual(token.payload["roles"], [INSTRUCTOR]) self.assertEqual(token.payload["locale"], "en_US") self.assertDictEqual(token.payload["permissions"], permissions) @@ -208,31 +214,6 @@ def test_for_lti_with_playlist(self): ) self.assertEqual(token.payload["playlist_id"], playlist_id) - def test_for_lti_with_playlist_for_student(self): - """Test JWT initialization from `for_lti` method with a playlist but student role.""" - permissions = {"can_access_dashboard": False, "can_update": False} - session_id = str(uuid.uuid4()) - resource_id = str(uuid.uuid4()) - lti, _passport = self.make_lti_instance(resource_id=resource_id, role=STUDENT) - - playlist_id = str(uuid.uuid4()) - - with self.assertRaises(AssertionError): - ResourceAccessToken.for_lti( - lti, - permissions, - session_id, - playlist_id=playlist_id, - ) - - with self.assertRaises(AssertionError): - ResourceRefreshToken.for_lti( - lti, - permissions, - session_id, - playlist_id=playlist_id, - ) - def test_for_live_session_anonymous(self): """Test JWT initialization from `for_live_session` method with public session.""" permissions = {"can_access_dashboard": False, "can_update": False} @@ -255,7 +236,9 @@ def test_for_live_session_anonymous(self): self.assertDictEqual(token.payload["permissions"], permissions) self.assertFalse(token.payload["maintenance"]) # settings.MAINTENANCE_MODE - self.assertEqual(token.payload["resource_id"], str(live_session.video.pk)) + self.assertEqual( + token.payload["resource_id"], str(live_session.video.playlist.pk) + ) self.assertDictEqual( token.payload["user"], { @@ -297,7 +280,7 @@ def test_for_live_session_lti(self): token.payload["consumer_site"], str(video.playlist.consumer_site.pk) ) self.assertEqual(token.payload["context_id"], str(video.playlist.lti_id)) - self.assertEqual(token.payload["resource_id"], str(video.pk)) + self.assertEqual(token.payload["resource_id"], str(video.playlist.pk)) self.assertDictEqual( token.payload["user"], { diff --git a/src/backend/marsha/core/tests/test_api_document.py b/src/backend/marsha/core/tests/test_api_document.py index dfeb1ecb18..8d014d9d72 100644 --- a/src/backend/marsha/core/tests/test_api_document.py +++ b/src/backend/marsha/core/tests/test_api_document.py @@ -42,7 +42,7 @@ def test_api_document_fetch_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) @@ -69,7 +69,7 @@ def test_api_document_fetch_instructor(self): title="bar baz", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.get( f"/api/documents/{document.id}/", @@ -104,7 +104,7 @@ def test_api_document_fetch_instructor_read_only(self): document = DocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=False, ) @@ -128,7 +128,7 @@ def test_api_document_fetch_list_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) @@ -141,7 +141,7 @@ def test_api_fetch_list_instructor(self): """An instrustor should not be able to fetch a document list.""" document = DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.get( "/api/documents/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -158,7 +158,7 @@ def test_api_document_create_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) @@ -183,7 +183,7 @@ def test_api_document_create_instructor(self): """An instrustor should not be able to create a document.""" document = DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.post( "/api/documents/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" @@ -248,7 +248,7 @@ def test_api_document_delete_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) @@ -262,7 +262,7 @@ def test_api_document_delete_instructor(self): """An instructor should be able to delete a document.""" document = DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) response = self.client.delete( f"/api/documents/{document.id}/", @@ -281,7 +281,7 @@ def test_api_document_update_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) data = {"title": "new title"} @@ -299,7 +299,7 @@ def test_api_document_update_instructor_read_only(self): document = DocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=False, ) data = {"title": "new title"} @@ -316,7 +316,7 @@ def test_api_document_update_instructor(self): """An instructor should be able to update a document.""" document = DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) data = {"title": "new title"} response = self.client.put( @@ -334,7 +334,7 @@ def test_api_document_update_title_with_extension(self): """Serializer should remove the extension from the document title (if any).""" document = DocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) data = {"title": "new title.pdf"} response = self.client.put( @@ -359,7 +359,7 @@ def test_api_document_initiate_upload_student(self): document = DocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=True, ) @@ -374,7 +374,7 @@ def test_api_document_initiate_upload_instructor_read_only(self): document = DocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=document, + resource=document.playlist, permissions__can_update=False, ) @@ -391,7 +391,7 @@ def test_api_document_initiate_upload_instructor(self): upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -443,7 +443,7 @@ def test_api_document_initiate_upload_file_without_extension(self): upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -495,7 +495,7 @@ def test_api_document_initiate_upload_file_without_mimetype(self): upload_state=random.choice(["ready", "error"]), ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=document) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=document.playlist) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( diff --git a/src/backend/marsha/core/tests/test_api_playlist_portability.py b/src/backend/marsha/core/tests/test_api_playlist_portability.py index 537f6d9583..5e608fd613 100644 --- a/src/backend/marsha/core/tests/test_api_playlist_portability.py +++ b/src/backend/marsha/core/tests/test_api_playlist_portability.py @@ -12,9 +12,9 @@ class PlaylistPortabilityAPITest(TestCase): """Test the API for playlist portability.""" - def _jwt_token(self, video): - """Build JWT token for a video with admin or instructor role.""" - jwt_token = InstructorOrAdminLtiTokenFactory(resource=video) + def _jwt_token(self, playlist): + """Build JWT token for a playlist with admin or instructor role.""" + jwt_token = InstructorOrAdminLtiTokenFactory(resource=playlist) return jwt_token def _patch_video(self, video, params): @@ -22,7 +22,7 @@ def _patch_video(self, video, params): return self.client.patch( f"/api/playlists/{video.playlist_id}/", json.dumps(params), - HTTP_AUTHORIZATION=f"Bearer {self._jwt_token(video)}", + HTTP_AUTHORIZATION=f"Bearer {self._jwt_token(video.playlist)}", content_type="application/json", ) diff --git a/src/backend/marsha/core/tests/test_api_xapi_statement.py b/src/backend/marsha/core/tests/test_api_xapi_statement.py index c8fc46c7fb..2cafae252f 100644 --- a/src/backend/marsha/core/tests/test_api_xapi_statement.py +++ b/src/backend/marsha/core/tests/test_api_xapi_statement.py @@ -29,7 +29,7 @@ def test_xapi_statement_api_with_anonymous_user(self): def test_xapi_statement_with_no_lrs_configured(self): """If no LRS configured a 200 status code should be returned.""" video = VideoFactory() - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = { "verb": { @@ -56,7 +56,7 @@ def test_xapi_statement_api_with_invalid_payload(self): playlist__consumer_site__lrs_url="http://lrs.com/data/xAPI", playlist__consumer_site__lrs_auth_token="Basic ThisIsABasicAuth", ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = {"foo": "bar"} @@ -107,7 +107,7 @@ def test_xapi_statement_with_request_error_to_lrs(self, xapi_send_mock): playlist__consumer_site__lrs_url="http://lrs.com/data/xAPI", playlist__consumer_site__lrs_auth_token="Basic ThisIsABasicAuth", ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = { "verb": { @@ -144,7 +144,7 @@ def test_xapi_statement_with_request_to_lrs_successful(self): playlist__consumer_site__lrs_url="http://lrs.com/data/xAPI", playlist__consumer_site__lrs_auth_token="Basic ThisIsABasicAuth", ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) data = { "verb": { @@ -172,7 +172,7 @@ def test_xapi_statement_with_missing_user(self): playlist__consumer_site__lrs_url="http://lrs.com/data/xAPI", playlist__consumer_site__lrs_auth_token="Basic ThisIsABasicAuth", ) - jwt_token = StudentLtiTokenFactory(resource=video) + jwt_token = StudentLtiTokenFactory(resource=video.playlist) del jwt_token.payload["user"] data = { @@ -202,7 +202,9 @@ def test_xapi_statement_document_resource(self): playlist__consumer_site__lrs_auth_token="Basic ThisIsABasicAuth", ) session_id = str(uuid.uuid4()) - jwt_token = StudentLtiTokenFactory(resource=document, session_id=session_id) + jwt_token = StudentLtiTokenFactory( + resource=document.playlist, session_id=session_id + ) data = { "verb": { diff --git a/src/backend/marsha/core/tests/views/test_lti_cache.py b/src/backend/marsha/core/tests/views/test_lti_cache.py index c65bca8667..593757f4d4 100644 --- a/src/backend/marsha/core/tests/views/test_lti_cache.py +++ b/src/backend/marsha/core/tests/views/test_lti_cache.py @@ -222,9 +222,9 @@ def test_views_direct_access_public_resource(self): elapsed, resource_origin = self._fetch_lti_request(url) self.assertEqual(resource_origin["id"], str(video.id)) - self.assertLess(elapsed, 0.1) + self.assertLess(elapsed, 0.2) - with self.assertNumQueries(2): + with self.assertNumQueries(3): elapsed, resource_origin = self._fetch_lti_request(url) self.assertEqual(resource_origin["id"], str(video.id)) - self.assertLess(elapsed, 0.1) + self.assertLess(elapsed, 0.2) diff --git a/src/backend/marsha/core/tests/views/test_lti_document.py b/src/backend/marsha/core/tests/views/test_lti_document.py index 418353524a..36025d3ff7 100644 --- a/src/backend/marsha/core/tests/views/test_lti_document.py +++ b/src/backend/marsha/core/tests/views/test_lti_document.py @@ -72,7 +72,7 @@ def test_views_lti_document_instructor_same_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(document.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(document.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -137,7 +137,7 @@ def test_views_lti_document_instructor_other_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(document.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(document.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -198,7 +198,7 @@ def test_views_lti_document_student_with_video( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(document.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(document.playlist.id)) self.assertEqual( jwt_token.payload["user"], { diff --git a/src/backend/marsha/core/tests/views/test_lti_video.py b/src/backend/marsha/core/tests/views/test_lti_video.py index 5106cc65f8..59b4df871c 100644 --- a/src/backend/marsha/core/tests/views/test_lti_video.py +++ b/src/backend/marsha/core/tests/views/test_lti_video.py @@ -93,7 +93,7 @@ def test_views_lti_video_post_instructor(self, mock_get_consumer_site, mock_veri jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise self.assertEqual(context.get("frontend_home_url"), "https://marsha.education") - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual(jwt_token.payload["context_id"], data["context_id"]) self.assertEqual( jwt_token.payload["consumer_site"], str(passport.consumer_site.id) @@ -249,7 +249,7 @@ def test_views_lti_video_instructor_live_mode_on( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -430,7 +430,7 @@ def test_views_lti_video_instructor_live_mode_and_chat_on( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -651,7 +651,7 @@ def test_views_lti_video_student_live_mode_on( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -808,7 +808,7 @@ def test_views_lti_video_post_administrator( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -1056,7 +1056,7 @@ def test_views_lti_video_restricted_resolutions_list( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -1196,7 +1196,7 @@ def test_views_lti_video_harvested_live_state_student( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -1310,7 +1310,7 @@ def test_views_lti_video_harvested_live_state_instructor( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -1452,7 +1452,7 @@ def test_views_lti_video_post_student_with_video( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -1596,7 +1596,7 @@ def test_views_lti_video_without_user_id_parameter( context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual(jwt_token.payload["context_id"], data["context_id"]) self.assertEqual( jwt_token.payload["consumer_site"], str(passport.consumer_site.id) @@ -1730,7 +1730,7 @@ def test_views_lti_video_with_timed_text(self, mock_get_consumer_site, mock_veri context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) ResourceRefreshToken(context.get("refresh_token")) # Must not raise - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual(jwt_token.payload["context_id"], data["context_id"]) self.assertEqual( jwt_token.payload["consumer_site"], str(passport.consumer_site.id) diff --git a/src/backend/marsha/core/tests/views/test_public_video.py b/src/backend/marsha/core/tests/views/test_public_video.py index 3a52ec82a8..e467c4af31 100644 --- a/src/backend/marsha/core/tests/views/test_public_video.py +++ b/src/backend/marsha/core/tests/views/test_public_video.py @@ -405,7 +405,7 @@ def test_video_accessible_from_mail(self): "username": livesession.username, }, ) - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual(jwt_token.payload["locale"], "fr_FR") self.assertEqual(jwt_token.payload["context_id"], video.playlist.lti_id) self.assertEqual( @@ -455,7 +455,7 @@ def test_video_ressource_public_accessible_from_mail(self): }, ) self.assertEqual(jwt_token.payload["roles"], ["none"]) - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual(jwt_token.payload["locale"], "en_US") self.assertIsNone(jwt_token.payload.get("context_id")) self.assertIsNone(jwt_token.payload.get("consumer_site")) diff --git a/src/backend/marsha/core/tests/views/test_site.py b/src/backend/marsha/core/tests/views/test_site.py index 9890994882..c5a13c2edc 100644 --- a/src/backend/marsha/core/tests/views/test_site.py +++ b/src/backend/marsha/core/tests/views/test_site.py @@ -48,6 +48,7 @@ def test_site_publicly_accessible(self): ) @override_switch("site", active=True) + @override_settings(BASE_STATIC_DIR="missing") def test_site_site_template_missing(self): """Test site with missing index file should return a 501.""" response = self.client.get("/") diff --git a/src/backend/marsha/core/views.py b/src/backend/marsha/core/views.py index fa2512b1ef..1da47d1c45 100644 --- a/src/backend/marsha/core/views.py +++ b/src/backend/marsha/core/views.py @@ -539,6 +539,7 @@ def _get_app_data(self): lti=self.lti, permissions=permissions, session_id=session_id, + playlist_id=str(app_data["resource"]["playlist"]["id"]), ) jwt_token = refresh_token.access_token app_data["jwt"] = str(jwt_token) diff --git a/src/backend/marsha/deposit/api.py b/src/backend/marsha/deposit/api.py index 6c011d399a..461982d494 100644 --- a/src/backend/marsha/deposit/api.py +++ b/src/backend/marsha/deposit/api.py @@ -25,6 +25,20 @@ ) +class ObjectFileDepositoryRelatedMixin: + """ + Get the related depository id contained in resource. + + It exposes a function used to get the related depository. + """ + + def get_related_filedepository_id(self): + """Get the related file depository ID from the request.""" + + # The file depository ID in the URL is mandatory. + return self.kwargs.get("filedepository_id") + + class FileDepositoryFilter(django_filters.FilterSet): """Filter for file depository.""" @@ -93,7 +107,7 @@ def get_permissions(self): ] elif self.action in ["retrieve"]: permission_classes = [ - core_permissions.IsTokenResourceRouteObject + core_permissions.IsPlaylistToken & ( core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin @@ -104,7 +118,7 @@ def get_permissions(self): elif self.action in ["update", "partial_update", "destroy"]: permission_classes = [ ( - core_permissions.IsTokenResourceRouteObject + core_permissions.IsPlaylistToken & ( core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin @@ -195,57 +209,14 @@ def lti_select(self, request): } ) - @action( - methods=["get"], - detail=True, - url_path="depositedfiles", - permission_classes=[ - core_permissions.IsTokenInstructor - | core_permissions.IsTokenAdmin - | core_permissions.IsTokenStudent - | IsFileDepositoryPlaylistOrOrganizationAdmin - ], - ) - # pylint: disable=unused-argument - def depositedfiles(self, request, pk=None): - """Get deposited files from a file_depository. - - Calling the endpoint returns a list of deposited files. - - Parameters - ---------- - request : Type[django.http.request.HttpRequest] - The request on the API endpoint - pk : int - The primary key of the file_depository - - Returns - ------- - Type[rest_framework.response.Response] - HttpResponse carrying deposited files as a JSON object. - - """ - file_depository = self.get_object() - queryset = file_depository.deposited_files.all() - if request.resource and any( - x in LTI_ROLES.get(STUDENT) for x in request.resource.roles - ): - queryset = queryset.filter(author_id=request.resource.user.get("id")) - filter_set = DepositedFileFilter(request.query_params, queryset=queryset) - page = self.paginate_queryset(filter_set.qs) - serializer = serializers.DepositedFileSerializer( - page, - many=True, - context={"request": self.request}, - ) - return self.get_paginated_response(serializer.data) - class DepositedFileViewSet( APIViewMixin, ObjectPkMixin, ObjectRelatedMixin, + ObjectFileDepositoryRelatedMixin, mixins.CreateModelMixin, + mixins.ListModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet, @@ -272,7 +243,7 @@ def get_permissions(self): | core_permissions.IsTokenStudent | core_permissions.UserIsAuthenticated ] - elif self.action in ["create", "list", "metadata"]: + elif self.action in ["list", "metadata"]: permission_classes = [ core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin @@ -289,9 +260,44 @@ def get_permissions(self): permission_classes = self.permission_classes return [permission() for permission in permission_classes] + def list(self, request, *args, **kwargs): + """Get a list of deposited files. + + Calling the endpoint returns a list of deposited files. + + Parameters + ---------- + request : Type[django.http.request.HttpRequest] + The request on the API endpoint + + Returns + ------- + Type[rest_framework.response.Response] + HttpResponse carrying deposited files as a JSON object. + + """ + queryset = self.filter_queryset(self.get_queryset()) + if request.resource and any( + x in LTI_ROLES.get(STUDENT) for x in request.resource.roles + ): + queryset = queryset.filter(author_id=request.resource.user.get("id")) + filter_set = DepositedFileFilter(request.query_params, queryset=queryset) + page = self.paginate_queryset(filter_set.qs) + serializer = serializers.DepositedFileSerializer( + page, + many=True, + context={"request": self.request}, + ) + return self.get_paginated_response(serializer.data) + @action(methods=["post"], detail=True, url_path="initiate-upload") # pylint: disable=unused-argument - def initiate_upload(self, request, pk=None): + def initiate_upload( + self, + request, + pk=None, + filedepository_id=None, + ): """Get an upload policy for a deposited file. Calling the endpoint resets the upload state to `pending` and returns an upload policy to diff --git a/src/backend/marsha/deposit/permissions.py b/src/backend/marsha/deposit/permissions.py index d2beba4514..6b1f308802 100644 --- a/src/backend/marsha/deposit/permissions.py +++ b/src/backend/marsha/deposit/permissions.py @@ -1,9 +1,8 @@ """Custom permission classes for the Deposit app.""" -from django.core.exceptions import ObjectDoesNotExist - from rest_framework import permissions from marsha.core import models +from marsha.deposit.models import FileDepository def _is_organization_admin(user_id, file_depository_id): @@ -59,12 +58,10 @@ def has_permission(self, request, view): """ if not request.resource: return False - try: - return ( - str(view.get_related_object().file_depository.id) == request.resource.id - ) - except ObjectDoesNotExist: - return False + + return FileDepository.objects.filter( + pk=view.get_related_filedepository_id(), playlist_id=request.resource.id + ).exists() class IsFileDepositoryPlaylistOrOrganizationAdmin(permissions.BasePermission): @@ -108,10 +105,7 @@ def has_permission(self, request, view): which exists, and if the current user is an admin for the playlist this file depository is a part of or admin of the linked organization. """ - try: - file_depository_id = view.get_related_object().file_depository.id - except (AttributeError, ObjectDoesNotExist): - file_depository_id = request.data.get("file_depository") + file_depository_id = view.get_related_filedepository_id() if not file_depository_id: return False diff --git a/src/backend/marsha/deposit/serializers.py b/src/backend/marsha/deposit/serializers.py index 97dcccbfab..3f0f8b2773 100644 --- a/src/backend/marsha/deposit/serializers.py +++ b/src/backend/marsha/deposit/serializers.py @@ -33,7 +33,7 @@ class Meta: # noqa "filename", "author_name", "id", - "file_depository", + "file_depository_id", "read", "url", "uploaded_on", @@ -42,7 +42,7 @@ class Meta: # noqa ) read_only_fields = ( "id", - "file_depository", + "file_depository_id", "url", "uploaded_on", "upload_state", @@ -52,7 +52,7 @@ class Meta: # noqa # filename = serializers.SerializerMethodField() url = serializers.SerializerMethodField() # Make sure file depository UUID is converted to a string during serialization - file_depository = serializers.PrimaryKeyRelatedField( + file_depository_id = serializers.PrimaryKeyRelatedField( read_only=True, pk_field=serializers.CharField() ) @@ -83,13 +83,10 @@ def create(self, validated_data): """ resource = self.context["request"].resource user = self.context["request"].user - file_depository_id = self.context["request"].data.get("file_depository") + file_depository_id = self.context["view"].get_related_filedepository_id() if not validated_data.get("file_depository_id"): - if resource: - validated_data["file_depository_id"] = resource.id - elif file_depository_id: - validated_data["file_depository_id"] = file_depository_id + validated_data["file_depository_id"] = file_depository_id if resource: validated_data["author_id"] = resource.user.get("id") diff --git a/src/backend/marsha/deposit/tests/api/depositedfiles/test_create.py b/src/backend/marsha/deposit/tests/api/depositedfiles/test_create.py index 3cc25f1a67..ada6fad42b 100644 --- a/src/backend/marsha/deposit/tests/api/depositedfiles/test_create.py +++ b/src/backend/marsha/deposit/tests/api/depositedfiles/test_create.py @@ -42,10 +42,10 @@ def test_api_deposited_file_create_student_with_user_fullname(self): """ file_depository = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=file_depository) + jwt_token = StudentLtiTokenFactory(resource=file_depository.playlist) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( @@ -62,7 +62,7 @@ def test_api_deposited_file_create_student_with_user_fullname(self): response.json(), { "author_name": jwt_token.get("user").get("user_fullname"), - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": "test.pdf", "id": str(DepositedFile.objects.first().id), "read": False, @@ -89,16 +89,18 @@ def test_api_deposited_file_create_student_with_username(self): file_depository = FileDepositoryFactory() jwt_token = StudentLtiTokenFactory( - resource=file_depository, user__user_fullname=None, user__username="student" + resource=file_depository.playlist, + user__user_fullname=None, + user__username="student", ) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", data=json.dumps( { "size": 123, "filename": "test.pdf", - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), } ), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", @@ -121,16 +123,18 @@ def test_api_deposited_file_create_student_without_username(self): file_depository = FileDepositoryFactory() jwt_token = StudentLtiTokenFactory( - resource=file_depository, user__user_fullname=None, user__username=None + resource=file_depository.playlist, + user__user_fullname=None, + user__username=None, ) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", data=json.dumps( { "size": 123, "filename": "test.pdf", - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), } ), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", @@ -153,14 +157,14 @@ def test_api_deposited_file_create_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "size": 123, "filename": "test.pdf", - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), } ), ) @@ -171,7 +175,7 @@ def test_api_deposited_file_create_user_access_token(self): response.json(), { "author_name": organization_access.user.username, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": "test.pdf", "id": str(DepositedFile.objects.first().id), "read": False, @@ -197,14 +201,14 @@ def test_api_deposited_file_create_user_access_token_organization_admin(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "size": 123, "filename": "test.pdf", - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), } ), ) @@ -215,7 +219,7 @@ def test_api_deposited_file_create_user_access_token_organization_admin(self): response.json(), { "author_name": organization_access.user.username, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": "test.pdf", "id": str(DepositedFile.objects.first().id), "read": False, @@ -240,14 +244,14 @@ def test_api_deposited_file_create_user_access_token_playlist_admin(self): jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.post( - "/api/depositedfiles/", + f"/api/filedepositories/{file_depository.id}/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", data=json.dumps( { "size": 123, "filename": "test.pdf", - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), } ), ) @@ -258,7 +262,7 @@ def test_api_deposited_file_create_user_access_token_playlist_admin(self): response.json(), { "author_name": playlist_access.user.username, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": "test.pdf", "id": str(DepositedFile.objects.first().id), "read": False, diff --git a/src/backend/marsha/deposit/tests/api/depositedfiles/test_delete.py b/src/backend/marsha/deposit/tests/api/depositedfiles/test_delete.py index 28bb216aa3..ebb0a2658f 100644 --- a/src/backend/marsha/deposit/tests/api/depositedfiles/test_delete.py +++ b/src/backend/marsha/deposit/tests/api/depositedfiles/test_delete.py @@ -33,7 +33,8 @@ def test_api_deposited_file_delete_student(self): self.assertEqual(DepositedFile.objects.count(), 1) response = self.client.delete( - f"/api/depositedfiles/{deposited_file.id}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -49,7 +50,8 @@ def test_api_deposited_file_delete_instructor(self): self.assertEqual(DepositedFile.objects.count(), 1) response = self.client.delete( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -66,7 +68,8 @@ def test_api_deposited_file_delete_user_access_token(self): self.assertEqual(DepositedFile.objects.count(), 1) response = self.client.delete( - f"/api/depositedfiles/{deposited_file.id}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -82,7 +85,8 @@ def test_api_deposited_file_delete_user_access_token_organization_admin(self): self.assertEqual(DepositedFile.objects.count(), 1) response = self.client.delete( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) @@ -100,7 +104,8 @@ def test_api_deposited_file_delete_user_access_token_playlist_admin(self): self.assertEqual(DepositedFile.objects.count(), 1) response = self.client.delete( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", ) diff --git a/src/backend/marsha/deposit/tests/api/depositedfiles/test_initiate_upload.py b/src/backend/marsha/deposit/tests/api/depositedfiles/test_initiate_upload.py index c1567b04ae..3368264df1 100644 --- a/src/backend/marsha/deposit/tests/api/depositedfiles/test_initiate_upload.py +++ b/src/backend/marsha/deposit/tests/api/depositedfiles/test_initiate_upload.py @@ -49,7 +49,9 @@ def test_api_deposited_file_initiate_upload_student(self): upload_state=random.choice(["ready", "error"]), file_depository__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = StudentLtiTokenFactory(resource=deposited_file.file_depository) + jwt_token = StudentLtiTokenFactory( + resource=deposited_file.file_depository.playlist + ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -57,7 +59,8 @@ def test_api_deposited_file_initiate_upload_student(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -105,7 +108,9 @@ def test_api_deposited_file_initiate_upload_file_without_size(self): upload_state=random.choice(["ready", "error"]), file_depository__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = StudentLtiTokenFactory(resource=deposited_file.file_depository) + jwt_token = StudentLtiTokenFactory( + resource=deposited_file.file_depository.playlist + ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -113,7 +118,8 @@ def test_api_deposited_file_initiate_upload_file_without_size(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf"}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -132,7 +138,9 @@ def test_api_deposited_file_initiate_upload_file_too_large(self): upload_state=random.choice(["ready", "error"]), file_depository__id="ed08da34-7447-4141-96ff-5740315d7b99", ) - jwt_token = StudentLtiTokenFactory(resource=deposited_file.file_depository) + jwt_token = StudentLtiTokenFactory( + resource=deposited_file.file_depository.playlist + ) now = datetime(2018, 8, 8, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -140,7 +148,8 @@ def test_api_deposited_file_initiate_upload_file_too_large(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 100}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -174,7 +183,8 @@ def test_api_deposited_file_initiate_upload_user_access_token(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -239,7 +249,8 @@ def test_api_deposited_file_initiate_upload_user_access_token_organization_admin ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -301,7 +312,8 @@ def test_api_deposited_file_initiate_upload_user_access_token_playlist_admin(sel ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/depositedfiles/{deposited_file.id}/initiate-upload/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/initiate-upload/", {"filename": "foo.pdf", "mimetype": "application/pdf", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", diff --git a/src/backend/marsha/deposit/tests/api/depositedfiles/test_update.py b/src/backend/marsha/deposit/tests/api/depositedfiles/test_update.py index 3ba1e65021..1571bb8e36 100644 --- a/src/backend/marsha/deposit/tests/api/depositedfiles/test_update.py +++ b/src/backend/marsha/deposit/tests/api/depositedfiles/test_update.py @@ -42,7 +42,8 @@ def test_api_deposited_file_update_student(self): data = {"read": True} response = self.client.patch( - f"/api/depositedfiles/{deposited_file.id}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/", json.dumps(data), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -58,7 +59,8 @@ def test_api_deposited_file_update_instructor(self): data = {"read": True} response = self.client.patch( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -68,7 +70,7 @@ def test_api_deposited_file_update_instructor(self): response.json(), { "author_name": deposited_file.author_name, - "file_depository": str(deposited_file.file_depository.id), + "file_depository_id": str(deposited_file.file_depository.id), "filename": deposited_file.filename, "id": str(deposited_file.id), "read": True, @@ -91,7 +93,8 @@ def test_api_deposited_file_update_user_access_token(self): data = {"read": True} response = self.client.patch( - f"/api/depositedfiles/{deposited_file.id}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id}/", json.dumps(data), HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -107,7 +110,8 @@ def test_api_deposited_file_update_user_access_token_organization_admin(self): data = {"read": True} response = self.client.patch( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -117,7 +121,7 @@ def test_api_deposited_file_update_user_access_token_organization_admin(self): response.json(), { "author_name": deposited_file.author_name, - "file_depository": str(deposited_file.file_depository.id), + "file_depository_id": str(deposited_file.file_depository.id), "filename": deposited_file.filename, "id": str(deposited_file.id), "read": True, @@ -141,7 +145,8 @@ def test_api_deposited_file_update_user_access_token_playlist_admin(self): data = {"read": True} response = self.client.patch( - f"/api/depositedfiles/{deposited_file.id!s}/", + f"/api/filedepositories/{deposited_file.file_depository.id}" + f"/depositedfiles/{deposited_file.id!s}/", data, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", content_type="application/json", @@ -151,7 +156,7 @@ def test_api_deposited_file_update_user_access_token_playlist_admin(self): response.json(), { "author_name": deposited_file.author_name, - "file_depository": str(deposited_file.file_depository.id), + "file_depository_id": str(deposited_file.file_depository.id), "filename": deposited_file.filename, "id": str(deposited_file.id), "read": True, diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_create.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_create.py index 972c559237..6d1a299402 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_create.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_create.py @@ -66,7 +66,7 @@ def test_api_file_depository_create_student_with_playlist_token(self): def test_api_file_depository_create_instructor(self): """An instructor without playlist token should not be able to create a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.post( "/api/filedepositories/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_delete.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_delete.py index c2ba1ec5b4..da261bf98f 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_delete.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_delete.py @@ -54,7 +54,7 @@ def test_api_file_depository_delete_user_logged_in(self): def test_api_file_depository_delete_student(self): """A student user should not be able to delete a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=file_depository) + jwt_token = StudentLtiTokenFactory(resource=file_depository.playlist) self.assertEqual(FileDepository.objects.count(), 1) response = self.client.delete( @@ -69,7 +69,7 @@ def test_api_file_depository_delete_instructor_read_only(self): """An instructor should not be able to delete a file_depository in read_only.""" file_depository = FileDepositoryFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=file_depository, + resource=file_depository.playlist, permissions__can_update=False, ) @@ -85,7 +85,7 @@ def test_api_file_depository_delete_instructor_read_only(self): def test_api_file_depository_delete_instructor(self): """An instructor should be able to delete a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) self.assertEqual(FileDepository.objects.count(), 1) response = self.client.delete( diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_depositedfiles.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_depositedfiles.py index 04cef6305a..ccd9ddc59b 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_depositedfiles.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_depositedfiles.py @@ -51,7 +51,7 @@ def test_api_file_depository_list_deposited_files_student(self): DepositedFileFactory.create_batch(3, file_depository=file_depository) owned_deposited_file = DepositedFileFactory(file_depository=file_depository) jwt_token = StudentLtiTokenFactory( - resource=file_depository, + resource=file_depository.playlist, permissions__can_update=True, user__id=owned_deposited_file.author_id, user__full_username=owned_deposited_file.author_name, @@ -71,7 +71,7 @@ def test_api_file_depository_list_deposited_files_student(self): "results": [ { "author_name": owned_deposited_file.author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": owned_deposited_file.filename, "id": str(owned_deposited_file.id), "read": False, @@ -90,7 +90,7 @@ def test_api_file_depository_list_deposited_files_instructor(self): deposited_files = DepositedFileFactory.create_batch( 3, file_depository=file_depository ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.get( f"/api/filedepositories/{file_depository.id}/depositedfiles/?limit=2", @@ -107,7 +107,7 @@ def test_api_file_depository_list_deposited_files_instructor(self): "results": [ { "author_name": deposited_files[2].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[2].filename, "id": str(deposited_files[2].id), "read": False, @@ -118,7 +118,7 @@ def test_api_file_depository_list_deposited_files_instructor(self): }, { "author_name": deposited_files[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[1].filename, "id": str(deposited_files[1].id), "read": False, @@ -140,7 +140,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): deposited_files_new = DepositedFileFactory.create_batch( 2, file_depository=file_depository ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.get( f"/api/filedepositories/{file_depository.id}/depositedfiles/?limit=10", @@ -156,7 +156,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): "results": [ { "author_name": deposited_files_new[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_new[1].filename, "id": str(deposited_files_new[1].id), "read": False, @@ -167,7 +167,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): }, { "author_name": deposited_files_new[0].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_new[0].filename, "id": str(deposited_files_new[0].id), "read": False, @@ -178,7 +178,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): }, { "author_name": deposited_files_read[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_read[1].filename, "id": str(deposited_files_read[1].id), "read": True, @@ -189,7 +189,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): }, { "author_name": deposited_files_read[0].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_read[0].filename, "id": str(deposited_files_read[0].id), "read": True, @@ -216,7 +216,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): "results": [ { "author_name": deposited_files_read[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_read[1].filename, "id": str(deposited_files_read[1].id), "read": True, @@ -227,7 +227,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): }, { "author_name": deposited_files_read[0].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_read[0].filename, "id": str(deposited_files_read[0].id), "read": True, @@ -254,7 +254,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): "results": [ { "author_name": deposited_files_new[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_new[1].filename, "id": str(deposited_files_new[1].id), "read": False, @@ -265,7 +265,7 @@ def test_api_file_depository_list_deposited_files_instructor_filtered(self): }, { "author_name": deposited_files_new[0].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files_new[0].filename, "id": str(deposited_files_new[0].id), "read": False, @@ -291,7 +291,7 @@ def test_api_file_depository_list_deposited_files_instructor_signed_urls(self): deposited_files = DepositedFileFactory.create_batch( 3, file_depository=file_depository, uploaded_on=now ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) now = datetime(2021, 11, 30, tzinfo=baseTimezone.utc) with mock.patch.object(timezone, "now", return_value=now), mock.patch( @@ -323,7 +323,7 @@ def test_api_file_depository_list_deposited_files_instructor_signed_urls(self): "results": [ { "author_name": deposited_files[2].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[2].filename, "id": str(deposited_files[2].id), "read": False, @@ -339,7 +339,7 @@ def test_api_file_depository_list_deposited_files_instructor_signed_urls(self): }, { "author_name": deposited_files[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[1].filename, "id": str(deposited_files[1].id), "read": False, @@ -355,7 +355,7 @@ def test_api_file_depository_list_deposited_files_instructor_signed_urls(self): }, { "author_name": deposited_files[0].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[0].filename, "id": str(deposited_files[0].id), "read": False, @@ -415,7 +415,7 @@ def test_api_file_depository_list_deposited_files_user_access_token_organization "results": [ { "author_name": deposited_files[2].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[2].filename, "id": str(deposited_files[2].id), "read": False, @@ -426,7 +426,7 @@ def test_api_file_depository_list_deposited_files_user_access_token_organization }, { "author_name": deposited_files[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[1].filename, "id": str(deposited_files[1].id), "read": False, @@ -466,7 +466,7 @@ def test_api_file_depository_list_deposited_files_user_access_token_playlist_adm "results": [ { "author_name": deposited_files[2].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[2].filename, "id": str(deposited_files[2].id), "read": False, @@ -477,7 +477,7 @@ def test_api_file_depository_list_deposited_files_user_access_token_playlist_adm }, { "author_name": deposited_files[1].author_name, - "file_depository": str(file_depository.id), + "file_depository_id": str(file_depository.id), "filename": deposited_files[1].filename, "id": str(deposited_files[1].id), "read": False, diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_list.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_list.py index f398edb8f2..5c49bf5d99 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_list.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_list.py @@ -43,7 +43,7 @@ def test_api_file_depository_fetch_list_student(self): """A student should not be able to fetch a list of file_depository.""" file_depository = FileDepositoryFactory() jwt_token = StudentLtiTokenFactory( - resource=file_depository, + resource=file_depository.playlist, permissions__can_update=True, ) @@ -55,7 +55,7 @@ def test_api_file_depository_fetch_list_student(self): def test_api_file_depository_fetch_list_instructor(self): """An instructor should not be able to fetch a file_depository list.""" file_depository = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.get( "/api/filedepositories/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_retrieve.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_retrieve.py index b41e45a6b2..69f95ed228 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_retrieve.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_retrieve.py @@ -38,7 +38,7 @@ def setUpClass(cls): def test_api_file_depository_fetch_student(self): """A student should be allowed to fetch a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=file_depository) + jwt_token = StudentLtiTokenFactory(resource=file_depository.playlist) response = self.client.get( f"/api/filedepositories/{file_depository.id!s}/", @@ -68,7 +68,7 @@ def test_api_file_depository_fetch_from_other_file_depository(self): """ file_depository = FileDepositoryFactory() other_file_depository = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=other_file_depository) + jwt_token = StudentLtiTokenFactory(resource=other_file_depository.playlist) response = self.client.get( f"/api/filedepositories/{file_depository.id!s}/", @@ -79,7 +79,7 @@ def test_api_file_depository_fetch_from_other_file_depository(self): def test_api_file_depository_fetch_instructor(self): """An instructor should be able to fetch a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.get( f"/api/filedepositories/{file_depository.id!s}/", diff --git a/src/backend/marsha/deposit/tests/api/filedepositories/test_update.py b/src/backend/marsha/deposit/tests/api/filedepositories/test_update.py index 3b181efdf0..15cf962617 100644 --- a/src/backend/marsha/deposit/tests/api/filedepositories/test_update.py +++ b/src/backend/marsha/deposit/tests/api/filedepositories/test_update.py @@ -55,7 +55,7 @@ def test_api_file_depository_update_user_logged_in(self): def test_api_file_depository_update_student(self): """A student user should not be able to update a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=file_depository) + jwt_token = StudentLtiTokenFactory(resource=file_depository.playlist) data = {"title": "new title"} response = self.client.patch( @@ -70,7 +70,7 @@ def test_api_file_depository_update_instructor_read_only(self): """An instructor should not be able to update a file_depository in read_only.""" file_depository = FileDepositoryFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=file_depository, + resource=file_depository.playlist, permissions__can_update=False, ) data = {"title": "new title"} @@ -86,7 +86,7 @@ def test_api_file_depository_update_instructor_read_only(self): def test_api_file_depository_update_instructor(self): """An instructor should be able to update a file_depository.""" file_depository = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository) + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) data = {"title": "new title", "description": "Hello"} response = self.client.patch( diff --git a/src/backend/marsha/deposit/tests/api/test_options.py b/src/backend/marsha/deposit/tests/api/test_options.py index 964f23521e..e7e111dbe6 100644 --- a/src/backend/marsha/deposit/tests/api/test_options.py +++ b/src/backend/marsha/deposit/tests/api/test_options.py @@ -17,7 +17,10 @@ class DepositedFiletCreateAPITest(TestCase): def test_api_deposited_files_options_anonymous(self): """Anonymous user can't fetch the deposited files options endpoint""" - response = self.client.options("/api/depositedfiles/") + file_depository = FileDepositoryFactory() + response = self.client.options( + f"/api/filedepositories/{file_depository.id}/depositedfiles/" + ) self.assertEqual(response.status_code, 401) @@ -25,10 +28,11 @@ def test_api_deposited_files_options_anonymous(self): def test_api_deposited_files_options_as_student(self): """A student can fetch the deposited files options endpoint""" - deposited_file = FileDepositoryFactory() - jwt_token = StudentLtiTokenFactory(resource=deposited_file) + file_depository = FileDepositoryFactory() + jwt_token = StudentLtiTokenFactory(resource=file_depository.playlist) response = self.client.options( - "/api/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/filedepositories/{file_depository.id}/depositedfiles/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["upload_max_size_bytes"], 10) @@ -37,11 +41,12 @@ def test_api_deposited_files_options_as_student(self): def test_api_deposited_files_options_instructor(self): """An instructor can fetch the deposited files options endpoint""" - classroom = FileDepositoryFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=classroom) + file_depository = FileDepositoryFactory() + jwt_token = InstructorOrAdminLtiTokenFactory(resource=file_depository.playlist) response = self.client.options( - "/api/depositedfiles/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/filedepositories/{file_depository.id}/depositedfiles/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 200) diff --git a/src/backend/marsha/deposit/tests/test_views_lti.py b/src/backend/marsha/deposit/tests/test_views_lti.py index db8f59f42a..2995fa2159 100644 --- a/src/backend/marsha/deposit/tests/test_views_lti.py +++ b/src/backend/marsha/deposit/tests/test_views_lti.py @@ -219,7 +219,9 @@ def test_views_lti_file_depository_instructor_same_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(file_depository.id)) + self.assertEqual( + jwt_token.payload["resource_id"], str(file_depository.playlist.id) + ) self.assertEqual( jwt_token.payload["user"], { diff --git a/src/backend/marsha/deposit/urls.py b/src/backend/marsha/deposit/urls.py index 21ef54c2ea..afe1bef963 100644 --- a/src/backend/marsha/deposit/urls.py +++ b/src/backend/marsha/deposit/urls.py @@ -12,7 +12,11 @@ router = DefaultRouter() router.register("filedepositories", FileDepositoryViewSet, basename="file_depository") -router.register("depositedfiles", DepositedFileViewSet, basename="deposited_file") + +filedepository_related_router = DefaultRouter() +filedepository_related_router.register( + "depositedfiles", DepositedFileViewSet, basename="deposited_file" +) urlpatterns = [ path( @@ -28,4 +32,8 @@ name="file_depository_lti_view", ), path("api/", include(router.urls)), + path( + "api/filedepositories//", + include(filedepository_related_router.urls), + ), ] diff --git a/src/backend/marsha/development/tests/test_views_lti_development.py b/src/backend/marsha/development/tests/test_views_lti_development.py index 3bbbc44007..fb7a486875 100644 --- a/src/backend/marsha/development/tests/test_views_lti_development.py +++ b/src/backend/marsha/development/tests/test_views_lti_development.py @@ -67,7 +67,7 @@ def test_views_lti_development_post_bypass_lti_student(self): context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -121,7 +121,7 @@ def test_views_lti_development_post_bypass_lti_instructor(self): context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { @@ -214,7 +214,7 @@ def test_views_lti_development_post_bypass_lti_instructor_no_video(self): context = json.loads(unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) video = Video.objects.get() - self.assertEqual(jwt_token.payload["resource_id"], str(video.id)) + self.assertEqual(jwt_token.payload["resource_id"], str(video.playlist.id)) self.assertEqual( jwt_token.payload["user"], { diff --git a/src/backend/marsha/markdown/api.py b/src/backend/marsha/markdown/api.py index 4eca605f91..733249aac6 100644 --- a/src/backend/marsha/markdown/api.py +++ b/src/backend/marsha/markdown/api.py @@ -23,6 +23,22 @@ from .utils.converter import LatexConversionException, render_latex_to_image +class ObjectMarkdownDocumentRelatedMixin: + """ + Get the related markdown document id contained in resource. + + It exposes a function used to get the related markdown document. + It is also useful to avoid URL crafting (when the url markdown_document_id doesn't + match token resource markdown document id). + """ + + def get_related_markdown_document_id(self): + """Get the related markdown document ID from the request.""" + + # The video ID in the URL is mandatory. + return self.kwargs.get("markdown_document_id") + + class MarkdownDocumentFilter(django_filters.FilterSet): """Filter for file depository.""" @@ -57,7 +73,7 @@ class MarkdownDocumentViewSet( permission_classes = [ ( - core_permissions.IsTokenResourceRouteObject + core_permissions.IsPlaylistToken & (core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin) ) | markdown_permissions.IsMarkdownDocumentPlaylistOrOrganizationAdmin @@ -272,6 +288,7 @@ class MarkdownImageViewSet( APIViewMixin, ObjectPkMixin, ObjectRelatedMixin, + ObjectMarkdownDocumentRelatedMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.RetrieveModelMixin, @@ -293,9 +310,7 @@ def get_permissions(self): else: permission_classes = [ markdown_permissions.IsTokenResourceRouteObjectRelatedMarkdownDocument - & core_permissions.IsTokenInstructor - | markdown_permissions.IsTokenResourceRouteObjectRelatedMarkdownDocument - & core_permissions.IsTokenAdmin + & (core_permissions.IsTokenInstructor | core_permissions.IsTokenAdmin) | IsRelatedMarkdownDocumentPlaylistOrOrganizationAdmin ] return [permission() for permission in permission_classes] @@ -306,7 +321,8 @@ def get_queryset(self): """ if self.request.resource: return MarkdownImage.objects.filter( - markdown_document__id=self.request.resource.id, + markdown_document__id=self.get_related_markdown_document_id(), + markdown_document__playlist__id=self.request.resource.id, ) return MarkdownImage.objects.all() diff --git a/src/backend/marsha/markdown/permissions.py b/src/backend/marsha/markdown/permissions.py index 95661e2574..ec6f659138 100644 --- a/src/backend/marsha/markdown/permissions.py +++ b/src/backend/marsha/markdown/permissions.py @@ -4,6 +4,7 @@ from rest_framework import permissions from marsha.core import models +from marsha.markdown.models import MarkdownDocument def _is_organization_admin(user_id, markdown_document_id): @@ -32,26 +33,25 @@ def _is_playlist_admin(user_id, markdown_document_id): ).exists() -class IsTokenResourceRouteObjectRelatedResource(permissions.BasePermission): +class IsTokenResourceRouteObjectRelatedMarkdownDocument(permissions.BasePermission): """ - Base permission class for JWT Tokens related to a resource object linked to a resource. + Base permission class for JWT Tokens related to a resource object linked to a + Markdown document. - These permissions grant access to users authenticated with a resource JWT token built from a - resource. + These permissions grants access to users authenticated with a JWT token built from a + resource ie related to a TokenUser as defined in `rest_framework_simplejwt`. """ - linked_resource_attribute = "" - def has_permission(self, request, view): """ - Allow the request if the JWT resource matches the resource - related to the object in the url. + Allow the request if the JWT resource matches the Markdown document related to the object + in the url. Parameters ---------- - request : Type[rest_framework.request.Request] + request : Type[django.http.request.HttpRequest] The request that holds the authenticated user - view : Type[rest_framework.viewsets or rest_framework.views] + view : Type[restframework.viewsets or restframework.views] The API view for which permissions are being checked Returns @@ -62,29 +62,9 @@ def has_permission(self, request, view): if not request.resource: return False - try: - return ( - str( - getattr( - view.get_related_object(), - self.linked_resource_attribute, - ).id - ) - == request.resource.id - ) - except ObjectDoesNotExist: - return False - - -class IsTokenResourceRouteObjectRelatedMarkdownDocument( - IsTokenResourceRouteObjectRelatedResource -): - """ - Base permission class for JWT Tokens related to a resource object - linked to a Markdown document. - """ - - linked_resource_attribute = "markdown_document" + return MarkdownDocument.objects.filter( + pk=view.get_related_markdown_document_id(), playlist_id=request.resource.id + ).exists() class IsMarkdownDocumentPlaylistOrOrganizationAdmin(permissions.BasePermission): diff --git a/src/backend/marsha/markdown/serializers.py b/src/backend/marsha/markdown/serializers.py index 9e1725ba35..1f3bac005a 100644 --- a/src/backend/marsha/markdown/serializers.py +++ b/src/backend/marsha/markdown/serializers.py @@ -64,14 +64,9 @@ def create(self, validated_data): The "validated_data" dictionary is returned after modification. """ - # resource here is a Markdown document - resource = self.context["request"].resource - markdown_document_id = self.context["request"].data.get("markdown_document") + markdown_document_id = self.context["view"].get_related_markdown_document_id() if not validated_data.get("markdown_document_id"): - if resource: - validated_data["markdown_document_id"] = resource.id - elif markdown_document_id: - validated_data["markdown_document_id"] = markdown_document_id + validated_data["markdown_document_id"] = markdown_document_id return super().create(validated_data) diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_create.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_create.py index 0bccad4ead..40a44a0b28 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_create.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_create.py @@ -38,7 +38,7 @@ def test_api_document_create_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -60,7 +60,9 @@ def test_api_document_create_instructor(self): """An instructor should not be able to create a Markdown document.""" markdown_document = MarkdownDocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.post( "/api/markdown-documents/", diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_delete.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_delete.py index c5454e6088..09a597ae78 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_delete.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_delete.py @@ -38,7 +38,7 @@ def test_api_document_delete_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -52,7 +52,9 @@ def test_api_document_delete_instructor(self): """An instructor should not be able to create a Markdown document.""" markdown_document = MarkdownDocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.delete( f"/api/markdown-documents/{markdown_document.pk}/", diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_list.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_list.py index 308ff8195c..b2826d860c 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_list.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_list.py @@ -36,7 +36,7 @@ def test_api_document_fetch_list_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -49,7 +49,9 @@ def test_api_document_fetch_list_instructor(self): """An instrustor should not be able to fetch a Markdown document list.""" markdown_document = MarkdownDocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.get( "/api/markdown-documents/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_render_latex.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_render_latex.py index c41ccffcbe..9bee8a7969 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_render_latex.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_render_latex.py @@ -32,7 +32,7 @@ def test_api_document_render_latex_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -48,7 +48,9 @@ def test_api_document_render_latex_instructor(self): """An instructor should be able to render LaTeX content.""" markdown_document = MarkdownDocumentFactory(is_draft=True) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.post( f"/api/markdown-documents/{markdown_document.pk}/latex-rendering/", diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_retrieve.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_retrieve.py index 0a09c55f09..c9dbc8f740 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_retrieve.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_retrieve.py @@ -43,7 +43,7 @@ def test_api_document_fetch_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -69,7 +69,9 @@ def test_api_document_fetch_instructor(self): translations__rendered_content="

Heading1

\n

Some content

", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.get( f"/api/markdown-documents/{markdown_document.pk}/", @@ -130,7 +132,7 @@ def test_api_document_fetch_instructor_read_only(self): markdown_document = MarkdownDocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=False, ) diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_update.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_update.py index 7a73c9b581..cb2dd9d3cc 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_update.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_update.py @@ -48,7 +48,7 @@ def test_api_document_update_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) data = {"title": "new title"} @@ -66,7 +66,7 @@ def test_api_document_update_instructor_read_only(self): markdown_document = MarkdownDocumentFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=False, ) data = {"title": "new title"} @@ -99,7 +99,9 @@ def test_api_document_update_instructor(self): """An instructor should be able to update a Markdown document.""" markdown_document = MarkdownDocumentFactory(is_draft=True) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) data = {"is_draft": False} diff --git a/src/backend/marsha/markdown/tests/api/markdown_documents/test_update_translations.py b/src/backend/marsha/markdown/tests/api/markdown_documents/test_update_translations.py index 24979934de..7689c350c7 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_documents/test_update_translations.py +++ b/src/backend/marsha/markdown/tests/api/markdown_documents/test_update_translations.py @@ -30,7 +30,7 @@ def test_api_document_translation_update_student(self): markdown_document = MarkdownDocumentFactory() jwt_token = StudentLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, permissions__can_update=True, ) @@ -53,7 +53,9 @@ def test_api_document_translation_update_instructor(self): """An instructor should be able to update a Markdown document translated content.""" markdown_document = MarkdownDocumentFactory(is_draft=True) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) data = { "language_code": "en", diff --git a/src/backend/marsha/markdown/tests/api/markdown_images/test_create.py b/src/backend/marsha/markdown/tests/api/markdown_images/test_create.py index 974f993b53..c5e8959694 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_images/test_create.py +++ b/src/backend/marsha/markdown/tests/api/markdown_images/test_create.py @@ -25,17 +25,21 @@ class MarkdownImageCreateApiTest(TestCase): def test_api_markdown_image_create_anonymous(self): """Anonymous users should not be able to create a Markdown image.""" - response = self.client.post("/api/markdown-images/") + markdown_document = MarkdownDocumentFactory() + response = self.client.post( + f"/api/markdown-documents/{markdown_document.id}/markdown-images/" + ) self.assertEqual(response.status_code, 401) def test_api_markdown_image_create_student(self): """Student users should not be able to create a Markdown image.""" markdown_document = MarkdownDocumentFactory() - jwt_token = StudentLtiTokenFactory(resource=markdown_document) + jwt_token = StudentLtiTokenFactory(resource=markdown_document.playlist) response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -44,10 +48,13 @@ def test_api_markdown_image_create_instructor(self): """Instructors users should be able to create a Markdown image.""" markdown_document = MarkdownDocumentFactory() - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 201) @@ -75,10 +82,13 @@ def test_api_markdown_image_create_already_existing_instructor(self): markdown_document = MarkdownDocumentFactory() MarkdownImageFactory(markdown_document=markdown_document) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 201) @@ -108,7 +118,8 @@ def test_api_markdown_image_instructor_create_in_read_only(self): ) response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_image.markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -122,7 +133,8 @@ def test_api_markdown_image_create_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -138,7 +150,7 @@ def test_api_markdown_image_create_user_access_token_organization_admin(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - "/api/markdown-images/", + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", data={"markdown_document": str(markdown_document.id)}, ) @@ -167,7 +179,7 @@ def test_api_markdown_image_create_user_access_token_playlist_admin(self): jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.post( - "/api/markdown-images/", + f"/api/markdown-documents/{markdown_document.id}/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", data={"markdown_document": str(markdown_document.id)}, ) diff --git a/src/backend/marsha/markdown/tests/api/markdown_images/test_delete.py b/src/backend/marsha/markdown/tests/api/markdown_images/test_delete.py index a5bb933bd5..deeaa9f402 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_images/test_delete.py +++ b/src/backend/marsha/markdown/tests/api/markdown_images/test_delete.py @@ -25,17 +25,23 @@ def test_api_markdown_image_delete_anonymous(self): """Anonymous users should not be able to delete a Markdown image.""" markdown_image = MarkdownImageFactory() - response = self.client.delete(f"/api/markdown-images/{markdown_image.id}/") + response = self.client.delete( + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/" + ) self.assertEqual(response.status_code, 401) def test_api_markdown_image_delete_student(self): """Student users should not be able to delete a Markdown image.""" markdown_image = MarkdownImageFactory() - jwt_token = StudentLtiTokenFactory(resource=markdown_image.markdown_document) + jwt_token = StudentLtiTokenFactory( + resource=markdown_image.markdown_document.playlist + ) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -45,13 +51,14 @@ def test_api_markdown_image_delete_instructor(self): markdown_image = MarkdownImageFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_image.markdown_document, + resource=markdown_image.markdown_document.playlist, ) self.assertEqual(MarkdownImage.objects.count(), 1) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 204) @@ -68,7 +75,8 @@ def test_api_markdown_image_delete_instructor(self): # Creating a new Markdown image should be allowed. response = self.client.post( - "/api/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}" + f"/api/markdown-documents/{markdown_image.markdown_document.id}/markdown-images/", + HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 201) @@ -78,12 +86,13 @@ def test_api_markdown_image_delete_instructor_in_read_only(self): markdown_image = MarkdownImageFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_image.markdown_document, + resource=markdown_image.markdown_document.playlist, permissions__can_update=False, ) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -101,7 +110,8 @@ def test_api_markdown_image_delete_instructor_other_markdown_document(self): jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document_token) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -115,7 +125,8 @@ def test_api_markdown_image_delete_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -134,7 +145,8 @@ def test_api_markdown_image_delete_user_access_token_organization_admin(self): self.assertEqual(MarkdownImage.objects.count(), 1) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 204) @@ -151,7 +163,7 @@ def test_api_markdown_image_delete_user_access_token_organization_admin(self): # Creating a new Markdown image should be allowed. response = self.client.post( - "/api/markdown-images/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", data={"markdown_document": str(markdown_image.markdown_document.id)}, ) @@ -173,7 +185,8 @@ def test_api_markdown_image_delete_user_access_token_playlist_admin(self): self.assertEqual(MarkdownImage.objects.count(), 1) response = self.client.delete( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 204) @@ -190,7 +203,7 @@ def test_api_markdown_image_delete_user_access_token_playlist_admin(self): # Creating a new Markdown image should be allowed. response = self.client.post( - "/api/markdown-images/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}/markdown-images/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", data={"markdown_document": str(markdown_image.markdown_document.id)}, ) diff --git a/src/backend/marsha/markdown/tests/api/markdown_images/test_initiate_upload.py b/src/backend/marsha/markdown/tests/api/markdown_images/test_initiate_upload.py index b5322758d3..1bbdc4a255 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_images/test_initiate_upload.py +++ b/src/backend/marsha/markdown/tests/api/markdown_images/test_initiate_upload.py @@ -30,17 +30,21 @@ def test_api_markdown_image_initiate_upload_anonymous(self): markdown_image = MarkdownImageFactory() response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/" + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/" ) self.assertEqual(response.status_code, 401) def test_api_markdown_image_initiate_upload_student(self): """Student users should not be allowed to initiate an upload.""" markdown_image = MarkdownImageFactory() - jwt_token = StudentLtiTokenFactory(resource=markdown_image.markdown_document) + jwt_token = StudentLtiTokenFactory( + resource=markdown_image.markdown_document.playlist + ) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -55,7 +59,9 @@ def test_api_markdown_image_initiate_upload_instructor(self): markdown_document=markdown_document, upload_state="ready", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) # Get the upload policy for this Markdown image # It should generate a key file with the Unix timestamp of the present time @@ -65,7 +71,8 @@ def test_api_markdown_image_initiate_upload_instructor(self): ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", data={"filename": "not_used.png", "mimetype": "image/png", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -111,12 +118,13 @@ def test_api_markdown_image_initiate_upload_instructor_read_only(self): markdown_image = MarkdownImageFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_image.markdown_document, + resource=markdown_image.markdown_document.playlist, permissions__can_update=False, ) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", data={"filename": "not_used.gif", "mimetype": "image/gif", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -132,7 +140,8 @@ def test_api_markdown_image_initiate_upload_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) self.assertEqual(response.status_code, 403) @@ -160,7 +169,8 @@ def test_api_markdown_image_initiate_upload_user_access_token_organization_admin ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", data={"filename": "not_used.png", "mimetype": "image/png", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -221,7 +231,8 @@ def test_api_markdown_image_initiate_upload_user_access_token_playlist_admin(sel ) as mock_dt: mock_dt.utcnow = mock.Mock(return_value=now) response = self.client.post( - f"/api/markdown-images/{markdown_image.id}/initiate-upload/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/initiate-upload/", data={"filename": "not_used.png", "mimetype": "image/png", "size": 10}, HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) diff --git a/src/backend/marsha/markdown/tests/api/markdown_images/test_retrieve.py b/src/backend/marsha/markdown/tests/api/markdown_images/test_retrieve.py index a02af8e09d..21dde7b8d8 100644 --- a/src/backend/marsha/markdown/tests/api/markdown_images/test_retrieve.py +++ b/src/backend/marsha/markdown/tests/api/markdown_images/test_retrieve.py @@ -26,7 +26,10 @@ class MarkdownImageRetrieveApiTest(TestCase): def test_api_markdown_image_read_detail_anonymous(self): """Anonymous users should not be allowed to retrieve a Markdown image.""" markdown_image = MarkdownImageFactory() - response = self.client.get(f"/api/markdown-images/{markdown_image.id}/") + response = self.client.get( + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/" + ) self.assertEqual(response.status_code, 401) content = json.loads(response.content) self.assertEqual( @@ -37,10 +40,13 @@ def test_api_markdown_image_read_detail_student(self): """Students users should not be allowed to read a Markdown image detail.""" markdown_image = MarkdownImageFactory() - jwt_token = StudentLtiTokenFactory(resource=markdown_image.markdown_document) + jwt_token = StudentLtiTokenFactory( + resource=markdown_image.markdown_document.playlist + ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -51,12 +57,13 @@ def test_api_markdown_image_instructor_read_detail_in_read_only(self): markdown_image = MarkdownImageFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_image.markdown_document, + resource=markdown_image.markdown_document.playlist, permissions__can_update=False, ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -74,10 +81,13 @@ def test_api_markdown_image_read_detail_token_user(self): upload_state="pending", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -102,12 +112,13 @@ def test_api_markdown_image_administrator_read_detail_in_read_only(self): markdown_image = MarkdownImageFactory() jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_image.markdown_document, + resource=markdown_image.markdown_document.playlist, permissions__can_update=False, ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -126,12 +137,13 @@ def test_api_markdown_image_read_detail_admin_user(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=markdown_document, + resource=markdown_document.playlist, roles=["administrator"], ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -164,10 +176,13 @@ def test_api_markdown_image_read_ready_markdown_image(self): extension="gif", ) - jwt_token = InstructorOrAdminLtiTokenFactory(resource=markdown_document) + jwt_token = InstructorOrAdminLtiTokenFactory( + resource=markdown_document.playlist + ) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -200,7 +215,8 @@ def test_api_markdown_image_read_detail_user_access_token(self): jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -219,7 +235,8 @@ def test_api_markdown_image_read_detail_user_access_token_organization_admin(sel jwt_token = UserAccessTokenFactory(user=organization_access.user) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) @@ -253,7 +270,8 @@ def test_api_markdown_image_read_detail_user_access_token_playlist_admin(self): jwt_token = UserAccessTokenFactory(user=playlist_access.user) response = self.client.get( - f"/api/markdown-images/{markdown_image.id}/", + f"/api/markdown-documents/{markdown_image.markdown_document.id}" + f"/markdown-images/{markdown_image.id}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}", ) diff --git a/src/backend/marsha/markdown/tests/test_views_lti.py b/src/backend/marsha/markdown/tests/test_views_lti.py index 0576cad18f..9fd6fa709b 100644 --- a/src/backend/marsha/markdown/tests/test_views_lti.py +++ b/src/backend/marsha/markdown/tests/test_views_lti.py @@ -157,7 +157,9 @@ def test_views_lti_markdown_document_instructor_same_playlist( context = json.loads(html.unescape(match.group(1))) jwt_token = ResourceAccessToken(context.get("jwt")) - self.assertEqual(jwt_token.payload["resource_id"], str(markdown_document.id)) + self.assertEqual( + jwt_token.payload["resource_id"], str(markdown_document.playlist.id) + ) self.assertEqual( jwt_token.payload["user"], { diff --git a/src/backend/marsha/markdown/urls.py b/src/backend/marsha/markdown/urls.py index 82bb374881..9f712fdc39 100644 --- a/src/backend/marsha/markdown/urls.py +++ b/src/backend/marsha/markdown/urls.py @@ -16,7 +16,11 @@ MarkdownDocumentViewSet, basename="markdown-documents", ) -router.register("markdown-images", MarkdownImageViewSet, basename="markdown-images") + +markdown_document_related_router = DefaultRouter() +markdown_document_related_router.register( + "markdown-images", MarkdownImageViewSet, basename="markdown-images" +) urlpatterns = [ path( @@ -25,4 +29,8 @@ name="markdown_document_lti_view", ), path("api/", include(router.urls)), + path( + "api/markdown-documents//", + include(markdown_document_related_router.urls), + ), ] diff --git a/src/backend/marsha/urls.py b/src/backend/marsha/urls.py index 99eb6d4851..38981b4531 100644 --- a/src/backend/marsha/urls.py +++ b/src/backend/marsha/urls.py @@ -69,24 +69,6 @@ "lti-user-associations", LtiUserAssociationViewSet, basename="lti_user_associations" ) -# Old routes to remove -router.register( - models.LiveSession.RESOURCE_NAME, - LiveSessionViewSet, - basename="live_sessions", -) -router.register( - models.SharedLiveMedia.RESOURCE_NAME, - SharedLiveMediaViewSet, - basename="sharedlivemedias", -) -router.register( - models.TimedTextTrack.RESOURCE_NAME, - TimedTextTrackViewSet, - basename="timed_text_tracks", -) -router.register(models.Thumbnail.RESOURCE_NAME, ThumbnailViewSet, basename="thumbnails") - # Video related resources (for nested routes) video_related_router = DefaultRouter() video_related_router.register( diff --git a/src/backend/marsha/websocket/consumers/video.py b/src/backend/marsha/websocket/consumers/video.py index 12f3aabe74..97379f9c79 100644 --- a/src/backend/marsha/websocket/consumers/video.py +++ b/src/backend/marsha/websocket/consumers/video.py @@ -45,7 +45,7 @@ async def _check_permissions(self): if isinstance(token, ResourceAccessToken): # With LTI: anyone with a valid token for the video can access - if token.payload.get("resource_id") != self.__get_video_id(): + if not await self._has_access_to_video(token): raise ConnectionRefusedError() elif isinstance(token, UserAccessToken): @@ -58,6 +58,13 @@ async def _check_permissions(self): else: raise RuntimeError("This should not happen") + @database_sync_to_async + def _has_access_to_video(self, token): + """Return if the user has access to the video.""" + return Video.objects.filter( + pk=self.__get_video_id(), playlist_id=token.payload.get("resource_id") + ).exists() + async def connect(self): """ Manage connection to this consumer. @@ -116,14 +123,16 @@ def retrieve_live_session(self): """Guess a live_session from the token and create it id not present.""" token = self.scope["token"] if LiveSessionServices.is_lti_token(token): - live_session, _ = LiveSessionServices.get_livesession_from_lti(token) + live_session, _ = LiveSessionServices.get_livesession_from_lti( + token, self.__get_video_id() + ) else: query_string = parse_qs(self.scope["query_string"]) if b"anonymous_id" not in query_string: raise ConnectionRefusedError() live_session, _ = LiveSessionServices.get_livesession_from_anonymous_id( anonymous_id=query_string[b"anonymous_id"][0].decode("utf-8"), - video_id=token.payload["resource_id"], + video_id=self.__get_video_id(), ) return live_session diff --git a/src/backend/marsha/websocket/tests/test_consumers_video.py b/src/backend/marsha/websocket/tests/test_consumers_video.py index 546764ac6c..d51193139d 100644 --- a/src/backend/marsha/websocket/tests/test_consumers_video.py +++ b/src/backend/marsha/websocket/tests/test_consumers_video.py @@ -300,7 +300,7 @@ async def test_connect_matching_video_anonymous(self): self.assertIsNone(live_session.channel_name) - jwt_token = ResourceAccessTokenFactory(resource=video) + jwt_token = ResourceAccessTokenFactory(resource=video.playlist) communicator = WebsocketCommunicator( base_application, @@ -331,7 +331,7 @@ async def test_connect_matching_video_admin(self): self.assertIsNone(live_session.channel_name) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.consumer_site.id), context_id=str(video.playlist.lti_id), user__id=live_session.lti_user_id, @@ -361,7 +361,7 @@ async def test_connect_no_matching_video(self): other_video = await self._get_video() jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.consumer_site.id), context_id=str(video.playlist.lti_id), ) @@ -432,7 +432,7 @@ async def test_video_update_channel_layer(self): ) jwt_token = StudentLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.consumer_site.id), ) @@ -551,7 +551,7 @@ async def test_video_update_instructor_channel_layer(self): ) jwt_token = InstructorOrAdminLtiTokenFactory( - resource=video, + resource=video.playlist, consumer_site=str(video.consumer_site.id), ) @@ -684,7 +684,7 @@ async def test_thumbnail_update_channel_layer(self): thumbnail = await self._get_thumbnail() jwt_token = StudentLtiTokenFactory( - resource=thumbnail.video, + resource=thumbnail.video.playlist, consumer_site=str(thumbnail.video.consumer_site.id), ) @@ -728,7 +728,7 @@ async def test_timed_text_track_update_channel_layer(self): timed_text_track = await self._get_timed_text_track() jwt_token = StudentLtiTokenFactory( - resource=timed_text_track.video, + resource=timed_text_track.video.playlist, consumer_site=str(timed_text_track.video.consumer_site.id), ) diff --git a/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.spec.tsx b/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.spec.tsx index eb6bf249a6..55a63df5fc 100644 --- a/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.spec.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.spec.tsx @@ -21,6 +21,7 @@ describe('useDepositedFileMetadata', () => { }); it('requests the deposited file metadata', async () => { + const fileDepositoryId = '1'; const depositedFileMetadata = { name: 'Deposited files List', description: 'Viewset for the API of the deposited file object.', @@ -32,16 +33,24 @@ describe('useDepositedFileMetadata', () => { ], upload_max_size_bytes: 100, }; - fetchMock.mock(`/api/depositedfiles/`, depositedFileMetadata); + fetchMock.mock( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + depositedFileMetadata, + ); - const { result } = renderHook(() => useDepositedFileMetadata('fr'), { - wrapper: WrapperReactQuery, - }); + const { result } = renderHook( + () => useDepositedFileMetadata('fr', fileDepositoryId), + { + wrapper: WrapperReactQuery, + }, + ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); - expect(fetchMock.lastCall()![0]).toEqual(`/api/depositedfiles/`); + expect(fetchMock.lastCall()![0]).toEqual( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { Authorization: 'Bearer some token', @@ -55,17 +64,26 @@ describe('useDepositedFileMetadata', () => { }); it('fails to get the deposited file metadata', async () => { - fetchMock.mock(`/api/depositedfiles/`, 404); + const fileDepositoryId = '1'; + fetchMock.mock( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + 404, + ); - const { result } = renderHook(() => useDepositedFileMetadata('en'), { - wrapper: WrapperReactQuery, - }); + const { result } = renderHook( + () => useDepositedFileMetadata('en', fileDepositoryId), + { + wrapper: WrapperReactQuery, + }, + ); await waitFor(() => { expect(result.current.isError).toBeTruthy(); }); - expect(fetchMock.lastCall()![0]).toEqual(`/api/depositedfiles/`); + expect(fetchMock.lastCall()![0]).toEqual( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { Authorization: 'Bearer some token', diff --git a/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.ts b/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.ts index 779123d12e..50ceda1c93 100644 --- a/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.ts +++ b/src/frontend/apps/lti_site/apps/deposit/api/useDepositedFileMetadata/index.ts @@ -5,6 +5,7 @@ import { DepositedFileMetadata } from 'apps/deposit/types/metadata'; export const useDepositedFileMetadata = ( locale: string, + fileDepositoryId: string, queryConfig?: UseQueryOptions< DepositedFileMetadata, 'depositedfiles', @@ -12,7 +13,7 @@ export const useDepositedFileMetadata = ( string[] >, ) => { - const key = ['depositedfiles', locale]; + const key = [`filedepositories/${fileDepositoryId}/depositedfiles`, locale]; return useQuery< DepositedFileMetadata, 'depositedfiles', diff --git a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardInstructor/index.spec.tsx b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardInstructor/index.spec.tsx index 76fbc92226..ba14326334 100644 --- a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardInstructor/index.spec.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardInstructor/index.spec.tsx @@ -99,7 +99,7 @@ describe('', () => { const depositedFiles: DepositedFile[] = []; for (let i = 0; i < 40; i++) { depositedFiles.push( - depositedFileMockFactory({ file_depository: fileDepository }), + depositedFileMockFactory({ file_depository_id: fileDepository.id }), ); } const queryClient = new QueryClient(); @@ -170,7 +170,7 @@ describe('', () => { const readStatus = read ? 'read' : 'new'; depositedFiles.push( depositedFileMockFactory({ - file_depository: fileDepository, + file_depository_id: fileDepository.id, filename: `file${i}_${readStatus}.txt`, read, }), diff --git a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.spec.tsx b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.spec.tsx index c5945be6c8..9e9d804561 100644 --- a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.spec.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.spec.tsx @@ -96,6 +96,8 @@ describe('', () => { modelName.DepositedFiles, depositedFile.id, file, + modelName.FileDepositories, + depositedFile.file_depository_id, ); }); @@ -112,7 +114,7 @@ describe('', () => { }); fetchMock.mock( - '/api/depositedfiles/', + '/api/filedepositories/1/depositedfiles/', { upload_max_size_bytes: Math.pow(10, 9), }, diff --git a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.tsx b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.tsx index 115612e41b..ca806e6bc8 100644 --- a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/DashboardStudent/UploadFiles/index.tsx @@ -65,7 +65,10 @@ export const UploadFiles = () => { const { addUpload, uploadManagerState } = useUploadManager(); const [filesToUpload, setFilesToUpload] = useState([]); const [uploading, setUploading] = useState(false); - const metadata = useDepositedFileMetadata(intl.locale); + const metadata = useDepositedFileMetadata( + intl.locale, + depositAppData.fileDepository?.id || '', + ); const uploadsInProgress = Object.values(uploadManagerState).filter((state) => [UploadManagerStatus.INIT, UploadManagerStatus.UPLOADING].includes( @@ -87,11 +90,20 @@ export const UploadFiles = () => { setUploading(true); try { - const depositedFile = await createDepositedFile({ - size: file.size, - filename: file.name, - }); - addUpload(modelName.DepositedFiles, depositedFile.id, file); + const depositedFile = await createDepositedFile( + { + size: file.size, + filename: file.name, + }, + depositAppData.fileDepository?.id || '', + ); + addUpload( + modelName.DepositedFiles, + depositedFile.id, + file, + modelName.FileDepositories, + depositedFile.file_depository_id, + ); refreshDepositedFiles(); } catch (error) { if ((error as object).hasOwnProperty('size') && metadata.data) { diff --git a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.spec.tsx b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.spec.tsx index 768392c3cf..f041fa8082 100644 --- a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.spec.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.spec.tsx @@ -36,17 +36,20 @@ describe('', () => { 'https://example.com/file.txt', ); - fetchMock.patch(`/api/depositedfiles/${depositedFile.id}/`, { - ...depositedFile, - read: true, - }); + fetchMock.patch( + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, + { + ...depositedFile, + read: true, + }, + ); fireEvent.click(downloadButton); fireEvent.blur(window); await waitFor(() => expect(fetchMock.lastCall()![0]).toEqual( - `/api/depositedfiles/${depositedFile.id}/`, + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, ), ); expect(fetchMock.lastCall()![1]).toEqual({ diff --git a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.tsx b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.tsx index bac8d83846..7324629cff 100644 --- a/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/components/Dashboard/common/DepositedFileRow/index.tsx @@ -29,7 +29,7 @@ export const DepositedFileRow = ({ file }: DepositedFileProps) => { const uploadedOnDate = uploadedOn ? uploadedOn.toFormat('dd/MM/yyyy') : null; const uploadedOnTime = uploadedOn ? uploadedOn.toFormat('HH:mm') : null; - const { mutate } = useUpdateDepositedFile(file.id); + const { mutate } = useUpdateDepositedFile(file.id, file.file_depository_id); const markFileAsRead = () => { const callback = () => { mutate( diff --git a/src/frontend/apps/lti_site/apps/deposit/data/queries/index.spec.tsx b/src/frontend/apps/lti_site/apps/deposit/data/queries/index.spec.tsx index f00aecc169..f781983dc3 100644 --- a/src/frontend/apps/lti_site/apps/deposit/data/queries/index.spec.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/data/queries/index.spec.tsx @@ -294,7 +294,7 @@ describe('queries', () => { it('requests the first page of the resource list', async () => { const fileDepository = fileDepositoryMockFactory(); const depositedFiles = Array(4).fill( - depositedFileMockFactory({ file_depository: fileDepository }), + depositedFileMockFactory({ file_depository_id: fileDepository.id }), ); fetchMock.mock( `/api/filedepositories/${fileDepository.id}/depositedfiles/?limit=3`, @@ -328,7 +328,7 @@ describe('queries', () => { it('requests the second page of the resource list', async () => { const fileDepository = fileDepositoryMockFactory(); const depositedFiles = Array(4).fill( - depositedFileMockFactory({ file_depository: fileDepository }), + depositedFileMockFactory({ file_depository_id: fileDepository.id }), ); fetchMock.mock( `/api/filedepositories/${fileDepository.id}/depositedfiles/?limit=3&offset=3`, @@ -362,7 +362,7 @@ describe('queries', () => { it('fails to get the resource list', async () => { const fileDepository = fileDepositoryMockFactory(); Array(4).fill( - depositedFileMockFactory({ file_depository: fileDepository }), + depositedFileMockFactory({ file_depository_id: fileDepository.id }), ); fetchMock.mock( `/api/filedepositories/${fileDepository.id}/depositedfiles/?limit=3`, @@ -398,12 +398,16 @@ describe('queries', () => { it('updates the resource', async () => { const depositedFile = depositedFileMockFactory(); fetchMock.patch( - `/api/depositedfiles/${depositedFile.id}/`, + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, depositedFile, ); const { result } = renderHook( - () => useUpdateDepositedFile(depositedFile.id), + () => + useUpdateDepositedFile( + depositedFile.id, + depositedFile.file_depository_id, + ), { wrapper: WrapperReactQuery, }, @@ -416,7 +420,7 @@ describe('queries', () => { }); expect(fetchMock.lastCall()![0]).toEqual( - `/api/depositedfiles/${depositedFile.id}/`, + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { @@ -434,10 +438,17 @@ describe('queries', () => { it('fails to update the resource', async () => { const depositedFile = depositedFileMockFactory(); - fetchMock.patch(`/api/depositedfiles/${depositedFile.id}/`, 400); + fetchMock.patch( + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, + 400, + ); const { result } = renderHook( - () => useUpdateDepositedFile(depositedFile.id), + () => + useUpdateDepositedFile( + depositedFile.id, + depositedFile.file_depository_id, + ), { wrapper: WrapperReactQuery, }, @@ -450,7 +461,7 @@ describe('queries', () => { }); expect(fetchMock.lastCall()![0]).toEqual( - `/api/depositedfiles/${depositedFile.id}/`, + `/api/filedepositories/${depositedFile.file_depository_id}/depositedfiles/${depositedFile.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { diff --git a/src/frontend/apps/lti_site/apps/deposit/data/queries/index.tsx b/src/frontend/apps/lti_site/apps/deposit/data/queries/index.tsx index 7f07db4e64..f10bfd1127 100644 --- a/src/frontend/apps/lti_site/apps/deposit/data/queries/index.tsx +++ b/src/frontend/apps/lti_site/apps/deposit/data/queries/index.tsx @@ -202,6 +202,7 @@ type UseUpdateDepositedFileOptions = UseMutationOptions< >; export const useUpdateDepositedFile = ( id: string, + parentId: string, options?: UseUpdateDepositedFileOptions, ) => { const queryClient = useQueryClient(); @@ -212,7 +213,7 @@ export const useUpdateDepositedFile = ( >({ mutationFn: (updatedDepositedFile) => updateOne({ - name: modelName.DepositedFiles, + name: `${modelName.FileDepositories}/${parentId}/${modelName.DepositedFiles}`, id, object: updatedDepositedFile, }), diff --git a/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.spec.ts b/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.spec.ts index 67f894ec16..125229b551 100644 --- a/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.spec.ts +++ b/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.spec.ts @@ -13,19 +13,26 @@ describe('sideEffects/createDepositedFile', () => { afterEach(() => fetchMock.restore()); it('creates a new deposited file and returns it', async () => { - fetchMock.mock('/api/depositedfiles/', { - id: 'shared_live_media_id', - is_ready_to_show: false, - show_download: true, - upload_state: 'pending', - video: 'video_id', - }); + const fileDepositoryId = '1'; + fetchMock.mock( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + { + id: 'shared_live_media_id', + is_ready_to_show: false, + show_download: true, + upload_state: 'pending', + video: 'video_id', + }, + ); const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); - const depositedFile = await createDepositedFile({ - size: file.size, - filename: file.name, - }); + const depositedFile = await createDepositedFile( + { + size: file.size, + filename: file.name, + }, + fileDepositoryId, + ); const fetchArgs = fetchMock.lastCall()![1]!; @@ -44,23 +51,34 @@ describe('sideEffects/createDepositedFile', () => { }); it('throws when it fails to create the deposited file (request failure)', async () => { + const fileDepositoryId = '1'; fetchMock.mock( - '/api/depositedfiles/', + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, Promise.reject(new Error('Failed to perform the request')), ); const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); await expect( - createDepositedFile({ size: file.size, filename: file.name }), + createDepositedFile( + { size: file.size, filename: file.name }, + fileDepositoryId, + ), ).rejects.toThrow(); }); it('throws when it fails to create the deposited file (API error)', async () => { - fetchMock.mock('/api/depositedfiles/', 400); + const fileDepositoryId = '1'; + fetchMock.mock( + `/api/filedepositories/${fileDepositoryId}/depositedfiles/`, + 400, + ); const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); await expect( - createDepositedFile({ size: file.size, filename: file.name }), + createDepositedFile( + { size: file.size, filename: file.name }, + fileDepositoryId, + ), ).rejects.toThrow(); }); }); diff --git a/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.ts b/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.ts index 66c343d89d..07e8cd9ac4 100644 --- a/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.ts +++ b/src/frontend/apps/lti_site/apps/deposit/data/sideEffects/createDepositedFile/index.ts @@ -7,10 +7,13 @@ import { useJwt, } from 'lib-components'; -export const createDepositedFile = async (file: { - size: number; - filename: string; -}): Promise => { +export const createDepositedFile = async ( + file: { + size: number; + filename: string; + }, + fileDepositoryId: string, +): Promise => { const jwt = useJwt.getState().getJwt(); if (!jwt) { @@ -18,7 +21,7 @@ export const createDepositedFile = async (file: { } const response = await fetchWrapper( - `${API_ENDPOINT}/${modelName.DepositedFiles}/`, + `${API_ENDPOINT}/${modelName.FileDepositories}/${fileDepositoryId}/${modelName.DepositedFiles}/`, { headers: { Authorization: `Bearer ${jwt}`, diff --git a/src/frontend/apps/lti_site/apps/deposit/utils/tests/factories.ts b/src/frontend/apps/lti_site/apps/deposit/utils/tests/factories.ts index 79542e84e1..2817894a5c 100644 --- a/src/frontend/apps/lti_site/apps/deposit/utils/tests/factories.ts +++ b/src/frontend/apps/lti_site/apps/deposit/utils/tests/factories.ts @@ -27,7 +27,7 @@ export const depositedFileMockFactory = ( ): DepositedFile => { return { author_name: faker.name.firstName() + ' ' + faker.name.lastName(), - file_depository: fileDepositoryMockFactory(), + file_depository_id: faker.datatype.uuid(), filename: faker.system.fileName(), size: faker.datatype.number().toString(), id: faker.datatype.uuid(), diff --git a/src/frontend/apps/lti_site/components/LTIRoutes/index.tsx b/src/frontend/apps/lti_site/components/LTIRoutes/index.tsx index 6a550e012c..daae8f99ad 100644 --- a/src/frontend/apps/lti_site/components/LTIRoutes/index.tsx +++ b/src/frontend/apps/lti_site/components/LTIRoutes/index.tsx @@ -8,6 +8,7 @@ import { UploadForm, UploadHandlers, UploadManager, + UploadingObject, WithParams, builderFullScreenErrorRoute, modelName, @@ -130,11 +131,13 @@ export const LTIInnerRoutes = () => { path={UPLOAD_FORM_ROUTE.default} element={ - {({ objectId, objectType }) => + {({ objectId, objectType, parentType, parentId }) => objectId && objectType ? ( ) : ( { describe('useThumbnail', () => { it('requests the resource', async () => { const thumbnail = thumbnailMockFactory(); - fetchMock.mock(`/api/thumbnails/${thumbnail.id}/`, thumbnail); + fetchMock.mock( + `/api/videos/${thumbnail.video}/thumbnails/${thumbnail.id}/`, + thumbnail, + ); - const { result } = renderHook(() => useThumbnail(thumbnail.id), { - wrapper: WrapperReactQuery, - }); + const { result } = renderHook( + () => useThumbnail(thumbnail.id, thumbnail.video), + { + wrapper: WrapperReactQuery, + }, + ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(fetchMock.calls().length).toEqual(1); expect(fetchMock.lastCall()![0]).toEqual( - `/api/thumbnails/${thumbnail.id}/`, + `/api/videos/${thumbnail.video}/thumbnails/${thumbnail.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { @@ -280,18 +286,24 @@ describe('queries', () => { it('fails to get the resource', async () => { const thumbnail = thumbnailMockFactory(); - fetchMock.mock(`/api/thumbnails/${thumbnail.id}/`, 404); + fetchMock.mock( + `/api/videos/${thumbnail.video}/thumbnails/${thumbnail.id}/`, + 404, + ); - const { result } = renderHook(() => useThumbnail(thumbnail.id), { - wrapper: WrapperReactQuery, - }); + const { result } = renderHook( + () => useThumbnail(thumbnail.id, thumbnail.video), + { + wrapper: WrapperReactQuery, + }, + ); await waitFor(() => { expect(result.current.isError).toBeTruthy(); }); expect(fetchMock.lastCall()![0]).toEqual( - `/api/thumbnails/${thumbnail.id}/`, + `/api/videos/${thumbnail.video}/thumbnails/${thumbnail.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { diff --git a/src/frontend/apps/lti_site/data/queries/index.tsx b/src/frontend/apps/lti_site/data/queries/index.tsx index 87e9207bbb..b4a0411962 100644 --- a/src/frontend/apps/lti_site/data/queries/index.tsx +++ b/src/frontend/apps/lti_site/data/queries/index.tsx @@ -124,9 +124,10 @@ export const usePlaylists = ( export const useThumbnail = ( thumbnailId: string, + videoId: string, queryConfig?: UseQueryOptions, ) => { - const key = ['thumbnails', thumbnailId]; + const key = [`videos/${videoId}/thumbnails`, thumbnailId]; return useQuery({ queryKey: key, queryFn: fetchOne, diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.spec.tsx index 48b34d07f0..3dd6fffd4e 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.spec.tsx @@ -49,7 +49,7 @@ describe('', () => { started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, - classroom: classroom.id, + classroom_id: classroom.id, }); render(); @@ -77,7 +77,7 @@ describe('', () => { started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, - classroom: classroom.id, + classroom_id: classroom.id, }); fetchMock.delete( `/api/classrooms/${classroom.id}/recordings/${classroomRecording.id}/`, @@ -114,7 +114,7 @@ describe('', () => { started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, - classroom: classroom.id, + classroom_id: classroom.id, }); fetchMock.delete( `/api/classrooms/${classroom.id}/recordings/${classroomRecording.id}/`, diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.tsx index 4baa9e346f..d35a33ebf5 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/DeleteClassroomRecordingButton/index.tsx @@ -91,7 +91,7 @@ export const DeleteClassroomRecordingButton = ({ onClickSubmit={() => { setIsModalOpen(false); deleteClassroomRecording.mutate({ - classroomId: recording.classroom, + classroomId: recording.classroom_id, classroomRecordingId: recording.id, }); }} diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.spec.tsx index eebe7e3150..8ad272ac71 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.spec.tsx @@ -49,7 +49,7 @@ describe('', () => { started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, - classroom: classroom.id, + classroom_id: classroom.id, }); render( @@ -74,7 +74,7 @@ describe('', () => { ] as any); const classroom = classroomMockFactory(); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, vod: classroomRecordingVodMockFactory({ upload_state: READY, }), @@ -104,7 +104,7 @@ describe('', () => { started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, - classroom: classroom.id, + classroom_id: classroom.id, }); fetchMock.mock( @@ -152,7 +152,7 @@ describe('', () => { ] as any); const classroom = classroomMockFactory(); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, vod: classroomRecordingVodMockFactory({ upload_state: READY, }), @@ -186,7 +186,7 @@ describe('', () => { ] as any); const classroom = classroomMockFactory(); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, @@ -219,7 +219,7 @@ describe('', () => { ] as any); const classroom = classroomMockFactory(); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, @@ -264,7 +264,7 @@ describe('', () => { ] as any); const classroom = classroomMockFactory(); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, started_at: DateTime.fromJSDate( new Date(2022, 1, 29, 11, 0, 0), ).toISO() as string, @@ -299,7 +299,7 @@ describe('', () => { vod_conversion_enabled: false, }); const classroomRecording = classroomRecordingMockFactory({ - classroom: classroom.id, + classroom_id: classroom.id, }); fetchMock.mock(`/api/classrooms/${classroom.id}/`, { classroom }); diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.tsx index 7ba6dba997..275baba1cd 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/Recordings/Recording/index.tsx @@ -100,7 +100,7 @@ const VodNotReady = ({ conversionEnabled, }: RecordingProps) => { const intl = useIntl(); - const { refetch: refetchClassroom } = useClassroom(recording.classroom); + const { refetch: refetchClassroom } = useClassroom(recording.classroom_id); const convertVOD = useCallback( (recording: ClassroomRecording) => { diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.spec.tsx index ba596b11df..d8459da5c3 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.spec.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import { uploadState, useJwt } from 'lib-components'; import { wrapInIntlProvider } from 'lib-tests'; @@ -47,7 +48,7 @@ describe('', () => { const classroom = classroomMockFactory({ id: '1', started: false }); const document = classroomDocumentMockFactory({ filename: 'my_document.pdf', - classroom: classroom, + classroom_id: classroom.id, }); render( wrapInIntlProvider( @@ -73,11 +74,44 @@ describe('', () => { ).toBeEnabled(); }); + it('sets a document as default', async () => { + const classroom = classroomMockFactory({ started: false }); + const document = classroomDocumentMockFactory({ + filename: 'my_document.pdf', + classroom_id: classroom.id, + }); + fetchMock.patch( + `/api/classrooms/${classroom.id}/classroomdocuments/${document.id}/`, + { + ...document, + is_default: true, + }, + ); + render( + wrapInIntlProvider( + wrapInClassroom( + + + , + classroom, + ), + ), + ); + + userEvent.click( + screen.getByRole('button', { name: 'Click to set as default document' }), + ); + await waitFor(() => expect(fetchMock.calls()).toHaveLength(1)); + }); + it('renders a row in UPLOAD_IN_PROGRESS state', () => { const classroom = classroomMockFactory({ id: '1', started: false }); const document = classroomDocumentMockFactory({ filename: 'my_document.pdf', - classroom: classroom, + classroom_id: classroom.id, upload_state: uploadState.PROCESSING, }); render( @@ -109,7 +143,7 @@ describe('', () => { const classroom = classroomMockFactory({ id: '1', started: false }); const document = classroomDocumentMockFactory({ filename: 'my_document.pdf', - classroom: classroom, + classroom_id: classroom.id, upload_state: uploadState.ERROR, }); render( diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.tsx index 7f41164871..6472002548 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/DocumentRow/index.tsx @@ -65,7 +65,10 @@ export const DocumentRow = ({ uploadingObject, }: DocumentRowProps) => { const intl = useIntl(); - const updateClassroomMutation = useUpdateClassroomDocument(document.id); + const updateClassroomMutation = useUpdateClassroomDocument( + document.classroom_id, + document.id, + ); const deleteDocumentMutation = useDeleteClassroomDocument(); const [isUploadInProgress, setIsUploadInProgress] = useState(false); @@ -88,12 +91,18 @@ export const DocumentRow = ({ }, [updateClassroomMutation]); const setDeleteDocument = useCallback(() => { - deleteDocumentMutation.mutate(document.id, { - onSuccess: () => { - window.dispatchEvent(new CustomEvent('classroomDocumentUpdated')); + deleteDocumentMutation.mutate( + { + classroomId: document.classroom_id, + classroomDocumentId: document.id, + }, + { + onSuccess: () => { + window.dispatchEvent(new CustomEvent('classroomDocumentUpdated')); + }, }, - }); - }, [deleteDocumentMutation, document.id]); + ); + }, [deleteDocumentMutation, document.classroom_id, document.id]); useEffect(() => { (document.upload_state === uploadState.PENDING && uploadingObject) || diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.spec.tsx index 0ebcc94300..c672955c5e 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.spec.tsx @@ -12,7 +12,10 @@ import { Deferred, render } from 'lib-tests'; import React, { PropsWithChildren } from 'react'; import { createClassroomDocument } from '@lib-classroom/data/sideEffects/createClassroomDocument'; -import { classroomDocumentMockFactory } from '@lib-classroom/utils/tests/factories'; +import { + classroomDocumentMockFactory, + classroomMockFactory, +} from '@lib-classroom/utils/tests/factories'; import { UploadDocuments } from '.'; @@ -55,7 +58,7 @@ describe('', () => { it('renders a Dropzone with the relevant messages', () => { fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, @@ -93,7 +96,7 @@ describe('', () => { }); fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, @@ -128,6 +131,8 @@ describe('', () => { modelName.CLASSROOM_DOCUMENTS, classroomDocument.id, file, + modelName.CLASSROOMS, + '1', ); }); @@ -143,7 +148,7 @@ describe('', () => { }); fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, @@ -191,7 +196,7 @@ describe('', () => { }); fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, @@ -236,7 +241,7 @@ describe('', () => { }); fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, @@ -274,7 +279,9 @@ describe('', () => { }); it('updates classroom documents defaults', async () => { + const classroom = classroomMockFactory(); const classroomDocument = classroomDocumentMockFactory({ + classroom_id: classroom.id, filename: 'file.txt', is_default: false, upload_state: READY, @@ -282,32 +289,39 @@ describe('', () => { url: 'https://example.com/file.txt', }); const classroomDocument2 = classroomDocumentMockFactory({ + classroom_id: classroom.id, filename: 'file2.txt', is_default: true, upload_state: READY, uploaded_on: '2020-01-01T00:00:00Z', url: 'https://example.com/file2.txt', }); - fetchMock.get('/api/classrooms/1/classroomdocuments/?limit=999', { - count: 2, - next: null, - previous: null, - results: [classroomDocument, classroomDocument2], - }); + fetchMock.get( + `/api/classrooms/${classroom.id}/classroomdocuments/?limit=999`, + { + count: 2, + next: null, + previous: null, + results: [classroomDocument, classroomDocument2], + }, + ); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroom.id}/classroomdocuments/`, { upload_max_size_bytes: Math.pow(10, 9), }, { method: 'OPTIONS' }, ); - fetchMock.patch(`/api/classroomdocuments/${classroomDocument.id}/`, { - status: 200, - }); + fetchMock.patch( + `/api/classrooms/${classroom.id}/classroomdocuments/${classroomDocument.id}/`, + { + status: 200, + }, + ); - render(); + render(); await screen.findByText('file.txt'); const setDefaultButton = screen.getByRole('button', { @@ -331,14 +345,18 @@ describe('', () => { uploaded_on: '2020-01-01T00:00:00Z', url: 'https://example.com/file.txt', }); - fetchMock.get('/api/classrooms/1/classroomdocuments/?limit=999', { - count: 1, - next: null, - previous: null, - results: [classroomDocument], - }); + const classroomId = classroomDocument.classroom_id; + fetchMock.get( + `/api/classrooms/${classroomId}/classroomdocuments/?limit=999`, + { + count: 1, + next: null, + previous: null, + results: [classroomDocument], + }, + ); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroomId}/classroomdocuments/`, { upload_max_size_bytes: Math.pow(10, 9), }, @@ -351,9 +369,12 @@ describe('', () => { uploadManagerState: {}, }); - fetchMock.delete(`/api/classroomdocuments/${classroomDocument.id}/`, 204); + fetchMock.delete( + `/api/classrooms/${classroomId}/classroomdocuments/${classroomDocument.id}/`, + 204, + ); - render(); + render(); await screen.findByRole('link', { name: 'file.txt' }); @@ -362,12 +383,12 @@ describe('', () => { }); userEvent.click(deleteButton); - await waitFor(() => expect(fetchMock.calls()).toHaveLength(5)); + await waitFor(() => expect(fetchMock.calls()).toHaveLength(6)); const deleteCall = fetchMock.calls( - `/api/classroomdocuments/${classroomDocument.id}/`, + `/api/classrooms/${classroomId}/classroomdocuments/${classroomDocument.id}/`, ); expect(deleteCall[0][0]).toEqual( - `/api/classroomdocuments/${classroomDocument.id}/`, + `/api/classrooms/${classroomId}/classroomdocuments/${classroomDocument.id}/`, ); expect(deleteCall[0][1]).toEqual({ headers: { @@ -393,7 +414,7 @@ describe('', () => { results: [classroomDocument], }); fetchMock.mock( - '/api/classroomdocuments/', + '/api/classrooms/1/classroomdocuments/', { upload_max_size_bytes: Math.pow(10, 9), }, diff --git a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.tsx b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.tsx index 0d523b9479..2b1db5ddfb 100644 --- a/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.tsx +++ b/src/frontend/packages/lib_classroom/src/components/ClassroomWidgetProvider/widgets/SupportSharing/UploadDocuments/index.tsx @@ -80,7 +80,7 @@ export const UploadDocuments = ({ classroomId }: UploadDocumentsProps) => { const intl = useIntl(); const { data: classroomDocuments, refetch: refreshClassroomDocuments } = useClassroomDocuments(classroomId, {}); - const metadata = useClassroomDocumentMetadata(intl.locale); + const metadata = useClassroomDocumentMetadata(classroomId, intl.locale); const { addUpload, uploadManagerState } = useUploadManager(); const [filesToUpload, setFilesToUpload] = useState([]); @@ -125,9 +125,15 @@ export const UploadDocuments = ({ classroomId }: UploadDocumentsProps) => { const document = await createClassroomDocument({ filename: file.name, size: file.size, - classroom: classroomId, + classroom_id: classroomId, }); - addUpload(modelName.CLASSROOM_DOCUMENTS, document.id, file); + addUpload( + modelName.CLASSROOM_DOCUMENTS, + document.id, + file, + modelName.CLASSROOMS, + classroomId, + ); refreshClassroomDocuments(); } catch (error) { if ((error as object).hasOwnProperty('size') && metadata.data) { diff --git a/src/frontend/packages/lib_classroom/src/components/DashboardClassroom/index.spec.tsx b/src/frontend/packages/lib_classroom/src/components/DashboardClassroom/index.spec.tsx index 8a97bda359..e92a1dee3f 100644 --- a/src/frontend/packages/lib_classroom/src/components/DashboardClassroom/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/components/DashboardClassroom/index.spec.tsx @@ -90,7 +90,7 @@ describe('', () => { results: [], }); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroom.id}/classroomdocuments/`, { upload_max_size_bytes: Math.pow(10, 9), }, @@ -126,7 +126,7 @@ describe('', () => { results: [], }); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroom.id}/classroomdocuments/`, { upload_max_size_bytes: Math.pow(10, 9), }, @@ -293,7 +293,7 @@ describe('', () => { results: [], }); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroom.id}/classroomdocuments/`, { upload_max_size_bytes: Math.pow(10, 9), }, diff --git a/src/frontend/packages/lib_classroom/src/data/queries/index.spec.tsx b/src/frontend/packages/lib_classroom/src/data/queries/index.spec.tsx index e2f76735e2..f94b3bfeef 100644 --- a/src/frontend/packages/lib_classroom/src/data/queries/index.spec.tsx +++ b/src/frontend/packages/lib_classroom/src/data/queries/index.spec.tsx @@ -469,7 +469,7 @@ describe('queries', () => { it('requests the resource list', async () => { const classroom = classroomMockFactory(); const classroomDocuments = Array(4).fill( - classroomDocumentMockFactory({ classroom }), + classroomDocumentMockFactory({ classroom_id: classroom.id }), ); fetchMock.mock( `/api/classrooms/${classroom.id}/classroomdocuments/?limit=999`, @@ -501,7 +501,9 @@ describe('queries', () => { it('fails to get the resource list', async () => { const classroom = classroomMockFactory(); - Array(4).fill(classroomDocumentMockFactory({ classroom })); + Array(4).fill( + classroomDocumentMockFactory({ classroom_id: classroom.id }), + ); fetchMock.mock( `/api/classrooms/${classroom.id}/classroomdocuments/?limit=999`, 404, @@ -536,12 +538,16 @@ describe('queries', () => { it('updates the resource', async () => { const classroomDocument = classroomDocumentMockFactory(); fetchMock.patch( - `/api/classroomdocuments/${classroomDocument.id}/`, + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/${classroomDocument.id}/`, classroomDocument, ); const { result } = renderHook( - () => useUpdateClassroomDocument(classroomDocument.id), + () => + useUpdateClassroomDocument( + classroomDocument.classroom_id, + classroomDocument.id, + ), { wrapper: WrapperReactQuery, }, @@ -554,7 +560,7 @@ describe('queries', () => { }); expect(fetchMock.lastCall()![0]).toEqual( - `/api/classroomdocuments/${classroomDocument.id}/`, + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/${classroomDocument.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { @@ -572,10 +578,17 @@ describe('queries', () => { it('fails to update the resource', async () => { const classroomDocument = classroomDocumentMockFactory(); - fetchMock.patch(`/api/classroomdocuments/${classroomDocument.id}/`, 400); + fetchMock.patch( + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/${classroomDocument.id}/`, + 400, + ); const { result } = renderHook( - () => useUpdateClassroomDocument(classroomDocument.id), + () => + useUpdateClassroomDocument( + classroomDocument.classroom_id, + classroomDocument.id, + ), { wrapper: WrapperReactQuery, }, @@ -588,7 +601,7 @@ describe('queries', () => { }); expect(fetchMock.lastCall()![0]).toEqual( - `/api/classroomdocuments/${classroomDocument.id}/`, + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/${classroomDocument.id}/`, ); expect(fetchMock.lastCall()![1]).toEqual({ headers: { diff --git a/src/frontend/packages/lib_classroom/src/data/queries/index.tsx b/src/frontend/packages/lib_classroom/src/data/queries/index.tsx index db63eedca8..550a8d7482 100644 --- a/src/frontend/packages/lib_classroom/src/data/queries/index.tsx +++ b/src/frontend/packages/lib_classroom/src/data/queries/index.tsx @@ -275,10 +275,12 @@ type UseUpdateClassroomDocumentOptions = UseMutationOptions< UseUpdateClassroomDocumentData >; export const useUpdateClassroomDocument = ( + classroomId: string, id: string, options?: UseUpdateClassroomDocumentOptions, ) => { const queryClient = useQueryClient(); + const urlPath = `${ClassroomModelName.CLASSROOMS}/${classroomId}/${ClassroomModelName.CLASSROOM_DOCUMENTS}`; return useMutation< ClassroomDocument, UseUpdateClassroomDocumentError, @@ -286,19 +288,19 @@ export const useUpdateClassroomDocument = ( >({ mutationFn: (updatedClassroomDocument) => updateOne({ - name: 'classroomdocuments', + name: urlPath, id, object: updatedClassroomDocument, }), ...options, onSuccess: (data, variables, context) => { - queryClient.invalidateQueries(['classroomdocuments']); + queryClient.invalidateQueries([urlPath]); if (options?.onSuccess) { options.onSuccess(data, variables, context); } }, onError: (error, variables, context) => { - queryClient.invalidateQueries(['classroomdocuments']); + queryClient.invalidateQueries([urlPath]); if (options?.onError) { options.onError(error, variables, context); } @@ -306,7 +308,10 @@ export const useUpdateClassroomDocument = ( }); }; -type UseDeleteClassroomDocumentData = string; +type UseDeleteClassroomDocumentData = { + classroomId: string; + classroomDocumentId: string; +}; type UseDeleteClassroomDocumentError = | { code: 'exception' } | { @@ -327,20 +332,24 @@ export const useDeleteClassroomDocument = ( UseDeleteClassroomDocumentError, UseDeleteClassroomDocumentData >({ - mutationFn: (classroomDocumentId) => + mutationFn: ({ classroomId, classroomDocumentId }) => deleteOne({ - name: 'classroomdocuments', + name: `${ClassroomModelName.CLASSROOMS}/${classroomId}/${ClassroomModelName.CLASSROOM_DOCUMENTS}`, id: classroomDocumentId, }), ...options, onSuccess: (data, variables, context) => { - queryClient.invalidateQueries(['classroomdocuments']); + queryClient.invalidateQueries([ + `${ClassroomModelName.CLASSROOMS}/${variables.classroomId}/${ClassroomModelName.CLASSROOM_DOCUMENTS}`, + ]); if (options?.onSuccess) { options.onSuccess(data, variables, context); } }, onError: (error, variables, context) => { - queryClient.invalidateQueries(['classroomdocuments']); + queryClient.invalidateQueries([ + `${ClassroomModelName.CLASSROOMS}/${variables.classroomId}/${ClassroomModelName.CLASSROOM_DOCUMENTS}`, + ]); if (options?.onError) { options.onError(error, variables, context); } @@ -417,6 +426,7 @@ export const useEndClassroomAction = classroomActionMutation< >(MutationClassroomAction.END); export const useClassroomDocumentMetadata = ( + classroomId: string, locale: string, queryConfig?: UseQueryOptions< ClassroomDocumentMetadata, @@ -425,7 +435,10 @@ export const useClassroomDocumentMetadata = ( string[] >, ) => { - const key = ['classroomdocuments', locale]; + const key = [ + `${ClassroomModelName.CLASSROOMS}/${classroomId}/${ClassroomModelName.CLASSROOM_DOCUMENTS}`, + locale, + ]; return useQuery< ClassroomDocumentMetadata, 'classroomdocuments', diff --git a/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.spec.ts b/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.spec.ts index a5c7303255..945d5cea60 100644 --- a/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.spec.ts +++ b/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.spec.ts @@ -19,12 +19,15 @@ describe('sideEffects/createClassroomDocument', () => { const classroomDocument = classroomDocumentMockFactory({ filename: file.name, }); - fetchMock.mock('/api/classroomdocuments/', classroomDocument); + fetchMock.mock( + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/`, + classroomDocument, + ); const createdClassroomDocument = await createClassroomDocument({ filename: file.name, size: file.size, - classroom: classroomDocument.classroom.id, + classroom_id: classroomDocument.classroom_id, }); const fetchArgs = fetchMock.lastCall()![1]!; @@ -38,30 +41,33 @@ describe('sideEffects/createClassroomDocument', () => { }); it('throws when it fails to create the deposited file (request failure)', async () => { + const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); + const classroomDocument = classroomDocumentMockFactory(); fetchMock.mock( - '/api/classroomdocuments/', + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/`, Promise.reject(new Error('Failed to perform the request')), ); - const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); - const classroomDocument = classroomDocumentMockFactory(); await expect( createClassroomDocument({ filename: file.name, size: file.size, - classroom: classroomDocument.classroom.id, + classroom_id: classroomDocument.classroom_id, }), ).rejects.toThrow('Failed to perform the request'); }); it('throws when it fails to create the deposited file (API error)', async () => { - fetchMock.mock('/api/classroomdocuments/', 400); const file = new File(['anrusitanrsui tnarsuit narsuit'], 'TestFile.txt'); const classroomDocument = classroomDocumentMockFactory(); + fetchMock.mock( + `/api/classrooms/${classroomDocument.classroom_id}/classroomdocuments/`, + 400, + ); await expect( createClassroomDocument({ filename: file.name, size: file.size, - classroom: classroomDocument.classroom.id, + classroom_id: classroomDocument.classroom_id, }), ).rejects.toThrow(); }); diff --git a/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.ts b/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.ts index a0881ee09d..3a2197a3dd 100644 --- a/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.ts +++ b/src/frontend/packages/lib_classroom/src/data/sideEffects/createClassroomDocument/index.ts @@ -10,15 +10,17 @@ import { export const createClassroomDocument = async (file: { filename: string; size: number; - classroom: Classroom['id']; + classroom_id: Classroom['id']; }) => { const jwt = useJwt.getState().getJwt(); if (!jwt) { throw new Error('No JWT found'); } + const classroomModelName = ClassroomModelName.CLASSROOMS; + const classroomDocumentsModelName = ClassroomModelName.CLASSROOM_DOCUMENTS; const response = await fetchWrapper( - `${API_ENDPOINT}/${String(ClassroomModelName.CLASSROOM_DOCUMENTS)}/`, + `${API_ENDPOINT}/${classroomModelName}/${file.classroom_id}/${classroomDocumentsModelName}/`, { headers: { Authorization: `Bearer ${jwt}`, diff --git a/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.spec.ts b/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.spec.ts index 7891a46e4d..b5de8df2b6 100644 --- a/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.spec.ts +++ b/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.spec.ts @@ -17,7 +17,7 @@ describe('sideEffects/createVOD', () => { it('calls create-vod for a classroom recording', async () => { const classroomRecording = classroomRecordingMockFactory(); fetchMock.mock( - `/api/classrooms/${classroomRecording.classroom}/recordings/${classroomRecording.id}/create-vod/`, + `/api/classrooms/${classroomRecording.classroom_id}/recordings/${classroomRecording.id}/create-vod/`, { key: 'value', }, @@ -38,7 +38,7 @@ describe('sideEffects/createVOD', () => { it('throws when it fails to call create-vod (request failure)', async () => { const classroomRecording = classroomRecordingMockFactory(); fetchMock.mock( - `/api/classrooms/${classroomRecording.classroom}/recordings/${classroomRecording.id}/create-vod/`, + `/api/classrooms/${classroomRecording.classroom_id}/recordings/${classroomRecording.id}/create-vod/`, Promise.reject(new Error('Failed to perform the request')), ); await expect(createVOD(classroomRecording, 'my title')).rejects.toThrow( @@ -49,7 +49,7 @@ describe('sideEffects/createVOD', () => { it('throws when it fails to call create-vod (API error)', async () => { const classroomRecording = classroomRecordingMockFactory(); fetchMock.mock( - `/api/classrooms/${classroomRecording.classroom}/recordings/${classroomRecording.id}/create-vod/`, + `/api/classrooms/${classroomRecording.classroom_id}/recordings/${classroomRecording.id}/create-vod/`, 400, ); await expect(createVOD(classroomRecording, 'my title')).rejects.toThrow(); diff --git a/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.ts b/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.ts index 07273ed17f..03ee8081d2 100644 --- a/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.ts +++ b/src/frontend/packages/lib_classroom/src/data/sideEffects/createVOD/index.ts @@ -18,7 +18,7 @@ export const createVOD = async ( } const response = await fetchWrapper( - `${API_ENDPOINT}/${ClassroomModelName.CLASSROOMS}/${recording.classroom}/${ClassroomModelName.CLASSROOM_RECORDINGS}/${recording.id}/create-vod/`, + `${API_ENDPOINT}/${ClassroomModelName.CLASSROOMS}/${recording.classroom_id}/${ClassroomModelName.CLASSROOM_RECORDINGS}/${recording.id}/create-vod/`, { headers: { Authorization: `Bearer ${jwt}`, diff --git a/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts b/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts index b5118b6aef..0a242393e9 100644 --- a/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts +++ b/src/frontend/packages/lib_classroom/src/utils/tests/factories.ts @@ -80,7 +80,7 @@ export const classroomDocumentMockFactory = ( classroomDocument: Partial = {}, ): ClassroomDocument => { return { - classroom: classroomMockFactory(), + classroom_id: faker.datatype.uuid().toString(), filename: faker.system.fileName(), id: faker.datatype.uuid(), is_default: false, @@ -95,7 +95,7 @@ export const classroomRecordingMockFactory = ( classroomRecording: Partial = {}, ): ClassroomRecording => { return { - classroom: faker.datatype.uuid(), + classroom_id: faker.datatype.uuid(), id: faker.datatype.uuid(), started_at: faker.date.recent().toISOString(), video_file_url: faker.internet.url(), diff --git a/src/frontend/packages/lib_components/src/common/UploadField/index.tsx b/src/frontend/packages/lib_components/src/common/UploadField/index.tsx index 44bb4db099..8cbaee12ca 100644 --- a/src/frontend/packages/lib_components/src/common/UploadField/index.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadField/index.tsx @@ -4,7 +4,10 @@ import Dropzone from 'react-dropzone'; import { defineMessages, useIntl } from 'react-intl'; import styled from 'styled-components'; -import { useUploadManager } from '@lib-components/common/UploadManager'; +import { + UploadingObject, + useUploadManager, +} from '@lib-components/common/UploadManager'; import { uploadableModelName } from '@lib-components/types/models'; import { DropzonePlaceholder } from './DropzonePlaceholder'; @@ -25,16 +28,23 @@ const DropzoneStyled = styled.div` export interface UploadFieldProps { objectType: uploadableModelName; objectId: string; + parentType?: Maybe; + parentId?: Maybe; } -export const UploadField = ({ objectType, objectId }: UploadFieldProps) => { +export const UploadField = ({ + objectType, + objectId, + parentType, + parentId, +}: UploadFieldProps) => { const { addUpload } = useUploadManager(); const [file, setFile] = useState>(undefined); const intl = useIntl(); const onDrop = (files: File[]) => { setFile(files[0]); - addUpload(objectType, objectId, files[0]); + addUpload(objectType, objectId, files[0], parentType, parentId); }; return ( diff --git a/src/frontend/packages/lib_components/src/common/UploadForm/index.tsx b/src/frontend/packages/lib_components/src/common/UploadForm/index.tsx index fdae484570..c048ddd1af 100644 --- a/src/frontend/packages/lib_components/src/common/UploadForm/index.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadForm/index.tsx @@ -14,6 +14,7 @@ import { Loader } from '@lib-components/common/Loader'; import { UploadField } from '@lib-components/common/UploadField'; import { UploadManagerStatus, + UploadingObject, useUploadManager, } from '@lib-components/common/UploadManager'; import { builderDashboardRoute } from '@lib-components/data/routes'; @@ -113,9 +114,16 @@ const UploadFormBack = styled.div` export interface UploadFormProps { objectId: UploadableObject['id']; objectType: uploadableModelName; + parentType?: Maybe; + parentId?: Maybe; } -export const UploadForm = ({ objectId, objectType }: UploadFormProps) => { +export const UploadForm = ({ + objectId, + objectType, + parentType, + parentId, +}: UploadFormProps) => { const appData = useAppConfig(); const { uploadManagerState, resetUpload } = useUploadManager(); const objectStatus = uploadManagerState[objectId]?.status; @@ -199,7 +207,9 @@ export const UploadForm = ({ objectId, objectType }: UploadFormProps) => { /> - + diff --git a/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.spec.tsx b/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.spec.tsx index 4f31b0e232..3188d65b6b 100644 --- a/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.spec.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.spec.tsx @@ -14,7 +14,7 @@ import { Thumbnail, Video } from '@lib-components/types/tracks'; import { UploadHandlers } from './UploadHandlers'; -import { UploadManagerContext, UploadManagerStatus } from '.'; +import { UploadManagerContext, UploadManagerStatus, UploadingObject } from '.'; jest.mock('data/sideEffects/updateResource', () => ({ updateResource: jest.fn(), @@ -36,7 +36,7 @@ const mockGetResource = getStoreResource as jest.MockedFunction< const mockUpdateResource = updateResource as jest.MockedFunction< typeof updateResource >; -const mockFetResource = fetchResource as jest.MockedFunction< +const mockFetchResource = fetchResource as jest.MockedFunction< typeof fetchResource >; @@ -99,6 +99,8 @@ describe('', () => { title: file.name, }, modelName.VIDEOS, + undefined, + undefined, ); }); @@ -143,7 +145,7 @@ describe('', () => { }); it('fetch the resource when the upload manager status is UPLOADING', async () => { - mockFetResource.mockResolvedValue(object); + mockFetchResource.mockResolvedValue(object); const { rerender } = render( ', () => { , ); - expect(mockFetResource).not.toHaveBeenCalled(); + expect(mockFetchResource).not.toHaveBeenCalled(); rerender( ', () => { ); await waitFor(() => { - expect(mockFetResource).toHaveBeenCalledWith(modelName.VIDEOS, object.id); + expect(mockFetchResource).toHaveBeenCalledWith( + modelName.VIDEOS, + object.id, + undefined, + undefined, + ); + }); + }); + + it('fetch the resource with parent resource', async () => { + const image = new File(['(⌐□_□)'], 'course.jpg', { type: 'image/jpeg' }); + const thumbnailState = { + file: image, + objectId: uuidv4(), + objectType: modelName.THUMBNAILS, + progress: 0, + status: UploadManagerStatus.UPLOADING, + parentId: uuidv4(), + parentType: modelName.VIDEOS as UploadingObject['parentType'], + }; + const thumbnail = { id: thumbnailState.objectId } as Thumbnail; + mockFetchResource.mockResolvedValue(thumbnail); + + const { rerender } = render( + {}, + uploadManagerState: { + [thumbnailState.objectId]: { + ...thumbnailState, + status: UploadManagerStatus.INIT, + }, + }, + }} + > + + , + ); + + expect(mockFetchResource).not.toHaveBeenCalled(); + + rerender( + {}, + uploadManagerState: { + [thumbnailState.objectId]: { + ...thumbnailState, + status: UploadManagerStatus.UPLOADING, + }, + }, + }} + > + + , + ); + + await waitFor(() => { + expect(mockFetchResource).toHaveBeenCalledWith( + modelName.THUMBNAILS, + thumbnail.id, + modelName.VIDEOS, + thumbnailState.parentId, + ); }); }); diff --git a/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.tsx b/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.tsx index 469761d696..30090e4996 100644 --- a/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadManager/UploadHandlers.tsx @@ -14,7 +14,8 @@ const UploadSuccessHandler = ({ }: { objectState: UploadManagerState[string]; }) => { - const { status, file, objectId, objectType } = objectState; + const { status, file, objectId, objectType, parentId, parentType } = + objectState; // once upload on S3 is finished, push new state to backend useEffect(() => { @@ -40,10 +41,12 @@ const UploadSuccessHandler = ({ title: file.name, }, objectType, + parentId, + parentType, ); } })(); - }, [file.name, objectId, objectType, status]); + }, [file.name, objectId, objectType, parentId, parentType, status]); // update the ressource beeing uploaded useEffect(() => { @@ -52,9 +55,9 @@ const UploadSuccessHandler = ({ return; } - await fetchResource(objectType, objectId); + await fetchResource(objectType, objectId, parentType, parentId); })(); - }, [objectId, objectType, status]); + }, [objectId, objectType, parentId, parentType, status]); return null; }; diff --git a/src/frontend/packages/lib_components/src/common/UploadManager/index.spec.tsx b/src/frontend/packages/lib_components/src/common/UploadManager/index.spec.tsx index 9d0c97ff5d..923acf9214 100644 --- a/src/frontend/packages/lib_components/src/common/UploadManager/index.spec.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadManager/index.spec.tsx @@ -109,6 +109,99 @@ describe('', () => { } }); + it('uploads the file with a parent path', async () => { + const objectType = modelName.THUMBNAILS; + const objectId = uuidv4(); + const parentType = modelName.VIDEOS; + const parentId = uuidv4(); + const file = new File(['(⌐□_□)'], 'course.jpg', { type: 'image/jpeg' }); + + const initiateUploadDeferred = new Deferred(); + const mockInitiateUpload = fetchMock.mock( + `/api/videos/${parentId}/thumbnails/${objectId}/initiate-upload/`, + initiateUploadDeferred.promise, + { method: 'POST' }, + ); + + const fileUploadDeferred = new Deferred(); + xhrMock.post( + 'https://s3.aws.example.com/', + () => fileUploadDeferred.promise, + ); + + render( + + + , + ); + + { + const { addUpload, uploadManagerState } = getLatestHookValues(); + expect(uploadManagerState).toEqual({}); + act(() => addUpload(objectType, objectId, file, parentType, parentId)); + } + { + const { uploadManagerState } = getLatestHookValues(); + expect(uploadManagerState).toEqual({ + [objectId]: { + objectId, + objectType, + file, + progress: 0, + status: UploadManagerStatus.INIT, + parentType, + parentId, + }, + }); + expect(mockInitiateUpload.calls()).toHaveLength(1); + } + { + await act(async () => { + initiateUploadDeferred.resolve({ + fields: { + key: 'foo', + }, + url: 'https://s3.aws.example.com/', + }); + // We need to wait for the upload to complete before we can check the state + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + const { uploadManagerState } = getLatestHookValues(); + expect(uploadManagerState).toEqual({ + [objectId]: { + objectId, + objectType, + file, + progress: 0, + status: UploadManagerStatus.UPLOADING, + parentType, + parentId, + }, + }); + } + { + await act(async () => { + fileUploadDeferred.resolve( + new MockResponse().body('form data body').status(204), + ); + // We need to wait for the upload to complete before we can check the state + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + const { uploadManagerState } = getLatestHookValues(); + expect(uploadManagerState).toEqual({ + [objectId]: { + objectId, + objectType, + file, + progress: 0, + status: UploadManagerStatus.SUCCESS, + parentType, + parentId, + }, + }); + } + }); + it('reports the error and does not upload to AWS when it fails to get the policy', async () => { const objectType = modelName.VIDEOS; const objectId = uuidv4(); diff --git a/src/frontend/packages/lib_components/src/common/UploadManager/index.tsx b/src/frontend/packages/lib_components/src/common/UploadManager/index.tsx index 4d482fb5e7..9a513da7d6 100644 --- a/src/frontend/packages/lib_components/src/common/UploadManager/index.tsx +++ b/src/frontend/packages/lib_components/src/common/UploadManager/index.tsx @@ -1,3 +1,4 @@ +import { Maybe } from '@lib-common/types'; import React, { createContext, useCallback, @@ -31,6 +32,12 @@ export interface UploadingObject { progress: number; status: UploadManagerStatus; message?: string; + parentType?: + | modelName.VIDEOS + | MarkdownDocumentModelName.MARKDOWN_DOCUMENTS + | ClassroomModelName.CLASSROOMS + | FileDepositoryModelName.FileDepositories; + parentId?: string; } export interface UploadManagerState { @@ -82,7 +89,7 @@ export const UploadManager = ({ Object.values(uploadManagerState) .filter(({ status }) => status === UploadManagerStatus.INIT) - .forEach(({ file, objectId, objectType }) => { + .forEach(({ file, objectId, objectType, parentId, parentType }) => { (async () => { let presignedPost: AWSPresignedPost; try { @@ -92,6 +99,8 @@ export const UploadManager = ({ file.name, file.type, file.size, + parentType, + parentId, ); } catch (error) { if ((error as ApiException).type === 'SizeError') { @@ -196,7 +205,13 @@ export const useUploadManager = () => { useContext(UploadManagerContext); const addUpload = useCallback( - (objectType: uploadableModelName, objectId: string, file: File) => { + ( + objectType: uploadableModelName, + objectId: string, + file: File, + parentType?: Maybe, + parentId?: Maybe, + ) => { setUploadState((state) => ({ ...state, [objectId]: { @@ -205,6 +220,8 @@ export const useUploadManager = () => { file, progress: 0, status: UploadManagerStatus.INIT, + parentType, + parentId, }, })); }, diff --git a/src/frontend/packages/lib_components/src/data/sideEffects/getResource/index.tsx b/src/frontend/packages/lib_components/src/data/sideEffects/getResource/index.tsx index d7c1b76d9e..c477292b0b 100644 --- a/src/frontend/packages/lib_components/src/data/sideEffects/getResource/index.tsx +++ b/src/frontend/packages/lib_components/src/data/sideEffects/getResource/index.tsx @@ -1,3 +1,6 @@ +import { Maybe } from '@lib-common/types'; + +import { UploadingObject } from '@lib-components/common'; import { fetchWrapper } from '@lib-components/common/queries/fetchWrapper'; import { addResource } from '@lib-components/data/stores/generics'; import { useJwt } from '@lib-components/hooks/stores/useJwt'; @@ -16,8 +19,13 @@ import { report } from '@lib-components/utils/errors/report'; export async function getResource( resourceName: uploadableModelName, resourceId: string, + parentType?: Maybe, + parentId?: Maybe, ): Promise { - const endpoint = `${API_ENDPOINT}/${resourceName}/${resourceId}/`; + let endpoint = `${API_ENDPOINT}/${resourceName}/${resourceId}/`; + if (parentId && parentType) { + endpoint = `${API_ENDPOINT}/${parentType}/${parentId}/${resourceName}/${resourceId}/`; + } try { const response = await fetchWrapper(endpoint, { diff --git a/src/frontend/packages/lib_components/src/data/sideEffects/initiateUpload/index.ts b/src/frontend/packages/lib_components/src/data/sideEffects/initiateUpload/index.ts index f0f4d02879..fb74d4be7d 100644 --- a/src/frontend/packages/lib_components/src/data/sideEffects/initiateUpload/index.ts +++ b/src/frontend/packages/lib_components/src/data/sideEffects/initiateUpload/index.ts @@ -1,3 +1,6 @@ +import { Maybe } from '@lib-common/types'; + +import { UploadingObject } from '@lib-components/common'; import { fetchWrapper } from '@lib-components/common/queries/fetchWrapper'; import { useJwt } from '@lib-components/hooks/stores'; import { API_ENDPOINT } from '@lib-components/settings'; @@ -10,6 +13,11 @@ import { UploadableObject } from '@lib-components/types/tracks'; * policy to authenticate this upload with S3. * @param objectType The kind of object for which we're uploading a file (model name). * @param objectId The ID of the object for which we're uploading a file. + * @param filename The name of the file we're uploading. + * @param mimetype The mimetype of the file we're uploading. + * @param size The size of the file we're uploading. + * @param parentType The kind of parent object for which we're uploading a file (nullable model name). + * @param parentId The ID of the parent object for which we're uploading a file (nullable). */ export const initiateUpload = async ( objectType: uploadableModelName, @@ -17,22 +25,25 @@ export const initiateUpload = async ( filename: string, mimetype: string, size: number, + parentType?: Maybe, + parentId?: Maybe, ) => { - const response = await fetchWrapper( - `${API_ENDPOINT}/${objectType}/${objectId}/initiate-upload/`, - { - body: JSON.stringify({ - filename, - mimetype, - size, - }), - headers: { - Authorization: `Bearer ${useJwt.getState().getJwt() ?? ''}`, - 'Content-Type': 'application/json', - }, - method: 'POST', + let input = `${API_ENDPOINT}/${objectType}/${objectId}/initiate-upload/`; + if (parentId && parentType) { + input = `${API_ENDPOINT}/${parentType}/${parentId}/${objectType}/${objectId}/initiate-upload/`; + } + const response = await fetchWrapper(input, { + body: JSON.stringify({ + filename, + mimetype, + size, + }), + headers: { + Authorization: `Bearer ${useJwt.getState().getJwt() ?? ''}`, + 'Content-Type': 'application/json', }, - ); + method: 'POST', + }); if (!response.ok) { const contentType = response.headers.get('content-type'); diff --git a/src/frontend/packages/lib_components/src/data/sideEffects/updateResource/index.ts b/src/frontend/packages/lib_components/src/data/sideEffects/updateResource/index.ts index d14ce1ae0b..1caccdb643 100644 --- a/src/frontend/packages/lib_components/src/data/sideEffects/updateResource/index.ts +++ b/src/frontend/packages/lib_components/src/data/sideEffects/updateResource/index.ts @@ -1,3 +1,6 @@ +import { Maybe } from '@lib-common/types'; + +import { UploadingObject } from '@lib-components/common'; import { fetchWrapper } from '@lib-components/common/queries/fetchWrapper'; import { useJwt } from '@lib-components/hooks/stores'; import { API_ENDPOINT } from '@lib-components/settings'; @@ -7,8 +10,13 @@ import { Resource } from '@lib-components/types/tracks'; export async function updateResource( resource: R, resourceName: uploadableModelName, + parentId?: Maybe, + parentType?: Maybe, ): Promise { - const endpoint = `${API_ENDPOINT}/${resourceName}/${resource.id}/`; + let endpoint = `${API_ENDPOINT}/${resourceName}/${resource.id}/`; + if (parentId && parentType) { + endpoint = `${API_ENDPOINT}/${parentType}/${parentId}/${resourceName}/${resource.id}/`; + } const response = await fetchWrapper(endpoint, { body: JSON.stringify(resource), diff --git a/src/frontend/packages/lib_components/src/data/stores/generics.ts b/src/frontend/packages/lib_components/src/data/stores/generics.ts index 6cf355e821..a3b7ccb1d4 100644 --- a/src/frontend/packages/lib_components/src/data/stores/generics.ts +++ b/src/frontend/packages/lib_components/src/data/stores/generics.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any */ + import { ClassroomModelName } from '@lib-components/types/apps/classroom/models'; import { FileDepositoryModelName } from '@lib-components/types/apps/deposit/models'; import { MarkdownDocumentModelName } from '@lib-components/types/apps/markdown/models'; diff --git a/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts b/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts index c95235b18e..5d4220be87 100644 --- a/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts +++ b/src/frontend/packages/lib_components/src/types/apps/classroom/models.ts @@ -128,7 +128,7 @@ export interface EndClassroomActionResponse { } export interface ClassroomDocument extends Resource { - classroom: Classroom; + classroom_id: string; filename: string; is_default: boolean; upload_state: uploadState; @@ -142,7 +142,7 @@ export type ClassroomRecordingVod = Pick< >; export interface ClassroomRecording extends Resource { - classroom: string; + classroom_id: string; video_file_url: string; started_at: string; vod: Nullable; diff --git a/src/frontend/packages/lib_components/src/types/apps/deposit/models.ts b/src/frontend/packages/lib_components/src/types/apps/deposit/models.ts index 341525a97c..6ec9a9352a 100644 --- a/src/frontend/packages/lib_components/src/types/apps/deposit/models.ts +++ b/src/frontend/packages/lib_components/src/types/apps/deposit/models.ts @@ -17,7 +17,7 @@ export interface FileDepository extends Resource { export interface DepositedFile extends Resource { author_name: string; - file_depository: FileDepository; + file_depository_id: string; filename: string; size: string; read: boolean; diff --git a/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.spec.tsx b/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.spec.tsx index c021391361..efa7396958 100644 --- a/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.spec.tsx +++ b/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.spec.tsx @@ -551,7 +551,7 @@ describe('', () => { // Drop an image const markdownImageId = '5459a5b2-2f81-11ed-ab8f-47c92ec0ac16'; fetchMock.postOnce( - `/api/markdown-images/`, + `/api/markdown-documents/1/markdown-images/`, markdownImageMockFactory({ id: markdownImageId, active_stamp: null, @@ -562,7 +562,7 @@ describe('', () => { }), ); fetchMock.postOnce( - `/api/markdown-images/${markdownImageId}/initiate-upload/`, + `/api/markdown-documents/1/markdown-images/${markdownImageId}/initiate-upload/`, { fields: { key: 'foo', @@ -589,7 +589,7 @@ describe('', () => { // Image is uploaded fetchMock.get( - `/api/markdown-images/${markdownImageId}/`, + `/api/markdown-documents/1/markdown-images/${markdownImageId}/`, markdownImageMockFactory({ id: markdownImageId, is_ready_to_show: true, diff --git a/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.tsx b/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.tsx index 275adf1d18..16e7547066 100644 --- a/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.tsx +++ b/src/frontend/packages/lib_markdown/src/components/MarkdownEditor/index.tsx @@ -125,7 +125,10 @@ export const MarkdownEditor = ({ markdownDocumentId }: MarkdownEditorProps) => { ); }; - const { addImageUpload } = useImageUploadManager(onImageUploadFinished); + const { addImageUpload } = useImageUploadManager( + markdownDocumentId, + onImageUploadFinished, + ); // note: we don't want to fetch the markdown document regularly to prevent // any editor update while the user has not saved her document. diff --git a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.spec.tsx b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.spec.tsx index 3d77fb25d0..7ae39b0c36 100644 --- a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.spec.tsx +++ b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.spec.tsx @@ -381,7 +381,7 @@ describe('', () => { const markdownText = fs.readFileSync(file, { encoding: 'utf8' }); fetchMock.getOnce( - '/api/markdown-images/981a52a2-7caf-49a5-bb36-1d8512152214/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/981a52a2-7caf-49a5-bb36-1d8512152214/`, markdownImageMockFactory({ id: '981a52a2-7caf-49a5-bb36-1d8512152214', url: 'https://s3.link/easy-to-find.png', @@ -389,7 +389,7 @@ describe('', () => { ); fetchMock.getOnce( - '/api/markdown-images/066036cc-2dde-11ed-89f4-afcf72a20b4c/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/066036cc-2dde-11ed-89f4-afcf72a20b4c/`, markdownImageMockFactory({ id: '066036cc-2dde-11ed-89f4-afcf72a20b4c', url: null, diff --git a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.tsx b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.tsx index b4aa71d05a..c824071212 100644 --- a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.tsx +++ b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/index.tsx @@ -90,7 +90,10 @@ export const MdxRenderer = ({ remarkLatexPlugin(markdownDocumentId), remarkMermaidPlugin, remarkMath, - remarkLocallyHostedImagePlugin(localImagesUrlCache.current), + remarkLocallyHostedImagePlugin( + markdownDocumentId, + localImagesUrlCache.current, + ), ]; const rehypePlugins: PluggableList = [ options?.useMathjax ? rehypeMathjax : rehypeKatex, diff --git a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/remarkLocallyHostedImagePlugin.tsx b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/remarkLocallyHostedImagePlugin.tsx index 03ea4a3ea9..aba6c03bef 100644 --- a/src/frontend/packages/lib_markdown/src/components/MdxRenderer/remarkLocallyHostedImagePlugin.tsx +++ b/src/frontend/packages/lib_markdown/src/components/MdxRenderer/remarkLocallyHostedImagePlugin.tsx @@ -11,6 +11,7 @@ import { fetchOneMarkdownImage } from '@lib-markdown/data/queries'; import { MarkdownImageCache } from './types'; const remarkLocallyHostedImagePlugin = ( + markdownDocumentId: string, localImagesUrlCache: MarkdownImageCache, ) => { // `markdownDocumentId` is mandatory to allow API calls @@ -41,7 +42,10 @@ const remarkLocallyHostedImagePlugin = ( } if (image.url.startsWith('/uploaded/image/')) { - const response = await fetchOneMarkdownImage(imageId); + const response = await fetchOneMarkdownImage( + markdownDocumentId, + imageId, + ); if (response.url) { localImagesUrlCache[imageId] = { url: response.url, diff --git a/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.spec.tsx b/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.spec.tsx index bb90e94e99..fa4f92c2bf 100644 --- a/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.spec.tsx +++ b/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.spec.tsx @@ -23,13 +23,17 @@ jest.mock('lib-components', () => ({ describe('useImageUploadManager', () => { let getLatestUseImageUploadManagerHookValues: () => any = () => {}; let getLatestUseUploadManagerHookValues: () => any = () => {}; + const markdownDocumentId = 'truc'; const onImageUploadFinished = jest.fn(); const TestComponent = () => { const uploadManager = useUploadManager(); getLatestUseUploadManagerHookValues = () => uploadManager; - const imageUploadManager = useImageUploadManager(onImageUploadFinished); + const imageUploadManager = useImageUploadManager( + markdownDocumentId, + onImageUploadFinished, + ); getLatestUseImageUploadManagerHookValues = () => imageUploadManager; return null; }; @@ -49,8 +53,8 @@ describe('useImageUploadManager', () => { const initiateUploadDeferred = new Deferred(); - const mockcreateMarkdownImage = fetchMock.postOnce( - `/api/markdown-images/`, + const mockCreateMarkdownImage = fetchMock.postOnce( + `/api/markdown-documents/${markdownDocumentId}/markdown-images/`, markdownImageMockFactory({ id: objectId, active_stamp: null, @@ -62,7 +66,7 @@ describe('useImageUploadManager', () => { ); const mockInitiateUpload = fetchMock.postOnce( - `/api/markdown-images/${objectId}/initiate-upload/`, + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${objectId}/initiate-upload/`, initiateUploadDeferred.promise, ); @@ -93,15 +97,19 @@ describe('useImageUploadManager', () => { file, progress: 0, status: UploadManagerStatus.INIT, + parentType: modelName.MARKDOWN_DOCUMENTS, + parentId: markdownDocumentId, }, }); expect( mockInitiateUpload.calls( - `/api/markdown-images/${objectId}/initiate-upload/`, + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${objectId}/initiate-upload/`, ), ).toHaveLength(1); expect( - mockcreateMarkdownImage.calls(`/api/markdown-images/`), + mockCreateMarkdownImage.calls( + `/api/markdown-documents/${markdownDocumentId}/markdown-images/`, + ), ).toHaveLength(1); } { @@ -122,6 +130,8 @@ describe('useImageUploadManager', () => { file, progress: 0, status: UploadManagerStatus.UPLOADING, + parentType: modelName.MARKDOWN_DOCUMENTS, + parentId: markdownDocumentId, }, }); expect(screen.getByRole('status')).toHaveTextContent('course.gif0%'); @@ -129,7 +139,7 @@ describe('useImageUploadManager', () => { // When status will turn to SUCCESS the image polling will start fetchMock.get( - `/api/markdown-images/${objectId}/`, + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${objectId}/`, markdownImageMockFactory({ id: objectId, is_ready_to_show: false, @@ -152,6 +162,8 @@ describe('useImageUploadManager', () => { file, progress: 0, status: UploadManagerStatus.SUCCESS, + parentType: modelName.MARKDOWN_DOCUMENTS, + parentId: markdownDocumentId, }, }); expect(screen.getByRole('status')).toHaveTextContent( @@ -162,7 +174,7 @@ describe('useImageUploadManager', () => { { act(() => { fetchMock.get( - `/api/markdown-images/${objectId}/`, + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${objectId}/`, markdownImageMockFactory({ id: objectId, is_ready_to_show: true, diff --git a/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.tsx b/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.tsx index 8a1f04d203..15a5d751fe 100644 --- a/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.tsx +++ b/src/frontend/packages/lib_markdown/src/components/useImageUploadManager/index.tsx @@ -33,6 +33,7 @@ const toasterStyle = { }; export const useImageUploadManager = ( + markdownDocumentId: string, onImageUploadFinished: (imageId: string, imageFileName: string) => void, ) => { const intl = useIntl(); @@ -55,7 +56,7 @@ export const useImageUploadManager = ( // Once the update is done, the file will be processed, we have to wait for the processing // to be done too, hence the polling. await toast.promise( - pollForMarkdownImage(imageId), + pollForMarkdownImage(markdownDocumentId, imageId), { loading: intl.formatMessage(messages.processing, { imageName: uploadingObject.file.name, @@ -83,16 +84,28 @@ export const useImageUploadManager = ( resetUpload(imageId); } }); - }, [intl, onImageUploadFinished, resetUpload, uploadManagerState]); + }, [ + intl, + markdownDocumentId, + onImageUploadFinished, + resetUpload, + uploadManagerState, + ]); const addImageUpload = useCallback( async (file: File) => { - const response = await createMarkdownImage(); + const response = await createMarkdownImage(markdownDocumentId); const markdownImageId = response.id; - addUpload(modelName.MARKDOWN_IMAGES, markdownImageId, file); + addUpload( + modelName.MARKDOWN_IMAGES, + markdownImageId, + file, + modelName.MARKDOWN_DOCUMENTS, + markdownDocumentId, + ); return markdownImageId; }, - [addUpload], + [addUpload, markdownDocumentId], ); return { addImageUpload }; diff --git a/src/frontend/packages/lib_markdown/src/data/queries/index.tsx b/src/frontend/packages/lib_markdown/src/data/queries/index.tsx index 3463bf7321..488c6bf3ab 100644 --- a/src/frontend/packages/lib_markdown/src/data/queries/index.tsx +++ b/src/frontend/packages/lib_markdown/src/data/queries/index.tsx @@ -212,10 +212,16 @@ export const markdownRenderLatex = ( }; // It has to be called outside hook context -export const fetchOneMarkdownImage = (markdownImageId: string): any => { +export const fetchOneMarkdownImage = ( + markdownDocumentId: string, + markdownImageId: string, +): any => { return fetchOne({ meta: undefined, pageParam: undefined, - queryKey: ['markdown-images', markdownImageId], + queryKey: [ + `markdown-documents/${markdownDocumentId}/markdown-images`, + markdownImageId, + ], }); }; diff --git a/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.spec.tsx b/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.spec.tsx index bc17d7b4e4..a008009193 100644 --- a/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.spec.tsx +++ b/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.spec.tsx @@ -1,9 +1,11 @@ import fetchMock from 'fetch-mock'; import { useJwt } from 'lib-components'; +import { v4 as uuidv4 } from 'uuid'; import { createMarkdownImage } from './index'; describe('createMarkdownImage', () => { + const markdownDocumentId = uuidv4(); beforeEach(() => { useJwt.setState({ jwt: 'token', @@ -13,17 +15,20 @@ describe('createMarkdownImage', () => { afterEach(() => fetchMock.restore()); it('creates a new shared live media and returns it', async () => { - fetchMock.mock('/api/markdown-images/', { - active_stamp: null, - filename: null, - id: '5570eb90-764e-4300-b92e-d3426e9046d2', - is_ready_to_show: false, - upload_state: 'pending', - url: null, - markdown_document: '72f53735-3283-456c-a562-4e1b59e2a686', - }); + fetchMock.mock( + `/api/markdown-documents/${markdownDocumentId}/markdown-images/`, + { + active_stamp: null, + filename: null, + id: '5570eb90-764e-4300-b92e-d3426e9046d2', + is_ready_to_show: false, + upload_state: 'pending', + url: null, + markdown_document: markdownDocumentId, + }, + ); - const markdownImage = await createMarkdownImage(); + const markdownImage = await createMarkdownImage(markdownDocumentId); const fetchArgs = fetchMock.lastCall()![1]!; @@ -34,7 +39,7 @@ describe('createMarkdownImage', () => { is_ready_to_show: false, upload_state: 'pending', url: null, - markdown_document: '72f53735-3283-456c-a562-4e1b59e2a686', + markdown_document: markdownDocumentId, }); expect(fetchArgs.headers).toEqual({ Authorization: 'Bearer token', @@ -45,19 +50,22 @@ describe('createMarkdownImage', () => { it('throws when it fails to create the Markdown image (request failure)', async () => { fetchMock.mock( - '/api/markdown-images/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/`, Promise.reject(new Error('Failed to perform the request')), ); - await expect(createMarkdownImage()).rejects.toThrow( + await expect(createMarkdownImage(markdownDocumentId)).rejects.toThrow( 'Failed to perform the request', ); }); it('throws when it fails to create the Markdown image (API error)', async () => { - fetchMock.mock('/api/markdown-images/', 400); + fetchMock.mock( + `/api/markdown-documents/${markdownDocumentId}/markdown-images/`, + 400, + ); - await expect(createMarkdownImage()).rejects.toThrow( + await expect(createMarkdownImage(markdownDocumentId)).rejects.toThrow( 'Failed to create a new markdown image.', ); }); diff --git a/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.ts b/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.ts index 7d19d3ae7f..4ac7a4605d 100644 --- a/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.ts +++ b/src/frontend/packages/lib_markdown/src/data/sideEffects/createMarkdownImage/index.ts @@ -8,9 +8,9 @@ import { useJwt, } from 'lib-components'; -export const createMarkdownImage = async () => { +export const createMarkdownImage = async (markdownDocumentId: string) => { const response = await fetchWrapper( - `${API_ENDPOINT}/${modelName.MARKDOWN_IMAGES}/`, + `${API_ENDPOINT}/${modelName.MARKDOWN_DOCUMENTS}/${markdownDocumentId}/${modelName.MARKDOWN_IMAGES}/`, { headers: { Authorization: `Bearer ${useJwt.getState().getJwt()}`, diff --git a/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.spec.tsx b/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.spec.tsx index 4c22cce959..0500208fc1 100644 --- a/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.spec.tsx +++ b/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.spec.tsx @@ -2,6 +2,7 @@ import { waitFor } from '@testing-library/react'; import fetchMock from 'fetch-mock'; import { markdownImageMockFactory } from 'index'; import { report } from 'lib-components'; +import { v4 as uuidv4 } from 'uuid'; import { pollForMarkdownImage } from './index'; @@ -10,6 +11,9 @@ jest.mock('lib-components', () => ({ report: jest.fn(), })); +const markdownDocumentId = uuidv4(); +const markdownImageId = uuidv4(); + describe('pollForMarkdownImage', () => { beforeEach(() => { jest.clearAllMocks(); @@ -22,10 +26,10 @@ describe('pollForMarkdownImage', () => { it('polls the image, backing off until it is ready and resolves with a success', async () => { fetchMock.mock( - '/api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, JSON.stringify( markdownImageMockFactory({ - id: 'c43f0c8f-4d3b-4219-86c3-86367b2b88cc', + id: markdownImageId, is_ready_to_show: false, }), ), @@ -33,14 +37,15 @@ describe('pollForMarkdownImage', () => { ); const promise = pollForMarkdownImage( - 'c43f0c8f-4d3b-4219-86c3-86367b2b88cc', + markdownDocumentId, + markdownImageId, 1, ); await waitFor(() => { expect( fetchMock.calls( - '/api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, { method: 'GET', }, @@ -49,11 +54,11 @@ describe('pollForMarkdownImage', () => { }); const markdownImage = markdownImageMockFactory({ - id: 'c43f0c8f-4d3b-4219-86c3-86367b2b88cc', + id: markdownImageId, is_ready_to_show: true, }); fetchMock.mock( - '/api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, JSON.stringify(markdownImage), { method: 'GET', @@ -64,7 +69,7 @@ describe('pollForMarkdownImage', () => { await waitFor(() => { expect( fetchMock.calls( - '/api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, { method: 'GET', }, @@ -77,26 +82,26 @@ describe('pollForMarkdownImage', () => { it('polls non-existing image', async () => { fetchMock.mock( - '/api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, 404, { method: 'GET' }, ); await expect(async () => { - await pollForMarkdownImage('c43f0c8f-4d3b-4219-86c3-86367b2b88cc'); + await pollForMarkdownImage(markdownDocumentId, markdownImageId); }).rejects.toThrow( - 'Failed to get /api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/.', + `Failed to get /api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/.`, ); expect(report).toHaveBeenCalledWith( Error( - 'Failed to get /api/markdown-images/c43f0c8f-4d3b-4219-86c3-86367b2b88cc/.', + `Failed to get /api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/.`, ), ); }); it('resolves with a failure and reports it when it fails to poll the image', async () => { fetchMock.mock( - '/api/markdown-images/15cf570a-5dc6-421a-9856-59e1b008a6fb/', + `/api/markdown-documents/${markdownDocumentId}/markdown-images/${markdownImageId}/`, Promise.reject(new Error('Failed to get the image')), { method: 'GET', @@ -105,7 +110,7 @@ describe('pollForMarkdownImage', () => { ); await expect(async () => { - await pollForMarkdownImage('15cf570a-5dc6-421a-9856-59e1b008a6fb'); + await pollForMarkdownImage(markdownDocumentId, markdownImageId); }).rejects.toThrow('Failed to get the image'); expect(report).toHaveBeenCalledWith(Error('Failed to get the image')); diff --git a/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.tsx b/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.tsx index a529c35e4e..c04d675a50 100644 --- a/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.tsx +++ b/src/frontend/packages/lib_markdown/src/data/sideEffects/pollForMarkdownImage/index.tsx @@ -6,12 +6,13 @@ import { MarkdownImage, report } from 'lib-components'; import { fetchOneMarkdownImage } from '@lib-markdown/data/queries'; export async function pollForMarkdownImage( - resourceId: string, + documentId: string, + imageId: string, timer = 15, counter = 1, ): Promise { try { - const image = await fetchOneMarkdownImage(resourceId); + const image = await fetchOneMarkdownImage(documentId, imageId); if (image.is_ready_to_show) { return image; @@ -19,7 +20,7 @@ export async function pollForMarkdownImage( counter++; timer = timer * counter; await new Promise((resolve) => window.setTimeout(resolve, 100 * timer)); - return await pollForMarkdownImage(resourceId, timer, counter); + return await pollForMarkdownImage(documentId, imageId, timer, counter); } } catch (error) { report(error); diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.spec.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.spec.tsx index 6ae7a99690..77f53d972a 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.spec.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.spec.tsx @@ -174,6 +174,8 @@ describe('', () => { modelName.TIMEDTEXTTRACKS, mockTimedTextTrack.id, file, + modelName.VIDEOS, + mockedVideo.id, ), ); }); diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.tsx index fcaf76a55c..07deddf29d 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/LocalizedTimedTextTrackUpload/index.tsx @@ -103,6 +103,8 @@ export const LocalizedTimedTextTrackUpload = ({ modelName.TIMEDTEXTTRACKS, timedTextTrackId, event.target.files[0], + modelName.VIDEOS, + video.id, ); } catch (error) { if ((error as object).hasOwnProperty('size') && metadata.data) { diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.spec.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.spec.tsx index 08a0d23519..519e9557ff 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.spec.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.spec.tsx @@ -153,6 +153,8 @@ describe('', () => { modelName.SHAREDLIVEMEDIAS, mockedSharedLiveMedia.id, file, + modelName.VIDEOS, + mockedVideo.id, ); }); @@ -386,6 +388,8 @@ describe('', () => { modelName.SHAREDLIVEMEDIAS, mockedSharedLiveMedia.id, file, + modelName.VIDEOS, + mockedVideo.id, ); }); diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.tsx index 4506bd9c24..637ec548d5 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/SharedLiveMedia/index.tsx @@ -107,6 +107,8 @@ export const SharedLiveMedia = ({ isLive, isTeacher }: SharedMediaProps) => { modelName.SHAREDLIVEMEDIAS, sharedLiveMediaId, event.target.files[0], + modelName.VIDEOS, + video.id, ); } }; diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.spec.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.spec.tsx index a459ab957d..beb0a0280a 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.spec.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.spec.tsx @@ -149,6 +149,8 @@ describe('', () => { modelName.THUMBNAILS, mockedThumbnail.id, file, + modelName.VIDEOS, + mockedVideo.id, ); }); diff --git a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.tsx b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.tsx index 32771c5ec3..d2e7331e96 100644 --- a/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.tsx +++ b/src/frontend/packages/lib_video/src/components/common/VideoWidgetProvider/widgets/WidgetThumbnail/index.tsx @@ -93,7 +93,13 @@ export const WidgetThumbnail = ({ isLive = true }: WidgetThumbnailProps) => { } else { thumbnailId = thumbnail.id; } - addUpload(modelName.THUMBNAILS, thumbnailId, event.target.files[0]); + addUpload( + modelName.THUMBNAILS, + thumbnailId, + event.target.files[0], + modelName.VIDEOS, + video.id, + ); } catch (error) { if ( (error as object).hasOwnProperty('size') &&