chore: migration fixes

This commit is contained in:
NarayanBavisetti 2024-06-04 17:45:42 +05:30
parent 1c3a27deaa
commit bd58931690
12 changed files with 257 additions and 48 deletions

View File

@ -22,7 +22,7 @@ from plane.db.models import (
IssueProperty, IssueProperty,
Module, Module,
Project, Project,
ProjectDeployBoard, DeployBoard,
ProjectMember, ProjectMember,
State, State,
Workspace, Workspace,
@ -99,7 +99,7 @@ class ProjectAPIEndpoint(BaseAPIView):
) )
.annotate( .annotate(
is_deployed=Exists( is_deployed=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
) )

View File

@ -30,7 +30,7 @@ from .project import (
ProjectIdentifierSerializer, ProjectIdentifierSerializer,
ProjectLiteSerializer, ProjectLiteSerializer,
ProjectMemberLiteSerializer, ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer, DeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer, ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer, ProjectMemberRoleSerializer,

View File

@ -13,7 +13,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
ProjectMemberInvite, ProjectMemberInvite,
ProjectIdentifier, ProjectIdentifier,
ProjectDeployBoard, DeployBoard,
ProjectPublicMember, ProjectPublicMember,
) )
@ -206,14 +206,14 @@ class ProjectMemberLiteSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
class ProjectDeployBoardSerializer(BaseSerializer): class DeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project") project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace" read_only=True, source="workspace"
) )
class Meta: class Meta:
model = ProjectDeployBoard model = DeployBoard
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"workspace", "workspace",

View File

@ -2,6 +2,7 @@ from django.urls import path
from plane.app.views import ( from plane.app.views import (
ProjectViewSet, ProjectViewSet,
DeployBoardViewSet,
ProjectInvitationsViewset, ProjectInvitationsViewset,
ProjectMemberViewSet, ProjectMemberViewSet,
ProjectMemberUserEndpoint, ProjectMemberUserEndpoint,
@ -12,7 +13,6 @@ from plane.app.views import (
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
UserProjectInvitationsViewset, UserProjectInvitationsViewset,
ProjectPublicCoverImagesEndpoint, ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectRolesEndpoint, UserProjectRolesEndpoint,
ProjectArchiveUnarchiveEndpoint, ProjectArchiveUnarchiveEndpoint,
) )
@ -157,7 +157,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view( DeployBoardViewSet.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
@ -167,7 +167,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view( DeployBoardViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
"patch": "partial_update", "patch": "partial_update",

View File

@ -4,7 +4,7 @@ from .project.base import (
ProjectUserViewsEndpoint, ProjectUserViewsEndpoint,
ProjectFavoritesViewSet, ProjectFavoritesViewSet,
ProjectPublicCoverImagesEndpoint, ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet, DeployBoardViewSet,
ProjectArchiveUnarchiveEndpoint, ProjectArchiveUnarchiveEndpoint,
) )

View File

@ -28,7 +28,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.serializers import ( from plane.app.serializers import (
ProjectSerializer, ProjectSerializer,
ProjectListSerializer, ProjectListSerializer,
ProjectDeployBoardSerializer, DeployBoardSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
@ -46,7 +46,7 @@ from plane.db.models import (
Module, Module,
Cycle, Cycle,
Inbox, Inbox,
ProjectDeployBoard, DeployBoard,
IssueProperty, IssueProperty,
Issue, Issue,
) )
@ -138,7 +138,7 @@ class ProjectViewSet(BaseViewSet):
) )
.annotate( .annotate(
is_deployed=Exists( is_deployed=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
) )
@ -639,12 +639,12 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
return Response(files, status=status.HTTP_200_OK) return Response(files, status=status.HTTP_200_OK)
class ProjectDeployBoardViewSet(BaseViewSet): class DeployBoardViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectMemberPermission, ProjectMemberPermission,
] ]
serializer_class = ProjectDeployBoardSerializer serializer_class = DeployBoardSerializer
model = ProjectDeployBoard model = DeployBoard
def get_queryset(self): def get_queryset(self):
return ( return (
@ -673,7 +673,7 @@ class ProjectDeployBoardViewSet(BaseViewSet):
}, },
) )
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create( project_deploy_board, _ = DeployBoard.objects.get_or_create(
anchor=f"{slug}/{project_id}", anchor=f"{slug}/{project_id}",
project_id=project_id, project_id=project_id,
) )
@ -685,5 +685,5 @@ class ProjectDeployBoardViewSet(BaseViewSet):
project_deploy_board.save() project_deploy_board.save()
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = DeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -0,0 +1,156 @@
# # Generated by Django 4.2.7 on 2024-05-24 09:47
# Python imports
import uuid
from uuid import uuid4
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import plane.db.models.deploy_board
def populate_deploy_board(apps, schema_editor):
DeployBoard = apps.get_model("db", "DeployBoard")
ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard")
DeployBoard.objects.bulk_create(
[
DeployBoard(
entity_identifier=deploy_board.project_id,
project_id=deploy_board.project_id,
entity_name="project",
anchor=uuid4().hex,
comments=deploy_board.comments,
reactions=deploy_board.reactions,
inbox=deploy_board.inbox,
votes=deploy_board.votes,
view_props=deploy_board.views,
workspace_id=deploy_board.workspace_id,
created_at=deploy_board.created_at,
updated_at=deploy_board.updated_at,
created_by_id=deploy_board.created_by_id,
updated_by_id=deploy_board.updated_by_id,
)
for deploy_board in ProjectDeployBoard.objects.all()
],
batch_size=100,
)
class Migration(migrations.Migration):
dependencies = [
("db", "0066_account_id_token_cycle_logo_props_module_logo_props"),
]
operations = [
migrations.CreateModel(
name="DeployBoard",
fields=[
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Created At"
),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("entity_identifier", models.UUIDField(null=True)),
(
"entity_name",
models.CharField(
choices=[
("project", "Project"),
("issue", "Issue"),
("module", "Module"),
("cycle", "Task"),
("page", "Page"),
("view", "View"),
],
max_length=30,
),
),
(
"anchor",
models.CharField(
db_index=True,
default=plane.db.models.deploy_board.get_anchor,
max_length=255,
unique=True,
),
),
("comments", models.BooleanField(default=False)),
("reactions", models.BooleanField(default=False)),
("votes", models.BooleanField(default=False)),
("view_props", models.JSONField(default=dict)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"inbox",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="board_inbox",
to="db.inbox",
),
),
(
"project",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="project_%(class)s",
to="db.project",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_%(class)s",
to="db.workspace",
),
),
],
options={
"verbose_name": "Deploy Board",
"verbose_name_plural": "Deploy Boards",
"db_table": "deploy_boards",
"ordering": ("-created_at",),
"unique_together": {("entity_name", "entity_identifier")},
},
),
migrations.RunPython(populate_deploy_board),
]

