- {activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
-
- ) : (
- activeWorkspace?.name?.charAt(0) ?? "..."
+
+
+ {activeWorkspace?.logo && activeWorkspace.logo !== "" ? (
+
+ ) : (
+ activeWorkspace?.name?.charAt(0) ?? "..."
+ )}
+
+
+ {!sidebarCollapsed && (
+
+ {activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
+
)}
- {!sidebarCollapsed && (
-
- {activeWorkspace?.name ? activeWorkspace.name : "Loading..."}
-
- )}
-
{!sidebarCollapsed && (
Date: Thu, 16 Nov 2023 14:38:12 +0530
Subject: [PATCH 2/3] 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
---
apiserver/plane/api/serializers/__init__.py | 2 +-
apiserver/plane/api/serializers/page.py | 59 ++--
apiserver/plane/api/urls/page.py | 106 ++++--
apiserver/plane/api/urls_deprecated.py | 75 +++-
apiserver/plane/api/views/__init__.py | 5 +-
apiserver/plane/api/views/page.py | 327 +++++++++++++-----
.../plane/bgtasks/issue_automation_task.py | 34 +-
.../db/migrations/0048_auto_20231116_0713.py | 54 +++
.../db/migrations/0049_auto_20231116_0713.py | 72 ++++
apiserver/plane/db/models/__init__.py | 2 +-
apiserver/plane/db/models/issue.py | 18 -
apiserver/plane/db/models/page.py | 47 +++
12 files changed, 634 insertions(+), 167 deletions(-)
create mode 100644 apiserver/plane/db/migrations/0048_auto_20231116_0713.py
create mode 100644 apiserver/plane/db/migrations/0049_auto_20231116_0713.py
diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py
index 901f0bc01..c406453b7 100644
--- a/apiserver/plane/api/serializers/__init__.py
+++ b/apiserver/plane/api/serializers/__init__.py
@@ -85,7 +85,7 @@ from .integration import (
from .importer import ImporterSerializer
-from .page import PageSerializer, PageBlockSerializer, PageFavoriteSerializer
+from .page import PageSerializer, PageLogSerializer, SubPageSerializer, PageFavoriteSerializer
from .estimate import (
EstimateSerializer,
diff --git a/apiserver/plane/api/serializers/page.py b/apiserver/plane/api/serializers/page.py
index abdf958cb..ff152627a 100644
--- a/apiserver/plane/api/serializers/page.py
+++ b/apiserver/plane/api/serializers/page.py
@@ -6,28 +6,7 @@ from .base import BaseSerializer
from .issue import IssueFlatSerializer, LabelLiteSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
-from plane.db.models import Page, PageBlock, PageFavorite, PageLabel, Label
-
-
-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__"
+from plane.db.models import Page, PageLog, PageFavorite, PageLabel, Label, Issue, Module
class PageSerializer(BaseSerializer):
@@ -38,7 +17,6 @@ class PageSerializer(BaseSerializer):
write_only=True,
required=False,
)
- blocks = PageBlockLiteSerializer(read_only=True, many=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@@ -102,6 +80,41 @@ class PageSerializer(BaseSerializer):
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):
page_detail = PageSerializer(source="page", read_only=True)
diff --git a/apiserver/plane/api/urls/page.py b/apiserver/plane/api/urls/page.py
index 648702283..8b08dcc79 100644
--- a/apiserver/plane/api/urls/page.py
+++ b/apiserver/plane/api/urls/page.py
@@ -3,9 +3,9 @@ from django.urls import path
from plane.api.views import (
PageViewSet,
- PageBlockViewSet,
PageFavoriteViewSet,
- CreateIssueFromPageBlockEndpoint,
+ PageLogEndpoint,
+ SubPagesEndpoint,
)
@@ -31,27 +31,6 @@ urlpatterns = [
),
name="project-pages",
),
- path(
- "workspaces//projects//pages//page-blocks/",
- PageBlockViewSet.as_view(
- {
- "get": "list",
- "post": "create",
- }
- ),
- name="project-page-blocks",
- ),
- path(
- "workspaces//projects//pages//page-blocks//",
- PageBlockViewSet.as_view(
- {
- "get": "retrieve",
- "patch": "partial_update",
- "delete": "destroy",
- }
- ),
- name="project-page-blocks",
- ),
path(
"workspaces//projects//user-favorite-pages/",
PageFavoriteViewSet.as_view(
@@ -72,8 +51,83 @@ urlpatterns = [
name="user-favorite-pages",
),
path(
- "workspaces//projects//pages//page-blocks//issues/",
- CreateIssueFromPageBlockEndpoint.as_view(),
- name="page-block-issues",
+ "workspaces//projects//pages/",
+ PageViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//",
+ PageViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//archive/",
+ PageViewSet.as_view(
+ {
+ "post": "archive",
+ }
+ ),
+ name="project-page-archive",
+ ),
+ path(
+ "workspaces//projects//pages//unarchive/",
+ PageViewSet.as_view(
+ {
+ "post": "unarchive",
+ }
+ ),
+ name="project-page-unarchive",
+ ),
+ path(
+ "workspaces//projects//archived-pages/",
+ PageViewSet.as_view(
+ {
+ "get": "archive_list",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//lock/",
+ PageViewSet.as_view(
+ {
+ "post": "lock",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//unlock/",
+ PageViewSet.as_view(
+ {
+ "post": "unlock",
+ }
+ ),
+ ),
+ path(
+ "workspaces//projects//pages//transactions/",
+ PageLogEndpoint.as_view(),
+ name="page-transactions",
+ ),
+ path(
+ "workspaces//projects//pages//transactions//",
+ PageLogEndpoint.as_view(),
+ name="page-transactions",
+ ),
+ path(
+ "workspaces//projects//pages//sub-pages/",
+ SubPagesEndpoint.as_view(),
+ name="sub-page",
),
]
diff --git a/apiserver/plane/api/urls_deprecated.py b/apiserver/plane/api/urls_deprecated.py
index 67cc62e46..1f05675a2 100644
--- a/apiserver/plane/api/urls_deprecated.py
+++ b/apiserver/plane/api/urls_deprecated.py
@@ -124,9 +124,10 @@ from plane.api.views import (
## End Modules
# Pages
PageViewSet,
- PageBlockViewSet,
+ PageLogEndpoint,
+ SubPagesEndpoint,
PageFavoriteViewSet,
- CreateIssueFromPageBlockEndpoint,
+ CreateIssueFromBlockEndpoint,
## End Pages
# Api Tokens
ApiTokenEndpoint,
@@ -1222,25 +1223,81 @@ urlpatterns = [
name="project-pages",
),
path(
- "workspaces//projects//pages//page-blocks/",
- PageBlockViewSet.as_view(
+ "workspaces//projects//pages//archive/",
+ PageViewSet.as_view(
+ {
+ "post": "archive",
+ }
+ ),
+ name="project-page-archive",
+ ),
+ path(
+ "workspaces//projects//pages//unarchive/",
+ PageViewSet.as_view(
+ {
+ "post": "unarchive",
+ }
+ ),
+ name="project-page-unarchive"
+ ),
+ path(
+ "workspaces//projects//archived-pages/",
+ PageViewSet.as_view(
+ {
+ "get": "archive_list",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//lock/",
+ PageViewSet.as_view(
+ {
+ "post": "lock",
+ }
+ ),
+ name="project-pages",
+ ),
+ path(
+ "workspaces//projects//pages//unlock/",
+ PageViewSet.as_view(
+ {
+ "post": "unlock",
+ }
+ )
+ ),
+ path(
+ "workspaces//projects//pages//transactions/",
+ PageLogEndpoint.as_view(), name="page-transactions"
+ ),
+ path(
+ "workspaces//projects//pages//transactions//",
+ PageLogEndpoint.as_view(), name="page-transactions"
+ ),
+ path(
+ "workspaces//projects//pages//sub-pages/",
+ SubPagesEndpoint.as_view(), name="sub-page"
+ ),
+ path(
+ "workspaces//projects//estimates/",
+ BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
- name="project-page-blocks",
+ name="bulk-create-estimate-points",
),
path(
- "workspaces//projects//pages//page-blocks//",
- PageBlockViewSet.as_view(
+ "workspaces//projects//estimates//",
+ BulkEstimatePointEndpoint.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
- name="project-page-blocks",
+ name="bulk-create-estimate-points",
),
path(
"workspaces//projects//user-favorite-pages/",
@@ -1263,7 +1320,7 @@ urlpatterns = [
),
path(
"workspaces//projects//pages//page-blocks//issues/",
- CreateIssueFromPageBlockEndpoint.as_view(),
+ CreateIssueFromBlockEndpoint.as_view(),
name="page-block-issues",
),
## End Pages
diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py
index 787dfb3e2..46e88b0bc 100644
--- a/apiserver/plane/api/views/__init__.py
+++ b/apiserver/plane/api/views/__init__.py
@@ -138,9 +138,10 @@ from .importer import (
from .page import (
PageViewSet,
- PageBlockViewSet,
PageFavoriteViewSet,
- CreateIssueFromPageBlockEndpoint,
+ PageLogEndpoint,
+ SubPagesEndpoint,
+ CreateIssueFromBlockEndpoint,
)
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/api/views/page.py
index ca0927a51..d8c90fc8f 100644
--- a/apiserver/plane/api/views/page.py
+++ b/apiserver/plane/api/views/page.py
@@ -1,9 +1,19 @@
# Python imports
-from datetime import timedelta, date
+from datetime import timedelta, date, datetime
# Django imports
+from django.db import connection
from django.db.models import Exists, OuterRef, Q, Prefetch
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
from rest_framework import status
@@ -15,20 +25,37 @@ from .base import BaseViewSet, BaseAPIView
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
- PageBlock,
PageFavorite,
Issue,
IssueAssignee,
IssueActivity,
+ PageLog,
)
from plane.api.serializers import (
PageSerializer,
- PageBlockSerializer,
PageFavoriteSerializer,
+ PageLogSerializer,
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):
serializer_class = PageSerializer
model = Page
@@ -52,6 +79,7 @@ class PageViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(project__project_projectmember__member=self.request.user)
+ .filter(parent__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.select_related("project")
.select_related("workspace")
@@ -59,15 +87,7 @@ class PageViewSet(BaseViewSet):
.annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.prefetch_related("labels")
- .order_by("name", "-is_favorite")
- .prefetch_related(
- Prefetch(
- "blocks",
- queryset=PageBlock.objects.select_related(
- "page", "issue", "workspace", "project"
- ),
- )
- )
+ .order_by("-is_favorite","-created_at")
.distinct()
)
@@ -88,34 +108,90 @@ class PageViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, pk):
- page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
- # Only update access if the page owner is the requesting user
- if (
- page.access != request.data.get("access", page.access)
- and page.owned_by_id != request.user.id
- ):
+ try:
+ 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
+ if (
+ page.access != request.data.get("access", page.access)
+ and page.owned_by_id != request.user.id
+ ):
+ return Response(
+ {
+ "error": "Access cannot be updated since this page is owned by someone else"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
+ serializer = PageSerializer(page, 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)
+ except Page.DoesNotExist:
return Response(
{
"error": "Access cannot be updated since this page is owned by someone else"
},
status=status.HTTP_400_BAD_REQUEST,
)
- serializer = PageSerializer(page, 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 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):
- queryset = self.get_queryset()
+ queryset = self.get_queryset().filter(archived_at__isnull=True)
page_view = request.GET.get("page_view", False)
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
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
if page_view == "recent":
@@ -123,66 +199,95 @@ class PageViewSet(BaseViewSet):
day_before = current_time - timedelta(days=1)
todays_pages = queryset.filter(updated_at__date=date.today())
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=2)),
- ))
+ )
+ )
return Response(
- {
- "today": PageSerializer(todays_pages, many=True).data,
- "yesterday": PageSerializer(yesterdays_pages, many=True).data,
- "earlier_this_week": PageSerializer(earlier_this_week, many=True).data,
- },
- status=status.HTTP_200_OK,
- )
+ {
+ "today": PageSerializer(todays_pages, many=True).data,
+ "yesterday": PageSerializer(yesterdays_pages, many=True).data,
+ "earlier_this_week": PageSerializer(
+ earlier_this_week, many=True
+ ).data,
+ },
+ status=status.HTTP_200_OK,
+ )
# Favorite Pages
if page_view == "favorite":
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
if page_view == "created_by_me":
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
if page_view == "created_by_other":
- queryset = queryset.filter(~Q(owned_by=request.user), access=0)
- return Response(PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK)
+ queryset = queryset.filter(~Q(owned_by=request.user), access=0)
+ 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()
+ return Response(
+ {"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
)
- def perform_create(self, serializer):
- serializer.save(
- 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):
permission_classes = [
@@ -196,6 +301,7 @@ class PageFavoriteViewSet(BaseViewSet):
return self.filter_queryset(
super()
.get_queryset()
+ .filter(archived_at__isnull=True)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user)
.select_related("page", "page__owned_by")
@@ -218,24 +324,62 @@ class PageFavoriteViewSet(BaseViewSet):
page_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
-class CreateIssueFromPageBlockEndpoint(BaseAPIView):
+class PageLogEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
- def post(self, request, slug, project_id, page_id, page_block_id):
- page_block = PageBlock.objects.get(
- pk=page_block_id,
+ serializer_class = PageLogSerializer
+ model = PageLog
+
+ 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,
project_id=project_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(
- name=page_block.name,
+ name=request.data.get("name"),
project_id=project_id,
- description=page_block.description,
- description_html=page_block.description_html,
- description_stripped=page_block.description_stripped,
)
_ = IssueAssignee.objects.create(
issue=issue, assignee=request.user, project_id=project_id
@@ -245,11 +389,32 @@ class CreateIssueFromPageBlockEndpoint(BaseAPIView):
issue=issue,
actor=request.user,
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",
)
- page_block.issue = issue
- page_block.save()
-
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
+ )
+
diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py
index 4d77eb124..d9e1e8ef2 100644
--- a/apiserver/plane/bgtasks/issue_automation_task.py
+++ b/apiserver/plane/bgtasks/issue_automation_task.py
@@ -12,7 +12,7 @@ from celery import shared_task
from sentry_sdk import capture_exception
# 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
@@ -20,6 +20,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
def archive_and_close_old_issues():
archive_old_issues()
close_old_issues()
+ delete_archived_pages()
def archive_old_issues():
@@ -67,7 +68,7 @@ def archive_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
- if issues_to_update:
+ if issues_to_update:
Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
@@ -80,7 +81,7 @@ def archive_old_issues():
project_id=project_id,
current_instance=json.dumps({"archived_at": None}),
subscriber=False,
- epoch=int(timezone.now().timestamp())
+ epoch=int(timezone.now().timestamp()),
)
for issue in issues_to_update
]
@@ -142,17 +143,21 @@ def close_old_issues():
# Bulk Update the issues and log the activity
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(
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),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
- epoch=int(timezone.now().timestamp())
+ epoch=int(timezone.now().timestamp()),
)
for issue in issues_to_update
]
@@ -162,3 +167,20 @@ def close_old_issues():
print(e)
capture_exception(e)
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
+
diff --git a/apiserver/plane/db/migrations/0048_auto_20231116_0713.py b/apiserver/plane/db/migrations/0048_auto_20231116_0713.py
new file mode 100644
index 000000000..8c5de417e
--- /dev/null
+++ b/apiserver/plane/db/migrations/0048_auto_20231116_0713.py
@@ -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'),
+ ),
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/db/migrations/0049_auto_20231116_0713.py b/apiserver/plane/db/migrations/0049_auto_20231116_0713.py
new file mode 100644
index 000000000..75d5e5982
--- /dev/null
+++ b/apiserver/plane/db/migrations/0049_auto_20231116_0713.py
@@ -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''
+ 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"{page_block.name}
"
+ 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),
+ ]
\ No newline at end of file
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index 37ac6dfb5..c76df6e5b 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -68,7 +68,7 @@ from .integration import (
from .importer import Importer
-from .page import Page, PageBlock, PageFavorite, PageLabel
+from .page import Page, PageLog, PageFavorite, PageLabel
from .estimate import Estimate, EstimatePoint
diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py
index a951e5c11..da415058d 100644
--- a/apiserver/plane/db/models/issue.py
+++ b/apiserver/plane/db/models/issue.py
@@ -132,25 +132,7 @@ class Issue(ProjectBaseModel):
self.state = default_state
except ImportError:
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:
# Get the maximum display_id value from the database
last_id = IssueSequence.objects.filter(project=self.project).aggregate(
diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py
index 557fcb323..a8e284bb6 100644
--- a/apiserver/plane/db/models/page.py
+++ b/apiserver/plane/db/models/page.py
@@ -1,3 +1,5 @@
+import uuid
+
# Django imports
from django.db import models
from django.conf import settings
@@ -22,6 +24,15 @@ class Page(ProjectBaseModel):
labels = models.ManyToManyField(
"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:
verbose_name = "Page"
@@ -34,6 +45,42 @@ class Page(ProjectBaseModel):
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):
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="blocks")
name = models.CharField(max_length=255)
From dd60dec887b7efb54ee0c8ea0ef353ad1ccf2900 Mon Sep 17 00:00:00 2001
From: Manish Gupta <59428681+manishg3@users.noreply.github.com>
Date: Thu, 16 Nov 2023 14:38:55 +0530
Subject: [PATCH 3/3] Dev/mg selfhosting fix (#2782)
* fixes to self hosting
* self hosting fixes
* removed .temp
---------
Co-authored-by: sriram veeraghanta
---
deploy/selfhost/docker-compose.yml | 3 ++-
deploy/selfhost/variables.env | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml
index 03e136ba1..8ac4a7277 100644
--- a/deploy/selfhost/docker-compose.yml
+++ b/deploy/selfhost/docker-compose.yml
@@ -2,7 +2,8 @@ version: "3.8"
x-app-env : &app-env
environment:
- - NGINX_PORT=${NGINX_PORT:-84}
+ - NGINX_PORT=${NGINX_PORT:-80}
+ - WEB_URL=${WEB_URL:-http://localhost}
- DEBUG=${DEBUG:-0}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.selfhosted}
- NEXT_PUBLIC_ENABLE_OAUTH=${NEXT_PUBLIC_ENABLE_OAUTH:-0}
diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env
index 7581dfdc1..b2547cbbe 100644
--- a/deploy/selfhost/variables.env
+++ b/deploy/selfhost/variables.env
@@ -5,6 +5,7 @@ SPACE_REPLICAS=1
API_REPLICAS=1
NGINX_PORT=80
+WEB_URL=http://localhost
DEBUG=0
DJANGO_SETTINGS_MODULE=plane.settings.selfhosted
NEXT_PUBLIC_ENABLE_OAUTH=0