diff --git a/apiserver/back_migration.py b/apiserver/back_migration.py
deleted file mode 100644
index a0e45416a..000000000
--- a/apiserver/back_migration.py
+++ /dev/null
@@ -1,238 +0,0 @@
-# All the python scripts that are used for back migrations
-import uuid
-import random
-from django.contrib.auth.hashers import make_password
-from plane.db.models import ProjectIdentifier
-from plane.db.models import (
- Issue,
- IssueComment,
- User,
- Project,
- ProjectMember,
- Label,
- Integration,
-)
-
-
-# Update description and description html values for old descriptions
-def update_description():
- try:
- issues = Issue.objects.all()
- updated_issues = []
-
- for issue in issues:
- issue.description_html = f"
{issue.description}
"
- issue.description_stripped = issue.description
- updated_issues.append(issue)
-
- Issue.objects.bulk_update(
- updated_issues,
- ["description_html", "description_stripped"],
- batch_size=100,
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_comments():
- try:
- issue_comments = IssueComment.objects.all()
- updated_issue_comments = []
-
- for issue_comment in issue_comments:
- issue_comment.comment_html = (
- f"{issue_comment.comment_stripped}
"
- )
- updated_issue_comments.append(issue_comment)
-
- IssueComment.objects.bulk_update(
- updated_issue_comments, ["comment_html"], batch_size=100
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_project_identifiers():
- try:
- project_identifiers = ProjectIdentifier.objects.filter(
- workspace_id=None
- ).select_related("project", "project__workspace")
- updated_identifiers = []
-
- for identifier in project_identifiers:
- identifier.workspace_id = identifier.project.workspace_id
- updated_identifiers.append(identifier)
-
- ProjectIdentifier.objects.bulk_update(
- updated_identifiers, ["workspace_id"], batch_size=50
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_user_empty_password():
- try:
- users = User.objects.filter(password="")
- updated_users = []
-
- for user in users:
- user.password = make_password(uuid.uuid4().hex)
- user.is_password_autoset = True
- updated_users.append(user)
-
- User.objects.bulk_update(updated_users, ["password"], batch_size=50)
- print("Success")
-
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def updated_issue_sort_order():
- try:
- issues = Issue.objects.all()
- updated_issues = []
-
- for issue in issues:
- issue.sort_order = issue.sequence_id * random.randint(100, 500)
- updated_issues.append(issue)
-
- Issue.objects.bulk_update(
- updated_issues, ["sort_order"], batch_size=100
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_project_cover_images():
- try:
- project_cover_images = [
- "https://images.unsplash.com/photo-1677432658720-3d84f9d657b4?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1661107564401-57497d8fe86f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
- "https://images.unsplash.com/photo-1677352241429-dc90cfc7a623?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
- "https://images.unsplash.com/photo-1677196728306-eeafea692454?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1331&q=80",
- "https://images.unsplash.com/photo-1660902179734-c94c944f7830?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1255&q=80",
- "https://images.unsplash.com/photo-1672243775941-10d763d9adef?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1677040628614-53936ff66632?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1676920410907-8d5f8dd4b5ba?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
- "https://images.unsplash.com/photo-1676846328604-ce831c481346?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1155&q=80",
- "https://images.unsplash.com/photo-1676744843212-09b7e64c3a05?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1676798531090-1608bedeac7b?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1597088758740-56fd7ec8a3f0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1169&q=80",
- "https://images.unsplash.com/photo-1676638392418-80aad7c87b96?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
- "https://images.unsplash.com/photo-1649639194967-2fec0b4ea7bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1675883086902-b453b3f8146e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
- "https://images.unsplash.com/photo-1675887057159-40fca28fdc5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1173&q=80",
- "https://images.unsplash.com/photo-1675373980203-f84c5a672aa5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1675191475318-d2bf6bad1200?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
- "https://images.unsplash.com/photo-1675456230532-2194d0c4bcc0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80",
- "https://images.unsplash.com/photo-1675371788315-60fa0ef48267?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1332&q=80",
- ]
-
- projects = Project.objects.all()
- updated_projects = []
- for project in projects:
- project.cover_image = project_cover_images[random.randint(0, 19)]
- updated_projects.append(project)
-
- Project.objects.bulk_update(
- updated_projects, ["cover_image"], batch_size=100
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_user_view_property():
- try:
- project_members = ProjectMember.objects.all()
- updated_project_members = []
- for project_member in project_members:
- project_member.default_props = {
- "filters": {"type": None},
- "orderBy": "-created_at",
- "collapsed": True,
- "issueView": "list",
- "filterIssue": None,
- "groupByProperty": None,
- "showEmptyGroups": True,
- }
- updated_project_members.append(project_member)
-
- ProjectMember.objects.bulk_update(
- updated_project_members, ["default_props"], batch_size=100
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_label_color():
- try:
- labels = Label.objects.filter(color="")
- updated_labels = []
- for label in labels:
- label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF)
- updated_labels.append(label)
-
- Label.objects.bulk_update(updated_labels, ["color"], batch_size=100)
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def create_slack_integration():
- try:
- _ = Integration.objects.create(
- provider="slack", network=2, title="Slack"
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_integration_verified():
- try:
- integrations = Integration.objects.all()
- updated_integrations = []
- for integration in integrations:
- integration.verified = True
- updated_integrations.append(integration)
-
- Integration.objects.bulk_update(
- updated_integrations, ["verified"], batch_size=10
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
-
-
-def update_start_date():
- try:
- issues = Issue.objects.filter(
- state__group__in=["started", "completed"]
- )
- updated_issues = []
- for issue in issues:
- issue.start_date = issue.created_at.date()
- updated_issues.append(issue)
- Issue.objects.bulk_update(
- updated_issues, ["start_date"], batch_size=500
- )
- print("Success")
- except Exception as e:
- print(e)
- print("Failed")
diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py
index 0d72f9192..e3c1cd69d 100644
--- a/apiserver/plane/app/serializers/__init__.py
+++ b/apiserver/plane/app/serializers/__init__.py
@@ -36,9 +36,8 @@ from .project import (
)
from .state import StateSerializer, StateLiteSerializer
from .view import (
- GlobalViewSerializer,
- IssueViewSerializer,
- IssueViewFavoriteSerializer,
+ ViewSerializer,
+ ViewFavoriteSerializer,
)
from .cycle import (
CycleSerializer,
diff --git a/apiserver/plane/app/serializers/view.py b/apiserver/plane/app/serializers/view.py
index f864f2b6c..cc719f035 100644
--- a/apiserver/plane/app/serializers/view.py
+++ b/apiserver/plane/app/serializers/view.py
@@ -3,69 +3,33 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
-from .workspace import WorkspaceLiteSerializer
-from .project import ProjectLiteSerializer
-from plane.db.models import GlobalView, IssueView, IssueViewFavorite
+from plane.db.models import View, ViewFavorite
from plane.utils.issue_filters import issue_filters
-class GlobalViewSerializer(BaseSerializer):
- workspace_detail = WorkspaceLiteSerializer(
- source="workspace", read_only=True
- )
-
- class Meta:
- model = GlobalView
- fields = "__all__"
- read_only_fields = [
- "workspace",
- "query",
- ]
-
- def create(self, validated_data):
- query_params = validated_data.get("query_data", {})
- if bool(query_params):
- validated_data["query"] = issue_filters(query_params, "POST")
- else:
- validated_data["query"] = dict()
- return GlobalView.objects.create(**validated_data)
-
- def update(self, instance, validated_data):
- query_params = validated_data.get("query_data", {})
- if bool(query_params):
- validated_data["query"] = issue_filters(query_params, "POST")
- else:
- validated_data["query"] = dict()
- validated_data["query"] = issue_filters(query_params, "PATCH")
- return super().update(instance, validated_data)
-
-
-class IssueViewSerializer(DynamicBaseSerializer):
+class ViewSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
- project_detail = ProjectLiteSerializer(source="project", read_only=True)
- workspace_detail = WorkspaceLiteSerializer(
- source="workspace", read_only=True
- )
class Meta:
- model = IssueView
+ model = View
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"query",
+ "access",
]
def create(self, validated_data):
- query_params = validated_data.get("query_data", {})
+ query_params = validated_data.get("filters", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = {}
- return IssueView.objects.create(**validated_data)
+ return View.objects.create(**validated_data)
def update(self, instance, validated_data):
- query_params = validated_data.get("query_data", {})
+ query_params = validated_data.get("filters", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
@@ -74,11 +38,10 @@ class IssueViewSerializer(DynamicBaseSerializer):
return super().update(instance, validated_data)
-class IssueViewFavoriteSerializer(BaseSerializer):
- view_detail = IssueViewSerializer(source="issue_view", read_only=True)
+class ViewFavoriteSerializer(BaseSerializer):
class Meta:
- model = IssueViewFavorite
+ model = ViewFavorite
fields = "__all__"
read_only_fields = [
"workspace",
diff --git a/apiserver/plane/app/urls/user.py b/apiserver/plane/app/urls/user.py
index 9dae7b5da..2682c2381 100644
--- a/apiserver/plane/app/urls/user.py
+++ b/apiserver/plane/app/urls/user.py
@@ -14,6 +14,8 @@ from plane.app.views import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint,
+ UserWorkspaceViewViewSet,
+ UserProjectViewViewSet,
## End Workspaces
)
@@ -95,5 +97,47 @@ urlpatterns = [
SetUserPasswordEndpoint.as_view(),
name="set-password",
),
+ path(
+ "users/me/workspaces//views/",
+ UserWorkspaceViewViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="user-workspace-views",
+ ),
+ path(
+ "users/me/workspaces//views//",
+ UserWorkspaceViewViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="user-workspace-views",
+ ),
+ path(
+ "users/me/workspaces//projects//views/",
+ UserProjectViewViewSet.as_view(
+ {
+ "get": "list",
+ "post": "create",
+ }
+ ),
+ name="user-project-views",
+ ),
+ path(
+ "users/me/workspaces//projects//views//",
+ UserProjectViewViewSet.as_view(
+ {
+ "get": "retrieve",
+ "patch": "partial_update",
+ "delete": "destroy",
+ }
+ ),
+ name="user-project-views",
+ ),
## End User Graph
]
diff --git a/apiserver/plane/app/urls/views.py b/apiserver/plane/app/urls/views.py
index 36372c03a..a76ed8dbf 100644
--- a/apiserver/plane/app/urls/views.py
+++ b/apiserver/plane/app/urls/views.py
@@ -2,17 +2,16 @@ from django.urls import path
from plane.app.views import (
- IssueViewViewSet,
- GlobalViewViewSet,
- GlobalViewIssuesViewSet,
- IssueViewFavoriteViewSet,
+ ProjectViewViewSet,
+ WorkspaceViewViewSet,
+ ViewFavoriteViewSet,
)
urlpatterns = [
path(
"workspaces//projects//views/",
- IssueViewViewSet.as_view(
+ ProjectViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -22,7 +21,7 @@ urlpatterns = [
),
path(
"workspaces//projects//views//",
- IssueViewViewSet.as_view(
+ ProjectViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
@@ -34,7 +33,7 @@ urlpatterns = [
),
path(
"workspaces//views/",
- GlobalViewViewSet.as_view(
+ WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -44,10 +43,9 @@ urlpatterns = [
),
path(
"workspaces//views//",
- GlobalViewViewSet.as_view(
+ WorkspaceViewViewSet.as_view(
{
"get": "retrieve",
- "put": "update",
"patch": "partial_update",
"delete": "destroy",
}
@@ -56,7 +54,7 @@ urlpatterns = [
),
path(
"workspaces//issues/",
- GlobalViewIssuesViewSet.as_view(
+ WorkspaceViewViewSet.as_view(
{
"get": "list",
}
@@ -65,7 +63,7 @@ urlpatterns = [
),
path(
"workspaces//projects//user-favorite-views/",
- IssueViewFavoriteViewSet.as_view(
+ ViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
@@ -75,7 +73,7 @@ urlpatterns = [
),
path(
"workspaces//projects//user-favorite-views//",
- IssueViewFavoriteViewSet.as_view(
+ ViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py
index 0a959a667..27eb691e6 100644
--- a/apiserver/plane/app/views/__init__.py
+++ b/apiserver/plane/app/views/__init__.py
@@ -52,10 +52,12 @@ from .workspace import (
)
from .state import StateViewSet
from .view import (
- GlobalViewViewSet,
- GlobalViewIssuesViewSet,
- IssueViewViewSet,
- IssueViewFavoriteViewSet,
+ WorkspaceViewViewSet,
+ ProjectViewViewSet,
+ ViewFavoriteViewSet,
+ UserWorkspaceViewViewSet,
+ UserProjectViewViewSet,
+ ProjectViewViewSet,
)
from .cycle import (
CycleViewSet,
diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py
index 13acabfe8..1f852bb76 100644
--- a/apiserver/plane/app/views/search.py
+++ b/apiserver/plane/app/views/search.py
@@ -17,7 +17,7 @@ from plane.db.models import (
Cycle,
Module,
Page,
- IssueView,
+ View,
)
from plane.utils.issue_search import search_issues
@@ -161,7 +161,7 @@ class GlobalSearchEndpoint(BaseAPIView):
for field in fields:
q |= Q(**{f"{field}__icontains": query})
- issue_views = IssueView.objects.filter(
+ issue_views = View.objects.filter(
q,
project__project_projectmember__member=self.request.user,
workspace__slug=slug,
diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py
index 27f31f7a9..758496ef7 100644
--- a/apiserver/plane/app/views/view.py
+++ b/apiserver/plane/app/views/view.py
@@ -10,68 +10,308 @@ from django.db.models import (
When,
Exists,
Max,
+ Q,
)
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
-from django.db.models import Prefetch, OuterRef, Exists
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
-from . import BaseViewSet, BaseAPIView
+from . import BaseViewSet
from plane.app.serializers import (
- GlobalViewSerializer,
- IssueViewSerializer,
+ ViewSerializer,
IssueSerializer,
- IssueViewFavoriteSerializer,
+ ViewFavoriteSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
- WorkspaceViewerPermission,
- ProjectLitePermission,
)
from plane.db.models import (
Workspace,
- GlobalView,
- IssueView,
+ View,
Issue,
- IssueViewFavorite,
- IssueReaction,
+ ViewFavorite,
IssueLink,
IssueAttachment,
- IssueSubscriber,
)
from plane.utils.issue_filters import issue_filters
-from plane.utils.grouper import group_results
-class GlobalViewViewSet(BaseViewSet):
- serializer_class = IssueViewSerializer
- model = IssueView
+class UserWorkspaceViewViewSet(BaseViewSet):
+ serializer_class = ViewSerializer
+ model = View
permission_classes = [
WorkspaceEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
- serializer.save(workspace_id=workspace.id)
+ serializer.save(
+ workspace_id=workspace.id, access=0, owned_by=self.request.user
+ )
def get_queryset(self):
+ subquery = ViewFavorite.objects.filter(
+ user=self.request.user,
+ view_id=OuterRef("pk"),
+ workspace__slug=self.kwargs.get("slug"),
+ )
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
+ .filter(Q(owned_by=self.request.user) & Q(access=0))
.select_related("workspace")
+ .annotate(is_favorite=Exists(subquery))
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
)
+ def perform_update(self, serializer):
+ view = View.objects.get(pk=self.kwargs.get("pk"))
+ if view.is_locked:
+ return Response(
+ {"error": "You cannot update the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ if view.owned_by == self.request.user:
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(
+ {"error": "You cannot update the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
-class GlobalViewIssuesViewSet(BaseViewSet):
+ def lock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = True
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+ def unlock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot un lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = False
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+ def destroy(self, request, slug, pk):
+ view = View.objects.get(workspace__slug=slug, pk=pk)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot delete the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class WorkspaceViewViewSet(BaseViewSet):
+ serializer_class = ViewSerializer
+ model = View
+ permission_classes = [
+ WorkspaceEntityPermission,
+ ]
+
+ def perform_create(self, serializer):
+ workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
+ serializer.save(workspace_id=workspace.id, owned_by=self.request.user)
+
+ def get_queryset(self):
+ subquery = ViewFavorite.objects.filter(
+ user=self.request.user,
+ view_id=OuterRef("pk"),
+ workspace__slug=self.kwargs.get("slug"),
+ )
+ return self.filter_queryset(
+ super()
+ .get_queryset()
+ .filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(project__isnull=True)
+ .filter(Q(access=1))
+ .select_related("workspace")
+ .annotate(is_favorite=Exists(subquery))
+ .order_by(self.request.GET.get("order_by", "-created_at"))
+ .distinct()
+ )
+
+ def lock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = True
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+ def unlock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot un lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = False
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+
+class UserProjectViewViewSet(BaseViewSet):
+ serializer_class = ViewSerializer
+ model = View
+ permission_classes = [
+ ProjectEntityPermission,
+ ]
+
+ def perform_create(self, serializer):
+ workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
+ serializer.save(
+ workspace_id=workspace.id,
+ project_id=self.kwargs.get("project_id"),
+ access=0,
+ owned_by=self.request.user,
+ )
+
+ def get_queryset(self):
+ subquery = ViewFavorite.objects.filter(
+ user=self.request.user,
+ view_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("workspace")
+ .annotate(is_favorite=Exists(subquery))
+ .order_by(self.request.GET.get("order_by", "-created_at"))
+ .distinct()
+ )
+
+ def perform_update(self, serializer):
+ view = View.objects.get(pk=self.kwargs.get("pk"))
+ if view.owned_by == self.request.user:
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(
+ {"error": "You cannot update the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+
+ def destroy(self, request, slug, project_id, pk):
+ view = View.objects.get(
+ workspace__slug=slug, project_id=project_id, pk=pk
+ )
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot delete the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+ def lock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = True
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+ def unlock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot un lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = False
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+
+class ProjectViewViewSet(BaseViewSet):
+ serializer_class = ViewSerializer
+ model = View
+ permission_classes = [
+ ProjectEntityPermission,
+ ]
+
+ def perform_create(self, serializer):
+ workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
+ serializer.save(
+ workspace_id=workspace.id,
+ project_id=self.kwargs.get("project_id"),
+ owned_by=self.request.user,
+ )
+
+ def get_queryset(self):
+ subquery = ViewFavorite.objects.filter(
+ user=self.request.user,
+ view_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(access=1))
+ .select_related("workspace")
+ .annotate(is_favorite=Exists(subquery))
+ .order_by(self.request.GET.get("order_by", "-created_at"))
+ .distinct()
+ )
+
+ def lock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = True
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+ def unlock(self, request, slug, pk):
+ view = View.objects.get(pk=pk, workspace__slug=slug)
+ if view.owned_by != self.request.user:
+ return Response(
+ {"error": "You cannot un lock the view"},
+ status=status.HTTP_403_FORBIDDEN,
+ )
+ view.is_locked = False
+ view.save()
+ return Response(status=status.HTTP_200_OK)
+
+
+class WorkspaceViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
@@ -87,41 +327,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
+ .filter(project__project_projectmember__member=self.request.user)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
- .prefetch_related(
- Prefetch(
- "issue_reactions",
- queryset=IssueReaction.objects.select_related("actor"),
- )
- )
- )
-
- @method_decorator(gzip_page)
- def list(self, request, slug):
- filters = issue_filters(request.query_params, "GET")
- fields = [
- field
- for field in request.GET.get("fields", "").split(",")
- if field
- ]
-
- # 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 = (
- self.get_queryset()
- .filter(**filters)
- .filter(project__project_projectmember__member=self.request.user)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@@ -147,6 +355,29 @@ class GlobalViewIssuesViewSet(BaseViewSet):
)
)
+ @method_decorator(gzip_page)
+ def list(self, request, slug):
+ filters = issue_filters(request.query_params, "GET")
+ fields = [
+ field
+ for field in request.GET.get("fields", "").split(",")
+ if field
+ ]
+
+ # 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 = (self.get_queryset().filter(**filters))
+
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
@@ -213,52 +444,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
-class IssueViewViewSet(BaseViewSet):
- serializer_class = IssueViewSerializer
- model = IssueView
- permission_classes = [
- ProjectEntityPermission,
- ]
-
- def perform_create(self, serializer):
- serializer.save(project_id=self.kwargs.get("project_id"))
-
- def get_queryset(self):
- subquery = IssueViewFavorite.objects.filter(
- user=self.request.user,
- view_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)
- .select_related("project")
- .select_related("workspace")
- .annotate(is_favorite=Exists(subquery))
- .order_by("-is_favorite", "name")
- .distinct()
- )
-
- def list(self, request, slug, project_id):
- queryset = self.get_queryset()
- fields = [
- field
- for field in request.GET.get("fields", "").split(",")
- if field
- ]
- views = IssueViewSerializer(
- queryset, many=True, fields=fields if fields else None
- ).data
- return Response(views, status=status.HTTP_200_OK)
-
-
-class IssueViewFavoriteViewSet(BaseViewSet):
- serializer_class = IssueViewFavoriteSerializer
- model = IssueViewFavorite
+class ViewFavoriteViewSet(BaseViewSet):
+ serializer_class = ViewFavoriteSerializer
+ model = ViewFavorite
def get_queryset(self):
return self.filter_queryset(
@@ -270,18 +458,18 @@ class IssueViewFavoriteViewSet(BaseViewSet):
)
def create(self, request, slug, project_id):
- serializer = IssueViewFavoriteSerializer(data=request.data)
+ serializer = ViewFavoriteSerializer(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)
def destroy(self, request, slug, project_id, view_id):
- view_favourite = IssueViewFavorite.objects.get(
+ view_favorite = ViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
- view_favourite.delete()
+ view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apiserver/plane/db/migrations/0053_auto_20240102_1315.py b/apiserver/plane/db/migrations/0053_auto_20240102_1315.py
index 32b5ad2d5..472421abd 100644
--- a/apiserver/plane/db/migrations/0053_auto_20240102_1315.py
+++ b/apiserver/plane/db/migrations/0053_auto_20240102_1315.py
@@ -1,11 +1,12 @@
# Generated by Django 4.2.7 on 2024-01-02 13:15
-from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
+from plane.db.models import ProjectMember
from django.db import migrations
def workspace_user_properties(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
+ WorkspaceUserProperties = apps.get_model("db", "WorkspaceUserProperties")
updated_workspace_user_properties = []
for workspace_members in WorkspaceMember.objects.all():
updated_workspace_user_properties.append(
@@ -49,6 +50,7 @@ def project_user_properties(apps, schema_editor):
def issue_view(apps, schema_editor):
GlobalView = apps.get_model("db", "GlobalView")
+ IssueView = apps.get_model("db", "IssueView")
updated_issue_views = []
for global_view in GlobalView.objects.all():
diff --git a/apiserver/plane/db/migrations/0059_alter_issueviewfavorite_project_and_more.py b/apiserver/plane/db/migrations/0059_alter_issueviewfavorite_project_and_more.py
new file mode 100644
index 000000000..4dcfd993e
--- /dev/null
+++ b/apiserver/plane/db/migrations/0059_alter_issueviewfavorite_project_and_more.py
@@ -0,0 +1,70 @@
+# Generated by Django 4.2.7 on 2024-01-30 07:49
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+from django.db.models import F
+
+def views_owned_by(apps, schema_editor):
+ View = apps.get_model("db", "View")
+ View.objects.update(owned_by=F('created_by'))
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('db', '0058_alter_moduleissue_issue_and_more'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='IssueView',
+ new_name='View',
+ ),
+ migrations.AlterModelTable(
+ name='view',
+ table='views',
+ ),
+ migrations.RenameModel(
+ old_name='IssueViewFavorite',
+ new_name='ViewFavorite',
+ ),
+ migrations.AlterField(
+ model_name='viewfavorite',
+ name='project',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
+ ),
+ migrations.AlterModelTable(
+ name='workspaceuserproperties',
+ table='workspace_user_properties',
+ ),
+ migrations.AlterModelOptions(
+ name='view',
+ options={'ordering': ('-created_at',), 'verbose_name': 'View', 'verbose_name_plural': 'Views'},
+ ),
+ migrations.AddField(
+ model_name='view',
+ name='is_locked',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='view',
+ name='is_pinned',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='view',
+ name='owned_by',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='views', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='view',
+ name='access',
+ field=models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public'), (2, 'Shared')], default=1),
+ ),
+ migrations.AlterField(
+ model_name='viewfavorite',
+ name='view',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view_favorites', to='db.view'),
+ ),
+ migrations.RunPython(views_owned_by)
+ ]
diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py
index d9096bd01..bfe4741f6 100644
--- a/apiserver/plane/db/models/__init__.py
+++ b/apiserver/plane/db/models/__init__.py
@@ -52,7 +52,7 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
-from .view import GlobalView, IssueView, IssueViewFavorite
+from .view import View, ViewFavorite
from .module import (
Module,
diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py
index 13500b5a4..cf4c2e34b 100644
--- a/apiserver/plane/db/models/view.py
+++ b/apiserver/plane/db/models/view.py
@@ -3,7 +3,7 @@ from django.db import models
from django.conf import settings
# Module import
-from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
+from . import BaseModel, WorkspaceBaseModel
def get_default_filters():
@@ -84,7 +84,7 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
-class IssueView(WorkspaceBaseModel):
+class View(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
@@ -94,29 +94,44 @@ class IssueView(WorkspaceBaseModel):
default=get_default_display_properties
)
access = models.PositiveSmallIntegerField(
- default=1, choices=((0, "Private"), (1, "Public"))
+ default=1, choices=((0, "Private"), (1, "Public"), (2, "Shared"))
+ )
+ owned_by = models.ForeignKey(
+ settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="views", null=True, blank=True
)
sort_order = models.FloatField(default=65535)
+ is_locked = models.BooleanField(default=False)
+ is_pinned = models.BooleanField(default=False)
class Meta:
- verbose_name = "Issue View"
- verbose_name_plural = "Issue Views"
- db_table = "issue_views"
+ verbose_name = "View"
+ verbose_name_plural = "Views"
+ db_table = "views"
ordering = ("-created_at",)
+
+ def save(self, *args, **kwargs):
+ if self._state.adding:
+ largest_sort_order = View.objects.filter(
+ workspace=self.workspace
+ ).aggregate(largest=models.Max("sort_order"))["largest"]
+ if largest_sort_order is not None:
+ self.sort_order = largest_sort_order + 10000
+
+ super(View, self).save(*args, **kwargs)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.project.name}>"
-class IssueViewFavorite(ProjectBaseModel):
+class ViewFavorite(WorkspaceBaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="user_view_favorites",
)
view = models.ForeignKey(
- "db.IssueView", on_delete=models.CASCADE, related_name="view_favorites"
+ "db.View", on_delete=models.CASCADE, related_name="view_favorites"
)
class Meta:
diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py
index 7e5d6d90b..e2716f778 100644
--- a/apiserver/plane/db/models/workspace.py
+++ b/apiserver/plane/db/models/workspace.py
@@ -326,7 +326,7 @@ class WorkspaceUserProperties(BaseModel):
unique_together = ["workspace", "user"]
verbose_name = "Workspace User Property"
verbose_name_plural = "Workspace User Property"
- db_table = "Workspace_user_properties"
+ db_table = "workspace_user_properties"
ordering = ("-created_at",)
def __str__(self):
diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py
index 444248382..f03209250 100644
--- a/apiserver/plane/settings/common.py
+++ b/apiserver/plane/settings/common.py
@@ -282,10 +282,8 @@ if REDIS_SSL:
redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_BROKER_URL = broker_url
- CELERY_RESULT_BACKEND = broker_url
else:
CELERY_BROKER_URL = REDIS_URL
- CELERY_RESULT_BACKEND = REDIS_URL
CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task",