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.
This commit is contained in:
Rodrigo Mendez
2025-10-20 17:48:33 -06:00
committed by GitHub
parent 3f5ac6ddbc
commit 9ee599005b
4 changed files with 10 additions and 10 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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