View File

@ -53,13 +53,13 @@ from .page import Page, PageFavorite, PageLabel, PageLog
from .project import ( from .project import (
Project, Project,
ProjectBaseModel, ProjectBaseModel,
ProjectDeployBoard,
ProjectFavorite, ProjectFavorite,
ProjectIdentifier, ProjectIdentifier,
ProjectMember, ProjectMember,
ProjectMemberInvite, ProjectMemberInvite,
ProjectPublicMember, ProjectPublicMember,
) )
from .deploy_board import DeployBoard
from .session import Session from .session import Session
from .social_connection import SocialLoginConnection from .social_connection import SocialLoginConnection
from .state import State from .state import State

View File

@ -0,0 +1,53 @@
# Python imports
from uuid import uuid4
# Django imports
from django.db import models
# Module imports
from .workspace import WorkspaceBaseModel
def get_anchor():
return uuid4().hex
class DeployBoard(WorkspaceBaseModel):
TYPE_CHOICES = (
("project", "Project"),
("issue", "Issue"),
("module", "Module"),
("cycle", "Task"),
("page", "Page"),
("view", "View"),
)
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(
max_length=30,
choices=TYPE_CHOICES,
)
anchor = models.CharField(
max_length=255, default=get_anchor, unique=True, db_index=True
)
comments = models.BooleanField(default=False)
reactions = models.BooleanField(default=False)
inbox = models.ForeignKey(
"db.Inbox",
related_name="board_inbox",
on_delete=models.SET_NULL,
null=True,
)
votes = models.BooleanField(default=False)
view_props = models.JSONField(default=dict)
def __str__(self):
"""Return name of the deploy board"""
return f"{self.entity_identifier} <{self.entity_name}>"
class Meta:
unique_together = ["entity_name", "entity_identifier"]
verbose_name = "Deploy Board"
verbose_name_plural = "Deploy Boards"
db_table = "deploy_boards"
ordering = ("-created_at",)

