mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: page and page-blocks (#468)
* dev: initiate paper models * feat: page and page-blocks * dev: page id filter for page blocks
This commit is contained in:
parent
025c8b3835
commit
f7dbc5e9c0
@ -60,3 +60,5 @@ from .integration import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .importer import ImporterSerializer
|
from .importer import ImporterSerializer
|
||||||
|
|
||||||
|
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
|
||||||
|
46
apiserver/plane/api/serializers/page.py
Normal file
46
apiserver/plane/api/serializers/page.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Third party imports
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseSerializer
|
||||||
|
from .issue import IssueFlatSerializer
|
||||||
|
from plane.db.models import Page, PageBlock, PageFavorite
|
||||||
|
|
||||||
|
|
||||||
|
class PageSerializer(BaseSerializer):
|
||||||
|
is_favorite = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Page
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"owned_by",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PageBlockSerializer(BaseSerializer):
|
||||||
|
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PageBlock
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"page",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PageFavoriteSerializer(BaseSerializer):
|
||||||
|
page_detail = PageSerializer(source="page", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PageFavorite
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"workspace",
|
||||||
|
"project",
|
||||||
|
"user",
|
||||||
|
]
|
@ -100,6 +100,12 @@ from plane.api.views import (
|
|||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
## End Modules
|
## End Modules
|
||||||
|
# Pages
|
||||||
|
PageViewSet,
|
||||||
|
PageBlockViewSet,
|
||||||
|
PageFavoriteViewSet,
|
||||||
|
CreateIssueFromPageBlockEndpoint,
|
||||||
|
## End Pages
|
||||||
# Api Tokens
|
# Api Tokens
|
||||||
ApiTokenEndpoint,
|
ApiTokenEndpoint,
|
||||||
## End Api Tokens
|
## End Api Tokens
|
||||||
@ -899,6 +905,74 @@ urlpatterns = [
|
|||||||
name="user-favorite-module",
|
name="user-favorite-module",
|
||||||
),
|
),
|
||||||
## End Modules
|
## End Modules
|
||||||
|
# Pages
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
|
||||||
|
PageViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/",
|
||||||
|
PageBlockViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-blocks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/",
|
||||||
|
PageBlockViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-page-blocks",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
|
||||||
|
PageFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/<uuid:page_id>/",
|
||||||
|
PageFavoriteViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="user-favorite-pages",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
|
||||||
|
CreateIssueFromPageBlockEndpoint.as_view(),
|
||||||
|
name="page-block-issues",
|
||||||
|
),
|
||||||
|
## End Pages
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
|
@ -109,3 +109,5 @@ from .importer import (
|
|||||||
UpdateServiceImportStatusEndpoint,
|
UpdateServiceImportStatusEndpoint,
|
||||||
BulkImportIssuesEndpoint,
|
BulkImportIssuesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .page import PageViewSet, PageBlockViewSet, PageFavoriteViewSet, CreateIssueFromPageBlockEndpoint
|
||||||
|
184
apiserver/plane/api/views/page.py
Normal file
184
apiserver/plane/api/views/page.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import Exists, OuterRef, Q
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseViewSet, BaseAPIView
|
||||||
|
from plane.api.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
Page,
|
||||||
|
PageBlock,
|
||||||
|
PageFavorite,
|
||||||
|
Issue,
|
||||||
|
IssueAssignee,
|
||||||
|
IssueActivity,
|
||||||
|
)
|
||||||
|
from plane.api.serializers import (
|
||||||
|
PageSerializer,
|
||||||
|
PageBlockSerializer,
|
||||||
|
PageFavoriteSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PageViewSet(BaseViewSet):
|
||||||
|
serializer_class = PageSerializer
|
||||||
|
model = Page
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
subquery = PageFavorite.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
page_id=OuterRef("pk"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
)
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("owned_by")
|
||||||
|
.annotate(is_favorite=Exists(subquery))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PageBlockViewSet(BaseViewSet):
|
||||||
|
serializer_class = PageBlockSerializer
|
||||||
|
model = PageBlock
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(page_id=self.kwargs.get("page_id"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("page")
|
||||||
|
.select_related("issue")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
page_id=self.kwargs.get("page_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PageFavoriteViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
serializer_class = PageFavoriteSerializer
|
||||||
|
model = PageFavorite
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(user=self.request.user)
|
||||||
|
.select_related("page", "page__owned_by")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
serializer = PageFavoriteSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(user=request.user, project_id=project_id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"error": "The page is already added to favorites"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
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, page_id):
|
||||||
|
try:
|
||||||
|
page_favorite = PageFavorite.objects.get(
|
||||||
|
project=project_id,
|
||||||
|
user=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
page_id=page_id,
|
||||||
|
)
|
||||||
|
page_favorite.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
except PageFavorite.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Page is not in favorites"},
|
||||||
|
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 CreateIssueFromPageBlockEndpoint(BaseAPIView):
|
||||||
|
def post(self, request, slug, project_id, page_id, page_block_id):
|
||||||
|
try:
|
||||||
|
page_block = PageBlock.objects.get(
|
||||||
|
pk=page_block_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
page_id=page_id,
|
||||||
|
)
|
||||||
|
issue = Issue.objects.create(name=page_block.name, project_id=project_id)
|
||||||
|
_ = IssueAssignee.objects.create(
|
||||||
|
issue=issue, assignee=request.user, project_id=project_id
|
||||||
|
)
|
||||||
|
page_block.issue = issue
|
||||||
|
page_block.save()
|
||||||
|
|
||||||
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
|
except PageBlock.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Page Block does not exist"}, 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,
|
||||||
|
)
|
@ -60,3 +60,5 @@ from .integration import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
|
|
||||||
|
from .page import Page, PageBlock, PageFavorite
|
@ -85,7 +85,7 @@ class Issue(ProjectBaseModel):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from plane.db.models import State
|
from plane.db.models import State, PageBlock
|
||||||
|
|
||||||
# Get the completed states of the project
|
# Get the completed states of the project
|
||||||
completed_states = State.objects.filter(
|
completed_states = State.objects.filter(
|
||||||
@ -94,7 +94,15 @@ class Issue(ProjectBaseModel):
|
|||||||
# Check if the current issue state and completed state id are same
|
# Check if the current issue state and completed state id are same
|
||||||
if self.state.id in completed_states:
|
if self.state.id in completed_states:
|
||||||
self.completed_at = timezone.now()
|
self.completed_at = timezone.now()
|
||||||
|
# check if there are any page blocks
|
||||||
|
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||||
|
completed_at=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||||
|
completed_at=None
|
||||||
|
)
|
||||||
self.completed_at = None
|
self.completed_at = None
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
73
apiserver/plane/db/models/page.py
Normal file
73
apiserver/plane/db/models/page.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from . import ProjectBaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Page(ProjectBaseModel):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.JSONField(default=dict, blank=True)
|
||||||
|
description_html = models.TextField(blank=True, default="<p></p>")
|
||||||
|
description_stripped = models.TextField(blank=True, null=True)
|
||||||
|
owned_by = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="pages"
|
||||||
|
)
|
||||||
|
access = models.PositiveSmallIntegerField(
|
||||||
|
choices=((0, "Public"), (1, "Private")), default=0
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Page"
|
||||||
|
verbose_name_plural = "Pages"
|
||||||
|
db_table = "pages"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return owner email and page name"""
|
||||||
|
return f"{self.owned_by.email} <{self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class PageBlock(ProjectBaseModel):
|
||||||
|
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
description = models.JSONField(default=dict, blank=True)
|
||||||
|
description_html = models.TextField(blank=True, default="<p></p>")
|
||||||
|
description_stripped = models.TextField(blank=True, null=True)
|
||||||
|
issue = models.ForeignKey(
|
||||||
|
"db.Issue", on_delete=models.SET_NULL, related_name="blocks", null=True
|
||||||
|
)
|
||||||
|
completed_at = models.DateTimeField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Page Block"
|
||||||
|
verbose_name_plural = "Page Blocks"
|
||||||
|
db_table = "page_blocks"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return page and page block"""
|
||||||
|
return f"{self.page.name} <{self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class PageFavorite(ProjectBaseModel):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="page_favorites",
|
||||||
|
)
|
||||||
|
page = models.ForeignKey(
|
||||||
|
"db.Page", on_delete=models.CASCADE, related_name="page_favorites"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["page", "user"]
|
||||||
|
verbose_name = "Page Favorite"
|
||||||
|
verbose_name_plural = "Page Favorites"
|
||||||
|
db_table = "page_favorites"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return user and the page"""
|
||||||
|
return f"{self.user.email} <{self.page.name}>"
|
Loading…
Reference in New Issue
Block a user