Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/api/plane/db/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ def asset_url(self):
self.EntityTypeContext.PAGE_DESCRIPTION,
self.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
]:
# Description assets are not always project-bound. Workspace-level
# pages have no project (Page relates to projects through a M2M),
# and WorkspaceFileAssetEndpoint never sets project_id, so the
# column is NULL for those assets. Interpolating it would emit
# `projects/None/`, which 404s. Fall back to the workspace-scoped
# route instead, mirroring the two branches the clients' own URL
# builder (`getEditorAssetSrc`) already picks between.
if self.project_id is None:
return f"/api/assets/v2/workspaces/{self.workspace.slug}/{self.id}/"
return f"/api/assets/v2/workspaces/{self.workspace.slug}/projects/{self.project_id}/{self.id}/"

return None
83 changes: 83 additions & 0 deletions apps/api/plane/tests/unit/models/test_file_asset_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

"""Unit tests for ``FileAsset.asset_url``.

Regression coverage for #9564: description assets uploaded through
``WorkspaceFileAssetEndpoint`` are created without a ``project_id`` (the
endpoint only maps the entity id, and workspace-level pages have no project at
all), so ``asset_url`` used to build ``.../projects/None/<id>/``, which 404s.
Those assets must fall back to the workspace-scoped route.
"""

import pytest

from plane.db.models import FileAsset, Project

DESCRIPTION_ENTITY_TYPES = [
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
FileAsset.EntityTypeContext.COMMENT_DESCRIPTION,
FileAsset.EntityTypeContext.PAGE_DESCRIPTION,
FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION,
]


@pytest.fixture
def project(workspace, create_user):
"""A project inside the fixture workspace."""
return Project.objects.create(
name="Test Project",
identifier="TP",
workspace=workspace,
created_by=create_user,
)


@pytest.mark.unit
class TestFileAssetUrl:
"""Test the ``asset_url`` property of the FileAsset model"""

@pytest.mark.django_db
@pytest.mark.parametrize("entity_type", DESCRIPTION_ENTITY_TYPES)
def test_description_asset_without_project_uses_workspace_route(self, workspace, entity_type):
"""Description assets with no project resolve to the workspace-scoped route"""
asset = FileAsset.objects.create(
attributes={"name": "image.png", "type": "image/png", "size": 1024},
asset=f"{workspace.id}/image.png",
size=1024,
workspace=workspace,
entity_type=entity_type,
)

assert asset.project_id is None
assert asset.asset_url == f"/api/assets/v2/workspaces/{workspace.slug}/{asset.id}/"
assert "None" not in asset.asset_url

@pytest.mark.django_db
@pytest.mark.parametrize("entity_type", DESCRIPTION_ENTITY_TYPES)
def test_description_asset_with_project_uses_project_route(self, workspace, project, entity_type):
"""Description assets bound to a project keep the project-scoped route"""
asset = FileAsset.objects.create(
attributes={"name": "image.png", "type": "image/png", "size": 1024},
asset=f"{workspace.id}/image.png",
size=1024,
workspace=workspace,
project=project,
entity_type=entity_type,
)

assert asset.asset_url == f"/api/assets/v2/workspaces/{workspace.slug}/projects/{project.id}/{asset.id}/"

@pytest.mark.django_db
def test_workspace_level_asset_uses_static_route(self, workspace):
"""Workspace-level assets are unaffected by the project fallback"""
asset = FileAsset.objects.create(
attributes={"name": "logo.png", "type": "image/png", "size": 1024},
asset=f"{workspace.id}/logo.png",
size=1024,
workspace=workspace,
entity_type=FileAsset.EntityTypeContext.WORKSPACE_LOGO,
)

assert asset.asset_url == f"/api/assets/v2/static/{asset.id}/"