fix: pages revamping (#2760)

* fix: page transaction model

* fix: page transaction model

* fix: migration and optimisation

* fix: back migration of page blocks

* fix: added issue embed

* fix: migration fixes

* fix: resolved changes
This commit is contained in:
Bavisetti Narayan 2023-11-16 14:38:12 +05:30 committed by GitHub
parent bed66235f2
commit 0c1097592e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 634 additions and 167 deletions

View File

@ -85,7 +85,7 @@ from .integration import (
from .importer import ImporterSerializer from .importer import ImporterSerializer
from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
from .estimate import ( from .estimate import (
EstimateSerializer, EstimateSerializer,

View File

@ -6,28 +6,7 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
class PageBlockSerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(source="issue", read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = PageBlock
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
class PageBlockLiteSerializer(BaseSerializer):
class Meta:
model = PageBlock
fields = "__all__"
class PageSerializer(BaseSerializer): class PageSerializer(BaseSerializer):
@ -38,7 +17,6 @@ class PageSerializer(BaseSerializer):
write_only=True, write_only=True,
required=False, required=False,
) )
blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@ -102,6 +80,41 @@ class PageSerializer(BaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class SubPageSerializer(BaseSerializer):
entity_details = serializers.SerializerMethodField()
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
def get_entity_details(self, obj):
entity_name = obj.entity_name
if entity_name == 'forward_link' or entity_name == 'back_link':
try:
page = Page.objects.get(pk=obj.entity_identifier)
return PageSerializer(page).data
except Page.DoesNotExist:
return None
return None
class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"page",
]
class PageFavoriteSerializer(BaseSerializer): class PageFavoriteSerializer(BaseSerializer):
page_detail = PageSerializer(source="page", read_only=True) page_detail = PageSerializer(source="page", read_only=True)

View File

@ -3,9 +3,9 @@ from django.urls import path
from plane.api.views import ( from plane.api.views import (
PageViewSet, PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet, PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint, PageLogEndpoint,
SubPagesEndpoint,
) )
@ -31,27 +31,6 @@ urlpatterns = [
), ),
name="project-pages", 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( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
PageFavoriteViewSet.as_view( PageFavoriteViewSet.as_view(
@ -72,8 +51,83 @@ urlpatterns = [
name="user-favorite-pages", name="user-favorite-pages",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
CreateIssueFromPageBlockEndpoint.as_view(), PageViewSet.as_view(
name="page-block-issues", {
"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>/archive/",
PageViewSet.as_view(
{
"post": "archive",
}
),
name="project-page-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
PageViewSet.as_view(
{
"post": "lock",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
),
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(),
name="page-transactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
SubPagesEndpoint.as_view(),
name="sub-page",
), ),
] ]

View File

@ -124,9 +124,10 @@ from plane.api.views import (
## End Modules ## End Modules
# Pages # Pages
PageViewSet, PageViewSet,
PageBlockViewSet, PageLogEndpoint,
SubPagesEndpoint,
PageFavoriteViewSet, PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint, CreateIssueFromBlockEndpoint,
## End Pages ## End Pages
# Api Tokens # Api Tokens
ApiTokenEndpoint, ApiTokenEndpoint,
@ -1222,25 +1223,81 @@ urlpatterns = [
name="project-pages", name="project-pages",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/archive/",
PageBlockViewSet.as_view( PageViewSet.as_view(
{
"post": "archive",
}
),
name="project-page-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unarchive/",
PageViewSet.as_view(
{
"post": "unarchive",
}
),
name="project-page-unarchive"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-pages/",
PageViewSet.as_view(
{
"get": "archive_list",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/lock/",
PageViewSet.as_view(
{
"post": "lock",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/unlock/",
PageViewSet.as_view(
{
"post": "unlock",
}
)
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/",
PageLogEndpoint.as_view(), name="page-transactions"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/transactions/<uuid:transaction>/",
PageLogEndpoint.as_view(), name="page-transactions"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/sub-pages/",
SubPagesEndpoint.as_view(), name="sub-page"
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
} }
), ),
name="project-page-blocks", name="bulk-create-estimate-points",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
PageBlockViewSet.as_view( BulkEstimatePointEndpoint.as_view(
{ {
"get": "retrieve", "get": "retrieve",
"patch": "partial_update", "patch": "partial_update",
"delete": "destroy", "delete": "destroy",
} }
), ),
name="project-page-blocks", name="bulk-create-estimate-points",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-pages/",
@ -1263,7 +1320,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/page-blocks/<uuid:page_block_id>/issues/",
CreateIssueFromPageBlockEndpoint.as_view(), CreateIssueFromBlockEndpoint.as_view(),
name="page-block-issues", name="page-block-issues",
), ),
## End Pages ## End Pages

View File

@ -138,9 +138,10 @@ from .importer import (
from .page import ( from .page import (
PageViewSet, PageViewSet,
PageBlockViewSet,
PageFavoriteViewSet, PageFavoriteViewSet,
CreateIssueFromPageBlockEndpoint, PageLogEndpoint,
SubPagesEndpoint,
CreateIssueFromBlockEndpoint,
) )
from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .search import GlobalSearchEndpoint, IssueSearchEndpoint

View File

@ -1,9 +1,19 @@
# Python imports # Python imports
from datetime import timedelta, date from datetime import timedelta, date, datetime
# Django imports # Django imports
from django.db import connection
from django.db.models import Exists, OuterRef, Q, Prefetch from django.db.models import Exists, OuterRef, Q, Prefetch
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db.models import (
OuterRef,
Func,
F,
Q,
Exists,
)
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -15,20 +25,37 @@ from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import ( from plane.db.models import (
Page, Page,
PageBlock,
PageFavorite, PageFavorite,
Issue, Issue,
IssueAssignee, IssueAssignee,
IssueActivity, IssueActivity,
PageLog,
) )
from plane.api.serializers import ( from plane.api.serializers import (
PageSerializer, PageSerializer,
PageBlockSerializer,
PageFavoriteSerializer, PageFavoriteSerializer,
PageLogSerializer,
IssueLiteSerializer, IssueLiteSerializer,
SubPageSerializer,
) )
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
sql = """
WITH RECURSIVE descendants AS (
SELECT id FROM pages WHERE id = %s
UNION ALL
SELECT pages.id FROM pages, descendants WHERE pages.parent_id = descendants.id
)
UPDATE pages SET archived_at = %s WHERE id IN (SELECT id FROM descendants);
"""
# Execute the SQL query
with connection.cursor() as cursor:
cursor.execute(sql, [page_id, archived_at])
class PageViewSet(BaseViewSet): class PageViewSet(BaseViewSet):
serializer_class = PageSerializer serializer_class = PageSerializer
model = Page model = Page
@ -52,6 +79,7 @@ class PageViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user) .filter(project__project_projectmember__member=self.request.user)
.filter(parent__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=0)) .filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project") .select_related("project")
.select_related("workspace") .select_related("workspace")
@ -59,15 +87,7 @@ class PageViewSet(BaseViewSet):
.annotate(is_favorite=Exists(subquery)) .annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at")) .order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels") .prefetch_related("labels")
.order_by("name", "-is_favorite") .order_by("-is_favorite","-created_at")
.prefetch_related(
Prefetch(
"blocks",
queryset=PageBlock.objects.select_related(
"page", "issue", "workspace", "project"
),
)
)
.distinct() .distinct()
) )
@ -88,7 +108,21 @@ class PageViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
try:
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
if page.is_locked:
return Response(
{"error": "Page is locked"},
status=status.HTTP_400_BAD_REQUEST,
)
parent = request.data.get("parent", None)
if parent:
_ = Page.objects.get(
pk=parent, workspace__slug=slug, project_id=project_id
)
# Only update access if the page owner is the requesting user # Only update access if the page owner is the requesting user
if ( if (
page.access != request.data.get("access", page.access) page.access != request.data.get("access", page.access)
@ -100,22 +134,64 @@ class PageViewSet(BaseViewSet):
}, },
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = PageSerializer(page, data=request.data, partial=True) serializer = PageSerializer(page, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist:
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner can lock the page
if request.user.id != page.owned_by_id:
return Response(
{"error": "Only the page owner can lock the page"},
)
page.is_locked = True
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def unlock(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# only the owner can unlock the page
if request.user.id != page.owned_by_id:
return Response(
{"error": "Only the page owner can unlock the page"},
status=status.HTTP_400_BAD_REQUEST,
)
page.is_locked = False
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
queryset = self.get_queryset() queryset = self.get_queryset().filter(archived_at__isnull=True)
page_view = request.GET.get("page_view", False) page_view = request.GET.get("page_view", False)
if not page_view: if not page_view:
return Response({"error": "Page View parameter is required"}, status=status.HTTP_400_BAD_REQUEST) return Response(
{"error": "Page View parameter is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# All Pages # All Pages
if page_view == "all": if page_view == "all":
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Recent pages # Recent pages
if page_view == "recent": if page_view == "recent":
@ -123,15 +199,19 @@ class PageViewSet(BaseViewSet):
day_before = current_time - timedelta(days=1) day_before = current_time - timedelta(days=1)
todays_pages = queryset.filter(updated_at__date=date.today()) todays_pages = queryset.filter(updated_at__date=date.today())
yesterdays_pages = queryset.filter(updated_at__date=day_before) yesterdays_pages = queryset.filter(updated_at__date=day_before)
earlier_this_week = queryset.filter( updated_at__date__range=( earlier_this_week = queryset.filter(
updated_at__date__range=(
(timezone.now() - timedelta(days=7)), (timezone.now() - timedelta(days=7)),
(timezone.now() - timedelta(days=2)), (timezone.now() - timedelta(days=2)),
)) )
)
return Response( return Response(
{ {
"today": PageSerializer(todays_pages, many=True).data, "today": PageSerializer(todays_pages, many=True).data,
"yesterday": PageSerializer(yesterdays_pages, many=True).data, "yesterday": PageSerializer(yesterdays_pages, many=True).data,
"earlier_this_week": PageSerializer(earlier_this_week, many=True).data, "earlier_this_week": PageSerializer(
earlier_this_week, many=True
).data,
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -139,50 +219,75 @@ class PageViewSet(BaseViewSet):
# Favorite Pages # Favorite Pages
if page_view == "favorite": if page_view == "favorite":
queryset = queryset.filter(is_favorite=True) queryset = queryset.filter(is_favorite=True)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# My pages # My pages
if page_view == "created_by_me": if page_view == "created_by_me":
queryset = queryset.filter(owned_by=request.user) queryset = queryset.filter(owned_by=request.user)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Created by other Pages # Created by other Pages
if page_view == "created_by_other": if page_view == "created_by_other":
queryset = queryset.filter(~Q(owned_by=request.user), access=0) queryset = queryset.filter(~Q(owned_by=request.user), access=0)
return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK) return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
return Response({"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST)
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")
.order_by("sort_order")
.distinct()
) )
def perform_create(self, serializer): return Response(
serializer.save( {"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
project_id=self.kwargs.get("project_id"),
page_id=self.kwargs.get("page_id"),
) )
def archive(self, request, slug, project_id, page_id):
_ = Page.objects.get(
project_id=project_id,
owned_by_id=request.user.id,
workspace__slug=slug,
pk=page_id,
)
unarchive_archive_page_and_descendants(page_id, datetime.now())
return Response(status=status.HTTP_204_NO_CONTENT)
def unarchive(self, request, slug, project_id, page_id):
page = Page.objects.get(
project_id=project_id,
owned_by_id=request.user.id,
workspace__slug=slug,
pk=page_id,
)
page.parent = None
page.save()
unarchive_archive_page_and_descendants(page_id, None)
return Response(status=status.HTTP_204_NO_CONTENT)
def archive_list(self, request, slug, project_id):
pages = (
Page.objects.filter(
project_id=project_id,
workspace__slug=slug,
)
.filter(archived_at__isnull=False)
.filter(parent_id__isnull=True)
)
if not pages:
return Response(
{"error": "No pages found"}, status=status.HTTP_400_BAD_REQUEST
)
return Response(
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
)
class PageFavoriteViewSet(BaseViewSet): class PageFavoriteViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
@ -196,6 +301,7 @@ class PageFavoriteViewSet(BaseViewSet):
return self.filter_queryset( return self.filter_queryset(
super() super()
.get_queryset() .get_queryset()
.filter(archived_at__isnull=True)
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user) .filter(user=self.request.user)
.select_related("page", "page__owned_by") .select_related("page", "page__owned_by")
@ -218,24 +324,62 @@ class PageFavoriteViewSet(BaseViewSet):
page_favorite.delete() page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class CreateIssueFromPageBlockEndpoint(BaseAPIView): class PageLogEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
def post(self, request, slug, project_id, page_id, page_block_id): serializer_class = PageLogSerializer
page_block = PageBlock.objects.get( model = PageLog
pk=page_block_id,
def post(self, request, slug, project_id, page_id):
serializer = PageLogSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, page_id=page_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, page_id, transaction):
page_transaction = PageLog.objects.get(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
page_id=page_id, page_id=page_id,
transaction=transaction,
)
serializer = PageLogSerializer(
page_transaction, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, page_id, transaction):
transaction = PageLog.objects.get(
workspace__slug=slug,
project_id=project_id,
page_id=page_id,
transaction=transaction,
)
# Delete the transaction object
transaction.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CreateIssueFromBlockEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, page_id):
page = Page.objects.get(
workspace__slug=slug,
project_id=project_id,
pk=page_id,
) )
issue = Issue.objects.create( issue = Issue.objects.create(
name=page_block.name, name=request.data.get("name"),
project_id=project_id, project_id=project_id,
description=page_block.description,
description_html=page_block.description_html,
description_stripped=page_block.description_stripped,
) )
_ = IssueAssignee.objects.create( _ = IssueAssignee.objects.create(
issue=issue, assignee=request.user, project_id=project_id issue=issue, assignee=request.user, project_id=project_id
@ -245,11 +389,32 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
issue=issue, issue=issue,
actor=request.user, actor=request.user,
project_id=project_id, project_id=project_id,
comment=f"created the issue from {page_block.name} block", comment=f"created the issue from {page.name} block",
verb="created", verb="created",
) )
page_block.issue = issue
page_block.save()
return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK) return Response(IssueLiteSerializer(issue).data, status=status.HTTP_200_OK)
class SubPagesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
@method_decorator(gzip_page)
def get(self, request, slug, project_id, page_id):
pages = (
PageLog.objects.filter(
page_id=page_id,
project_id=project_id,
workspace__slug=slug,
entity_name__in=["forward_link", "back_link"],
)
.filter(archived_at__isnull=True)
.select_related("project")
.select_related("workspace")
)
return Response(
SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK
)

View File

@ -12,7 +12,7 @@ from celery import shared_task
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
from plane.db.models import Issue, Project, State from plane.db.models import Issue, Project, State, Page
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
@ -20,6 +20,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
def archive_and_close_old_issues(): def archive_and_close_old_issues():
archive_old_issues() archive_old_issues()
close_old_issues() close_old_issues()
delete_archived_pages()
def archive_old_issues(): def archive_old_issues():
@ -80,7 +81,7 @@ def archive_old_issues():
project_id=project_id, project_id=project_id,
current_instance=json.dumps({"archived_at": None}), current_instance=json.dumps({"archived_at": None}),
subscriber=False, subscriber=False,
epoch=int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp()),
) )
for issue in issues_to_update for issue in issues_to_update
] ]
@ -142,17 +143,21 @@ def close_old_issues():
# Bulk Update the issues and log the activity # Bulk Update the issues and log the activity
if issues_to_update: if issues_to_update:
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100) Issue.objects.bulk_update(
issues_to_update, ["state"], batch_size=100
)
[ [
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}), requested_data=json.dumps(
{"closed_to": str(issue.state_id)}
),
actor_id=str(project.created_by_id), actor_id=str(project.created_by_id),
issue_id=issue.id, issue_id=issue.id,
project_id=project_id, project_id=project_id,
current_instance=None, current_instance=None,
subscriber=False, subscriber=False,
epoch=int(timezone.now().timestamp()) epoch=int(timezone.now().timestamp()),
) )
for issue in issues_to_update for issue in issues_to_update
] ]
@ -162,3 +167,20 @@ def close_old_issues():
print(e) print(e)
capture_exception(e) capture_exception(e)
return return
def delete_archived_pages():
try:
pages_to_delete = Page.objects.filter(
archived_at__isnull=False,
archived_at__lte=(timezone.now() - timedelta(days=30)),
)
pages_to_delete._raw_delete(pages_to_delete.db)
return
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return

