From 9ee599005b9924d27040fd1f94c1f6c74aa74cbe Mon Sep 17 00:00:00 2001 From: Rodrigo Mendez <117670175+rodmgwgu@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:48:33 -0600 Subject: [PATCH] fix: always return an absolute url in libraries backup endpoint (#37508) The 'url' field on the GET /api/libraries/v2/{library_id}/backup/?task_id={task_id} endpoint was returning realtive paths when the file was stored on the default FileSystemStorage backend, which makes it inconsistent with other storage backends and semantically incorrect. This commit addresses this making sure it always returns an absolute url. --- .../core/djangoapps/content_libraries/api/libraries.py | 6 +++--- .../djangoapps/content_libraries/rest_api/libraries.py | 4 ++-- .../djangoapps/content_libraries/rest_api/serializers.py | 2 +- .../core/djangoapps/content_libraries/tests/test_api.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/api/libraries.py b/openedx/core/djangoapps/content_libraries/api/libraries.py index 8ad0930660..0d07889fab 100644 --- a/openedx/core/djangoapps/content_libraries/api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/api/libraries.py @@ -681,7 +681,7 @@ def get_backup_task_status( Returns a dictionary with the following keys: - state: One of "Pending", "Exporting", "Succeeded", "Failed" - - url: If state is "Succeeded", the URL where the exported .zip file can be downloaded. Otherwise, None. + - file: If state is "Succeeded", the FileField of the exported .zip. Otherwise, None. If no task is found, returns None. """ @@ -690,10 +690,10 @@ def get_backup_task_status( except UserTaskStatus.DoesNotExist: return None - result = {'state': task_status.state, 'url': None} + result = {'state': task_status.state, 'file': None} if task_status.state == UserTaskStatus.SUCCEEDED: artifact = UserTaskArtifact.objects.get(status=task_status, name='Output') - result['url'] = artifact.file.storage.url(artifact.file.name) + result['file'] = artifact.file return result diff --git a/openedx/core/djangoapps/content_libraries/rest_api/libraries.py b/openedx/core/djangoapps/content_libraries/rest_api/libraries.py index 1acdf7bb11..28329cbe77 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/libraries.py @@ -786,8 +786,8 @@ class LibraryBackupView(APIView): if not result: raise NotFound(detail="No backup found for this library.") - - return Response(LibraryBackupTaskStatusSerializer(result).data) + # Passing request context to the serializer so the url absolute path is correctly generated + return Response(LibraryBackupTaskStatusSerializer(result, context={'request': request}).data) # LTI 1.3 Views diff --git a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py index 56b8963b71..5f816d16e4 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py @@ -425,4 +425,4 @@ class LibraryBackupTaskStatusSerializer(serializers.Serializer): Serializer for checking the status of a library backup task. """ state = serializers.CharField() - url = serializers.URLField(allow_null=True) + url = serializers.FileField(source='file', allow_null=True, use_url=True) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_api.py b/openedx/core/djangoapps/content_libraries/tests/test_api.py index 611f06871c..670d630e5a 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_api.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_api.py @@ -1430,7 +1430,7 @@ class ContentLibraryExportTest(ContentLibrariesRestApiTest): status = api.get_backup_task_status(self.user.id, task_id=task_id) assert status is not None assert status['state'] == UserTaskStatus.IN_PROGRESS - assert status['url'] is None + assert status['file'] is None def test_get_backup_task_status_succeeded(self) -> None: # Create a mock UserTaskStatus in SUCCEEDED state @@ -1444,7 +1444,7 @@ class ContentLibraryExportTest(ContentLibrariesRestApiTest): # Create a mock UserTaskArtifact mock_artifact = mock.Mock() - mock_artifact.file.storage.url.return_value = "/media/user_tasks/2025/10/01/library-libOEXCSPROB_mOw1rPL.zip" + mock_artifact.file.url = "/media/user_tasks/2025/10/01/library-libOEXCSPROB_mOw1rPL.zip" with mock.patch( 'openedx.core.djangoapps.content_libraries.api.libraries.UserTaskStatus.objects.get' @@ -1458,7 +1458,7 @@ class ContentLibraryExportTest(ContentLibrariesRestApiTest): status = api.get_backup_task_status(self.user.id, task_id=task_id) assert status is not None assert status['state'] == UserTaskStatus.SUCCEEDED - assert status['url'] == "/media/user_tasks/2025/10/01/library-libOEXCSPROB_mOw1rPL.zip" + assert status['file'].url == "/media/user_tasks/2025/10/01/library-libOEXCSPROB_mOw1rPL.zip" def test_get_backup_task_status_failed(self) -> None: # Create a mock UserTaskStatus in FAILED state @@ -1478,4 +1478,4 @@ class ContentLibraryExportTest(ContentLibrariesRestApiTest): status = api.get_backup_task_status(self.user.id, task_id=task_id) assert status is not None assert status['state'] == UserTaskStatus.FAILED - assert status['url'] is None + assert status['file'] is None