feat: project public boards (#1772)

* feat: project public boards

* dev: public issue and comment reactions

* dev: public issue comments

* dev: public comments

* dev: inbox for public boards

* dev: inbox issues for public board

* dev: public inbox issue

* dev: migrations

* dev: update api endpoints

* dev: project boards and views

* dev: state and label details

* dev: public issue voting

* dev: issue voting

* dev: workspace details

* dev: project icon and emoji
This commit is contained in:
Nikhil 2023-08-11 19:27:44 +05:30 committed by GitHub
parent 079a5b28d8
commit feba1cc4d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2120 additions and 19 deletions

View File

@ -18,6 +18,7 @@ from .project import (
ProjectFavoriteSerializer,
ProjectLiteSerializer,
ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
)
from .state import StateSerializer, StateLiteSerializer
@ -42,6 +43,7 @@ from .issue import (
IssueSubscriberSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
)
from .module import (

View File

@ -31,6 +31,7 @@ from plane.db.models import (
IssueAttachment,
IssueReaction,
CommentReaction,
IssueVote,
)
@ -554,6 +555,14 @@ class CommentReactionSerializer(BaseSerializer):
class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
read_only_fields = fields
class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
@ -573,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"access",
]

View File

@ -14,6 +14,7 @@ from plane.db.models import (
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
)
@ -80,7 +81,14 @@ class ProjectSerializer(BaseSerializer):
class ProjectLiteSerializer(BaseSerializer):
class Meta:
model = Project
fields = ["id", "identifier", "name"]
fields = [
"id",
"identifier",
"name",
"cover_image",
"icon_prop",
"emoji",
]
read_only_fields = fields
@ -116,7 +124,6 @@ class ProjectMemberAdminSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True)
member = UserAdminLiteSerializer(read_only=True)
class Meta:
model = ProjectMember
fields = "__all__"
@ -149,8 +156,6 @@ class ProjectFavoriteSerializer(BaseSerializer):
]
class ProjectMemberLiteSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)
is_subscribed = serializers.BooleanField(read_only=True)
@ -159,3 +164,16 @@ class ProjectMemberLiteSerializer(BaseSerializer):
model = ProjectMember
fields = ["member", "id", "is_subscribed"]
read_only_fields = fields
class ProjectDeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
class Meta:
model = ProjectDeployBoard
fields = "__all__"
read_only_fields = [
"workspace",
"project" "anchor",
]

View File

@ -86,6 +86,7 @@ from plane.api.views import (
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
IssueReactionViewSet,
CommentReactionViewSet,
ExportIssuesEndpoint,
@ -165,6 +166,15 @@ from plane.api.views import (
NotificationViewSet,
UnreadNotificationEndpoint,
## End Notification
# Public Boards
ProjectDeployBoardViewSet,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectDeployBoardPublicSettingsEndpoint,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
## End Public Boards
)
@ -1481,4 +1491,128 @@ urlpatterns = [
name="unread-notifications",
),
## End Notification
# Public Boards
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
name="project-deploy-board-settings",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentPublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
IssueVotePublicViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="issue-vote-project-board",
),
## End Public Boards
]

View File