View File

@ -0,0 +1,54 @@
# Generated by Django 4.2.5 on 2023-11-13 12:53
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0047_webhook_apitoken_description_apitoken_expired_at_and_more'),
]
operations = [
migrations.CreateModel(
name='PageLog',
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)),
('transaction', models.UUIDField(default=uuid.uuid4)),
('entity_identifier', models.UUIDField(null=True)),
('entity_name', models.CharField(choices=[('to_do', 'To Do'), ('issue', 'issue'), ('image', 'Image'), ('video', 'Video'), ('file', 'File'), ('link', 'Link'), ('cycle', 'Cycle'), ('module', 'Module'), ('back_link', 'Back Link'), ('forward_link', 'Forward Link'), ('mention', 'Mention')], max_length=30, verbose_name='Transaction Type')),
('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')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_log', to='db.page')),
('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': 'Page Log',
'verbose_name_plural': 'Page Logs',
'db_table': 'page_logs',
'ordering': ('-created_at',),
'unique_together': {('page', 'transaction')}
},
),
migrations.AddField(
model_name='page',
name='archived_at',
field=models.DateField(null=True),
),
migrations.AddField(
model_name='page',
name='is_locked',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='page',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child_page', to='db.page'),
),
]

View File

@ -0,0 +1,72 @@
# Generated by Django 4.2.5 on 2023-11-15 09:16
# Python imports
import uuid
from django.db import migrations
def update_pages(apps, schema_editor):
try:
Page = apps.get_model("db", "Page")
PageBlock = apps.get_model("db", "PageBlock")
PageLog = apps.get_model("db", "PageLog")
updated_pages = []
page_logs = []
# looping through all the pages
for page in Page.objects.all():
page_blocks = PageBlock.objects.filter(
page_id=page.id, project_id=page.project_id, workspace_id=page.workspace_id
).order_by("sort_order")
if page_blocks:
# looping through all the page blocks in a page
for page_block in page_blocks:
if page_block.issue is not None:
project_identifier = page.project.identifier
sequence_id = page_block.issue.sequence_id
transaction = uuid.uuid4().hex
embed_component = f'<issue-embed-component id="{transaction}" entity_name="issue" entity_identifier="{page_block.issue_id}" sequence_id="{sequence_id}" project_identifier="{project_identifier}" title="{page_block.name}"></issue-embed-component>'
page.description_html += embed_component
# create the page transaction for the issue
page_logs.append(
PageLog(
page_id=page_block.page_id,
transaction=transaction,
entity_identifier=page_block.issue_id,
entity_name="issue",
project_id=page.project_id,
workspace_id=page.workspace_id,
created_by_id=page_block.created_by_id,
updated_by_id=page_block.updated_by_id,
)
)
else:
# adding the page block name and description to the page description
page.description_html += f"<h2>{page_block.name}</h2>"
page.description_html += page_block.description_html
updated_pages.append(page)
Page.objects.bulk_update(
updated_pages,
["description_html"],
batch_size=100,
)
PageLog.objects.bulk_create(page_logs, batch_size=100)
except Exception as e:
print(e)
class Migration(migrations.Migration):
dependencies = [
("db", "0048_auto_20231116_0713"),
]
operations = [
migrations.RunPython(update_pages),
]