View File

@ -18,7 +18,7 @@ from plane.db.models import (
State, State,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
ProjectDeployBoard, DeployBoard,
) )
from plane.app.serializers import ( from plane.app.serializers import (
IssueSerializer, IssueSerializer,
@ -39,7 +39,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
] ]
def get_queryset(self): def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -59,7 +59,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return InboxIssue.objects.none() return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id): def list(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -118,7 +118,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id, inbox_id): def create(self, request, slug, project_id, inbox_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -189,7 +189,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk): def partial_update(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -256,7 +256,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
) )
def retrieve(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:
@ -280,7 +280,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk): def destroy(self, request, slug, project_id, inbox_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if project_deploy_board.inbox is None: if project_deploy_board.inbox is None:

View File

@ -44,7 +44,7 @@ from plane.db.models import (
ProjectMember, ProjectMember,
IssueReaction, IssueReaction,
CommentReaction, CommentReaction,
ProjectDeployBoard, DeployBoard,
IssueVote, IssueVote,
ProjectPublicMember, ProjectPublicMember,
) )
@ -76,7 +76,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -103,11 +103,11 @@ class IssueCommentPublicViewSet(BaseViewSet):
.distinct() .distinct()
).order_by("created_at") ).order_by("created_at")
return IssueComment.objects.none() return IssueComment.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueComment.objects.none() return IssueComment.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -151,7 +151,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, issue_id, pk): def partial_update(self, request, slug, project_id, issue_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -184,7 +184,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, issue_id, pk): def destroy(self, request, slug, project_id, issue_id, pk):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -221,7 +221,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -236,11 +236,11 @@ class IssueReactionPublicViewSet(BaseViewSet):
.distinct() .distinct()
) )
return IssueReaction.objects.none() return IssueReaction.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueReaction.objects.none() return IssueReaction.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -280,7 +280,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, issue_id, reaction_code): def destroy(self, request, slug, project_id, issue_id, reaction_code):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -319,7 +319,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -334,11 +334,11 @@ class CommentReactionPublicViewSet(BaseViewSet):
.distinct() .distinct()
) )
return CommentReaction.objects.none() return CommentReaction.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return CommentReaction.objects.none() return CommentReaction.objects.none()
def create(self, request, slug, project_id, comment_id): def create(self, request, slug, project_id, comment_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
@ -380,7 +380,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, comment_id, reaction_code): def destroy(self, request, slug, project_id, comment_id, reaction_code):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
if not project_deploy_board.reactions: if not project_deploy_board.reactions:
@ -421,7 +421,7 @@ class IssueVotePublicViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
try: try:
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
@ -434,7 +434,7 @@ class IssueVotePublicViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
) )
return IssueVote.objects.none() return IssueVote.objects.none()
except ProjectDeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return IssueVote.objects.none() return IssueVote.objects.none()
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
@ -513,7 +513,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
if not ProjectDeployBoard.objects.filter( if not DeployBoard.objects.filter(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
).exists(): ).exists():
return Response( return Response(

View File

@ -11,10 +11,10 @@ from rest_framework.permissions import AllowAny
# Module imports # Module imports
from .base import BaseAPIView from .base import BaseAPIView
from plane.app.serializers import ProjectDeployBoardSerializer from plane.app.serializers import DeployBoardSerializer
from plane.db.models import ( from plane.db.models import (
Project, Project,
ProjectDeployBoard, DeployBoard,
) )
@ -24,10 +24,10 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
] ]
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project_deploy_board = ProjectDeployBoard.objects.get( project_deploy_board = DeployBoard.objects.get(
workspace__slug=slug, project_id=project_id workspace__slug=slug, project_id=project_id
) )
serializer = ProjectDeployBoardSerializer(project_deploy_board) serializer = DeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -41,7 +41,7 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
Project.objects.filter(workspace__slug=slug) Project.objects.filter(workspace__slug=slug)
.annotate( .annotate(
is_public=Exists( is_public=Exists(
ProjectDeployBoard.objects.filter( DeployBoard.objects.filter(
workspace__slug=slug, project_id=OuterRef("pk") workspace__slug=slug, project_id=OuterRef("pk")
) )
) )