@ -12,6 +12,9 @@ from .project import (
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
)
from .user import (
@ -75,8 +78,12 @@ from .issue import (
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
CommentReactionViewSet,
IssueReactionViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
ExportIssuesEndpoint
)
@ -145,7 +152,7 @@ from .estimate import (
from .release import ReleaseNotesEndpoint
from .inbox import InboxViewSet, InboxIssueViewSet
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .analytic import (
AnalyticsEndpoint,

View File

@ -15,7 +15,6 @@ from sentry_sdk import capture_exception
from .base import BaseViewSet
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
from plane.db.models import (
Project,
Inbox,
InboxIssue,
Issue,
@ -23,6 +22,7 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
ProjectMember,
ProjectDeployBoard,
)
from plane.api.serializers import (
IssueSerializer,
@ -377,4 +377,269 @@ class InboxIssueViewSet(BaseViewSet):
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
)
class InboxIssuePublicViewSet(BaseViewSet):
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = [
"status",
]
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
if project_deploy_board is not None:
return self.filter_queryset(
super()
.get_queryset()
.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("issue", "workspace", "project")
)
else:
return InboxIssue.objects.none()
def list(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.objects.filter(
issue_inbox__inbox_id=inbox_id,
workspace__slug=slug,
project_id=project_id,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.prefetch_related(
Prefetch(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
)
)
issues_data = IssueStateInboxSerializer(issues, many=True).data
return Response(
issues_data,
status=status.HTTP_200_OK,
)
except ProjectDeployBoard.DoesNotExist:
return Response({"error": "Project Deploy Board does not exist"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id, inbox_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Check for valid priority
if not request.data.get("issue", {}).get("priority", None) in [
"low",
"medium",
"high",
"urgent",
None,
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
)
# create an issue
issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"),
description=request.data.get("issue", {}).get("description", {}),
description_html=request.data.get("issue", {}).get(
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "low"),
project_id=project_id,
state=state,
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
)
# create an inbox issue
InboxIssue.objects.create(
inbox_id=inbox_id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
# Get the project member
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
# Get issue data
issue_data = request.data.pop("issue", False)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
# viewers and guests since only viewers and guests
issue_data = {
"name": issue_data.get("name", issue.name),
"description_html": issue_data.get("description_html", issue.description_html),
"description": issue_data.get("description", issue.description)
}
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
current_instance = issue
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
issue_activity.delay(
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
)
issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK)
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except InboxIssue.DoesNotExist:
return Response(
{"error": "Inbox Issue does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, inbox_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
if project_deploy_board.inbox is None:
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
if str(inbox_issue.created_by_id) != str(request.user.id):
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except InboxIssue.DoesNotExist:
return Response({"error": "Inbox Issue does not exists"}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -48,6 +48,7 @@ from plane.api.serializers import (
ProjectMemberLiteSerializer,
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
)
from plane.api.permissions import (
WorkspaceEntityPermission,
@ -70,6 +71,8 @@ from plane.db.models import (
ProjectMember,
IssueReaction,
CommentReaction,
ProjectDeployBoard,
IssueVote,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@ -1457,6 +1460,374 @@ class CommentReactionViewSet(BaseViewSet):
)
class IssueCommentPublicViewSet(BaseViewSet):
serializer_class = IssueCommentSerializer
model = IssueComment
filterset_fields = [
"issue__id",
"workspace__id",
]
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
if project_deploy_board.comments:
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(issue_id=self.kwargs.get("issue_id"))
.select_related("project")
.select_related("workspace")
.select_related("issue")
.distinct()
)
else:
return IssueComment.objects.none()
def create(self, request, slug, project_id, issue_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.comments:
return Response(
{"error": "Comments are not enabled for this project"},
status=status.HTTP_400_BAD_REQUEST,
)
access = (
"INTERNAL"
if ProjectMember.objects.filter(
project_id=project_id, member=request.user
).exists()
else "EXTERNAL"
)
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id,
issue_id=issue_id,
actor=request.user,
access=access,
)
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def partial_update(self, request, slug, project_id, issue_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.comments:
return Response(
{"error": "Comments are not enabled for this project"},
status=status.HTTP_400_BAD_REQUEST,
)
comment = IssueComment.objects.get(
workspace__slug=slug, pk=pk, actor=request.user
)
serializer = IssueCommentSerializer(
comment, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
type="comment.activity.updated",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
return Response(
{"error": "IssueComent Does not exists"},
status=status.HTTP_400_BAD_REQUEST,)
def destroy(self, request, slug, project_id, issue_id, pk):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.comments:
return Response(
{"error": "Comments are not enabled for this project"},
status=status.HTTP_400_BAD_REQUEST,
)
comment = IssueComment.objects.get(
workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user
)
issue_activity.delay(
type="comment.activity.deleted",
requested_data=json.dumps({"comment_id": str(pk)}),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueCommentSerializer(comment).data,
cls=DjangoJSONEncoder,
),
)
comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist):
return Response(
{"error": "IssueComent Does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueReactionPublicViewSet(BaseViewSet):
serializer_class = IssueReactionSerializer
model = IssueReaction
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
if project_deploy_board.reactions:
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.order_by("-created_at")
.distinct()
)
else:
return IssueReaction.objects.none()
def create(self, request, slug, project_id, issue_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.reactions:
return Response(
{"error": "Reactions are not enabled for this project board"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = IssueReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, issue_id=issue_id, actor=request.user
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project board does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, issue_id, reaction_code):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.reactions:
return Response(
{"error": "Reactions are not enabled for this project board"},
status=status.HTTP_400_BAD_REQUEST,
)
issue_reaction = IssueReaction.objects.get(
workspace__slug=slug,
issue_id=issue_id,
reaction=reaction_code,
actor=request.user,
)
issue_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueReaction.DoesNotExist:
return Response(
{"error": "Issue reaction does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CommentReactionPublicViewSet(BaseViewSet):
serializer_class = CommentReactionSerializer
model = CommentReaction
def get_queryset(self):
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
if project_deploy_board.reactions:
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(comment_id=self.kwargs.get("comment_id"))
.order_by("-created_at")
.distinct()
)
else:
return CommentReaction.objects.none()
def create(self, request, slug, project_id, comment_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.reactions:
return Response(
{"error": "Reactions are not enabled for this board"},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, comment_id=comment_id, actor=request.user
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project board does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, comment_id, reaction_code):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
if not project_deploy_board.reactions:
return Response(
{"error": "Reactions are not enabled for this board"},
status=status.HTTP_400_BAD_REQUEST,
)
comment_reaction = CommentReaction.objects.get(
project_id=project_id,
workspace__slug=slug,
comment_id=comment_id,
reaction=reaction_code,
actor=request.user,
)
comment_reaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except CommentReaction.DoesNotExist:
return Response(
{"error": "Comment reaction does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueVotePublicViewSet(BaseViewSet):
model = IssueVote
serializer_class = IssueVoteSerializer
def get_queryset(self):
return (
super()
.get_queryset()
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
)
def create(self, request, slug, project_id, issue_id):
try:
issue_vote, _ = IssueVote.objects.get_or_create(
actor_id=request.user.id,
project_id=project_id,
issue_id=issue_id,
vote=request.data.get("vote", 1),
)
serializer = IssueVoteSerializer(issue_vote)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, slug, project_id, issue_id):
try:
issue_vote = IssueVote.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
actor_id=request.user.id,
)
issue_vote.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ExportIssuesEndpoint(BaseAPIView):
permission_classes = [

View File

@ -5,7 +5,21 @@ from datetime import datetime
# Django imports
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery
from django.db.models import (
Q,
Exists,
OuterRef,
Func,
F,
Max,
CharField,
Func,
Subquery,
Prefetch,
When,
Case,
Value,
)
from django.core.validators import validate_email
from django.conf import settings
@ -13,6 +27,7 @@ from django.conf import settings
from rest_framework.response import Response
from rest_framework import status
from rest_framework import serializers
from rest_framework.permissions import AllowAny
from sentry_sdk import capture_exception
# Module imports
@ -23,10 +38,16 @@ from plane.api.serializers import (
ProjectDetailSerializer,
ProjectMemberInviteSerializer,
ProjectFavoriteSerializer,
IssueLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
)
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.api.permissions import (
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
)
from plane.db.models import (
Project,
@ -49,9 +70,17 @@ from plane.db.models import (
IssueAssignee,
ModuleMember,
Inbox,
ProjectDeployBoard,
Issue,
IssueReaction,
IssueLink,
IssueAttachment,
Label,
)
from plane.bgtasks.project_invitation_task import project_invitation
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
class ProjectViewSet(BaseViewSet):
@ -993,6 +1022,63 @@ class ProjectFavoritesViewSet(BaseViewSet):
)
class ProjectDeployBoardViewSet(BaseViewSet):
permission_classes = [
ProjectMemberPermission,
]
serializer_class = ProjectDeployBoardSerializer
model = ProjectDeployBoard
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
.select_related("project")
)
def create(self, request, slug, project_id):
try:
comments = request.data.get("comments", False)
reactions = request.data.get("reactions", False)
inbox = request.data.get("inbox", None)
votes = request.data.get("votes", False)
views = request.data.get(
"views",
{
"list": True,
"kanban": True,
"calendar": True,
"gantt": True,
"spreadsheet": True,
},
)
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
anchor=f"{slug}/{project_id}",
project_id=project_id,
)
project_deploy_board.comments = comments
project_deploy_board.reactions = reactions
project_deploy_board.inbox = inbox
project_deploy_board.votes = votes
project_deploy_board.views = views
project_deploy_board.save()
serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectMemberEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
@ -1011,3 +1097,176 @@ class ProjectMemberEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Project Deploy Board does not exists"},
status=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data
states = State.objects.filter(
workspace__slug=slug, project_id=project_id
).values("name", "group", "color", "id")
labels = Label.objects.filter(
workspace__slug=slug, project_id=project_id
).values("id", "name", "color", "parent")
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
issues = group_results(issues, group_by)
return Response(
{
"issues": issues,
"states": states,
"labels": labels,
},
status=status.HTTP_200_OK,
)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -0,0 +1,965 @@
# Generated by Django 4.2.3 on 2023-08-04 11:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.project
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='analyticview',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='analyticview',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='apitoken',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='apitoken',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='cycle',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='cycle',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cycle',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='cycle',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='cyclefavorite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='cyclefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cyclefavorite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='cyclefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='cycleissue',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='cycleissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cycleissue',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='cycleissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='estimate',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='estimate',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='estimate',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='estimate',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='estimatepoint',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='estimatepoint',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='estimatepoint',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='estimatepoint',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='fileasset',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='fileasset',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='githubcommentsync',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='githubcommentsync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubcommentsync',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='githubcommentsync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubissuesync',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='githubissuesync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubissuesync',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='githubissuesync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubrepository',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='githubrepository',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubrepository',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='githubrepository',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubrepositorysync',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='githubrepositorysync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubrepositorysync',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='githubrepositorysync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='importer',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='importer',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='importer',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='importer',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='inbox',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='inbox',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='inbox',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='inbox',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='inboxissue',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='inboxissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='inboxissue',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='inboxissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='integration',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='integration',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issue',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issue',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueactivity',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueactivity',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueactivity',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueactivity',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueassignee',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueassignee',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueassignee',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueassignee',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueattachment',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueattachment',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueattachment',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueattachment',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueblocker',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueblocker',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueblocker',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueblocker',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuecomment',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issuecomment',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuecomment',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issuecomment',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuelabel',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issuelabel',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuelabel',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issuelabel',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuelink',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issuelink',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuelink',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='issuelink',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issuelink',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueproperty',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueproperty',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueproperty',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueproperty',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuesequence',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issuesequence',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuesequence',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issuesequence',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueview',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueview',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueview',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueview',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueviewfavorite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='issueviewfavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueviewfavorite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='issueviewfavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='label',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='label',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='label',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='label',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='module',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='module',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='module',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='module',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulefavorite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='modulefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulefavorite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='modulefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='moduleissue',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='moduleissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='moduleissue',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='moduleissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulelink',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='modulelink',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulelink',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='modulelink',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulemember',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='modulemember',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulemember',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='modulemember',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='page',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='page',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='page',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='page',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pageblock',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='pageblock',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pageblock',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='pageblock',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pagefavorite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='pagefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pagefavorite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='pagefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pagelabel',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='pagelabel',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pagelabel',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='pagelabel',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='project',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='project',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='projectfavorite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='projectfavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectfavorite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='projectfavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='projectidentifier',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='projectidentifier',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='projectmember',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='projectmember',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectmember',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='projectmember',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='projectmemberinvite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='projectmemberinvite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectmemberinvite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='projectmemberinvite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='slackprojectsync',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='slackprojectsync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='slackprojectsync',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='slackprojectsync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='socialloginconnection',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='socialloginconnection',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='state',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='state',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='state',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='state',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='team',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='team',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='teammember',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='teammember',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='workspace',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='workspace',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='workspaceintegration',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='workspaceintegration',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='workspacemember',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='workspacemember',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='workspacememberinvite',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='workspacememberinvite',
name='updated_by',
field=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'),
),
migrations.AlterField(
model_name='workspacetheme',
name='created_by',
field=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'),
),
migrations.AlterField(
model_name='workspacetheme',
name='updated_by',
field=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'),
),
migrations.CreateModel(
name='ProjectDeployBoard',
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)),
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
('comments', models.BooleanField(default=False)),
('reactions', models.BooleanField(default=False)),
('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='bord_inbox', to='db.inbox')),
('project', models.ForeignKey(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': 'Project Deploy Board',
'verbose_name_plural': 'Project Deploy Boards',
'db_table': 'project_deploy_boards',
'ordering': ('-created_at',),
'unique_together': {('project', 'anchor')},
},
),
]

View File

@ -18,6 +18,7 @@ from .project import (
ProjectMemberInvite,
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
)
from .issue import (
@ -36,6 +37,7 @@ from .issue import (
IssueSubscriber,
IssueReaction,
CommentReaction,
IssueVote,
)
from .asset import FileAsset

View File

@ -301,6 +301,14 @@ class IssueComment(ProjectBaseModel):
related_name="comments",
null=True,
)
access = models.CharField(
choices=(
("INTERNAL", "INTERNAL"),
("EXTERNAL", "EXTERNAL"),
),
default="INTERNAL",
max_length=100,
)
def save(self, *args, **kwargs):
self.comment_stripped = (
@ -416,13 +424,14 @@ class IssueSubscriber(ProjectBaseModel):
class IssueReaction(ProjectBaseModel):
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="issue_reactions",
)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_reactions")
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_reactions"
)
reaction = models.CharField(max_length=20)
class Meta:
@ -437,13 +446,14 @@ class IssueReaction(ProjectBaseModel):
class CommentReaction(ProjectBaseModel):
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="comment_reactions",
)
comment = models.ForeignKey(IssueComment, on_delete=models.CASCADE, related_name="comment_reactions")
comment = models.ForeignKey(
IssueComment, on_delete=models.CASCADE, related_name="comment_reactions"
)
reaction = models.CharField(max_length=20)
class Meta:
@ -457,6 +467,27 @@ class CommentReaction(ProjectBaseModel):
return f"{self.issue.name} {self.actor.email}"
class IssueVote(ProjectBaseModel):
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes")
actor = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes"
)
vote = models.IntegerField(
choices=(
(-1, "DOWNVOTE"),
(1, "UPVOTE"),
)
)
class Meta:
unique_together = ["issue", "actor"]
verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes"
db_table = "issue_votes"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.actor.email}"
# TODO: Find a better method to save the model
@receiver(post_save, sender=Issue)

View File

@ -1,3 +1,6 @@
# Python imports
from uuid import uuid4
# Django imports
from django.db import models
from django.conf import settings
@ -31,12 +34,9 @@ def get_default_props():
"showEmptyGroups": True,
}
def get_default_preferences():
return {
"pages": {
"block_display": True
}
}
return {"pages": {"block_display": True}}
class Project(BaseModel):
@ -157,7 +157,6 @@ class ProjectMember(ProjectBaseModel):
preferences = models.JSONField(default=get_default_preferences)
sort_order = models.FloatField(default=65535)
def save(self, *args, **kwargs):
if self._state.adding:
smallest_sort_order = ProjectMember.objects.filter(
@ -217,3 +216,41 @@ class ProjectFavorite(ProjectBaseModel):
def __str__(self):
"""Return user of the project"""
return f"{self.user.email} <{self.project.name}>"
def get_anchor():
return uuid4().hex
def get_default_views():
return {
"list": True,
"kanban": True,
"calendar": True,
"gantt": True,
"spreadsheet": True,
}
class ProjectDeployBoard(ProjectBaseModel):
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="bord_inbox", on_delete=models.SET_NULL, null=True
)
votes = models.BooleanField(default=False)
views = models.JSONField(default=get_default_views)
class Meta:
unique_together = ["project", "anchor"]
verbose_name = "Project Deploy Board"
verbose_name_plural = "Project Deploy Boards"
db_table = "project_deploy_boards"
ordering = ("-created_at",)
def __str__(self):
"""Return project and anchor"""
return f"{self.anchor} <{self.project.name}>"