View File

@ -68,7 +68,7 @@ from .integration import (
from .importer import Importer from .importer import Importer
from .page import Page, PageBlock, PageFavorite, PageLabel from .page import Page, PageLog, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint from .estimate import Estimate, EstimatePoint

View File

@ -132,25 +132,7 @@ class Issue(ProjectBaseModel):
self.state = default_state self.state = default_state
except ImportError: except ImportError:
pass pass
else:
try:
from plane.db.models import State, PageBlock
# Check if the current issue state and completed state id are same
if self.state.group == "completed":
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:
PageBlock.objects.filter(issue_id=self.id).filter().update(
completed_at=None
)
self.completed_at = None
except ImportError:
pass
if self._state.adding: if self._state.adding:
# Get the maximum display_id value from the database # Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(project=self.project).aggregate( last_id = IssueSequence.objects.filter(project=self.project).aggregate(

View File

@ -1,3 +1,5 @@
import uuid
# Django imports # Django imports
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@ -22,6 +24,15 @@ class Page(ProjectBaseModel):
labels = models.ManyToManyField( labels = models.ManyToManyField(
"db.Label", blank=True, related_name="pages", through="db.PageLabel" "db.Label", blank=True, related_name="pages", through="db.PageLabel"
) )
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="child_page",
)
archived_at = models.DateField(null=True)
is_locked = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name = "Page" verbose_name = "Page"
@ -34,6 +45,42 @@ class Page(ProjectBaseModel):
return f"{self.owned_by.email} <{self.name}>" return f"{self.owned_by.email} <{self.name}>"
class PageLog(ProjectBaseModel):
TYPE_CHOICES = (
("to_do", "To Do"),
("issue", "issue"),
("image", "Image"),
("video", "Video"),
("file", "File"),
("link", "Link"),
("cycle","Cycle"),
("module", "Module"),
("back_link", "Back Link"),
("forward_link", "Forward Link"),
("mention", "Mention"),
)
transaction = models.UUIDField(default=uuid.uuid4)
page = models.ForeignKey(
Page, related_name="page_log", on_delete=models.CASCADE
)
entity_identifier = models.UUIDField(null=True)
entity_name = models.CharField(
max_length=30,
choices=TYPE_CHOICES,
verbose_name="Transaction Type",
)
class Meta:
unique_together = ["page", "transaction"]
verbose_name = "Page Log"
verbose_name_plural = "Page Logs"
db_table = "page_logs"
ordering = ("-created_at",)
def __str__(self):
return f"{self.page.name} {self.type}"
class PageBlock(ProjectBaseModel): class PageBlock(ProjectBaseModel):
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks") page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
name = models.CharField(max_length=255) name = models.CharField(max_length=255)