fix: user and workspace views

This commit is contained in:
NarayanBavisetti 2024-01-31 11:50:40 +05:30
parent f63a04c1ab
commit a77839a942
14 changed files with 459 additions and 418 deletions

View File

@ -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"<p>{issue.description}</p>"
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"<p>{issue_comment.comment_stripped}</p>"
)
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")

View File

@ -36,9 +36,8 @@ from .project import (
) )
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .view import ( from .view import (
GlobalViewSerializer, ViewSerializer,
IssueViewSerializer, ViewFavoriteSerializer,
IssueViewFavoriteSerializer,
) )
from .cycle import ( from .cycle import (
CycleSerializer, CycleSerializer,

View File

@ -3,69 +3,33 @@ from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer, DynamicBaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer from plane.db.models import View, ViewFavorite
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer): class ViewSerializer(DynamicBaseSerializer):
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):
is_favorite = serializers.BooleanField(read_only=True) 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: class Meta:
model = IssueView model = View
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",
"query", "query",
"access",
] ]
def create(self, validated_data): def create(self, validated_data):
query_params = validated_data.get("query_data", {}) query_params = validated_data.get("filters", {})
if bool(query_params): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
validated_data["query"] = {} validated_data["query"] = {}
return IssueView.objects.create(**validated_data) return View.objects.create(**validated_data)
def update(self, instance, 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): if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST") validated_data["query"] = issue_filters(query_params, "POST")
else: else:
@ -74,11 +38,10 @@ class IssueViewSerializer(DynamicBaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class IssueViewFavoriteSerializer(BaseSerializer): class ViewFavoriteSerializer(BaseSerializer):
view_detail = IssueViewSerializer(source="issue_view", read_only=True)
class Meta: class Meta:
model = IssueViewFavorite model = ViewFavorite
fields = "__all__" fields = "__all__"
read_only_fields = [ read_only_fields = [
"workspace", "workspace",

View File

@ -14,6 +14,8 @@ from plane.app.views import (
UserActivityGraphEndpoint, UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint, UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint, UserWorkspaceDashboardEndpoint,
UserWorkspaceViewViewSet,
UserProjectViewViewSet,
## End Workspaces ## End Workspaces
) )
@ -95,5 +97,47 @@ urlpatterns = [
SetUserPasswordEndpoint.as_view(), SetUserPasswordEndpoint.as_view(),
name="set-password", name="set-password",
), ),
path(
"users/me/workspaces/<str:slug>/views/",
UserWorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-workspace-views",
),
path(
"users/me/workspaces/<str:slug>/views/<uuid:pk>/",
UserWorkspaceViewViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="user-workspace-views",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/",
UserProjectViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-project-views",
),
path(
"users/me/workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
UserProjectViewViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="user-project-views",
),
## End User Graph ## End User Graph
] ]

View File

@ -2,17 +2,16 @@ from django.urls import path
from plane.app.views import ( from plane.app.views import (
IssueViewViewSet, ProjectViewViewSet,
GlobalViewViewSet, WorkspaceViewViewSet,
GlobalViewIssuesViewSet, ViewFavoriteViewSet,
IssueViewFavoriteViewSet,
) )
urlpatterns = [ urlpatterns = [
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/", "workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view( ProjectViewViewSet.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
@ -22,7 +21,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
IssueViewViewSet.as_view( ProjectViewViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
"put": "update", "put": "update",
@ -34,7 +33,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/views/", "workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view( WorkspaceViewViewSet.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
@ -44,10 +43,9 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/views/<uuid:pk>/", "workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view( WorkspaceViewViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
"put": "update",
"patch": "partial_update", "patch": "partial_update",
"delete": "destroy", "delete": "destroy",
} }
@ -56,7 +54,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/issues/", "workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view( WorkspaceViewViewSet.as_view(
{ {
"get": "list", "get": "list",
} }
@ -65,7 +63,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view( ViewFavoriteViewSet.as_view(
{ {
"get": "list", "get": "list",
"post": "create", "post": "create",
@ -75,7 +73,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view( ViewFavoriteViewSet.as_view(
{ {
"delete": "destroy", "delete": "destroy",
} }

View File

@ -52,10 +52,12 @@ from .workspace import (
) )
from .state import StateViewSet from .state import StateViewSet
from .view import ( from .view import (
GlobalViewViewSet, WorkspaceViewViewSet,
GlobalViewIssuesViewSet, ProjectViewViewSet,
IssueViewViewSet, ViewFavoriteViewSet,
IssueViewFavoriteViewSet, UserWorkspaceViewViewSet,
UserProjectViewViewSet,
ProjectViewViewSet,
) )
from .cycle import ( from .cycle import (
CycleViewSet, CycleViewSet,

View File

@ -17,7 +17,7 @@ from plane.db.models import (
Cycle, Cycle,
Module, Module,
Page, Page,
IssueView, View,
) )
from plane.utils.issue_search import search_issues from plane.utils.issue_search import search_issues
@ -161,7 +161,7 @@ class GlobalSearchEndpoint(BaseAPIView):
for field in fields: for field in fields:
q |= Q(**{f"{field}__icontains": query}) q |= Q(**{f"{field}__icontains": query})
issue_views = IssueView.objects.filter( issue_views = View.objects.filter(
q, q,
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
workspace__slug=slug, workspace__slug=slug,

View File

@ -10,68 +10,308 @@ from django.db.models import (
When, When,
Exists, Exists,
Max, Max,
Q,
) )
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db.models import Prefetch, OuterRef, Exists
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# Module imports # Module imports
from . import BaseViewSet, BaseAPIView from . import BaseViewSet
from plane.app.serializers import ( from plane.app.serializers import (
GlobalViewSerializer, ViewSerializer,
IssueViewSerializer,
IssueSerializer, IssueSerializer,
IssueViewFavoriteSerializer, ViewFavoriteSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
WorkspaceEntityPermission, WorkspaceEntityPermission,
ProjectEntityPermission, ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
) )
from plane.db.models import ( from plane.db.models import (
Workspace, Workspace,
GlobalView, View,
IssueView,
Issue, Issue,
IssueViewFavorite, ViewFavorite,
IssueReaction,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
IssueSubscriber,
) )
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet): class UserWorkspaceViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer serializer_class = ViewSerializer
model = IssueView model = View
permission_classes = [ permission_classes = [
WorkspaceEntityPermission, WorkspaceEntityPermission,
] ]
def perform_create(self, serializer): def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug")) 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): 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( return self.filter_queryset(
super() super()
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True) .filter(project__isnull=True)
.filter(Q(owned_by=self.request.user) & Q(access=0))
.select_related("workspace") .select_related("workspace")
.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"))
.distinct() .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 = [ permission_classes = [
WorkspaceEntityPermission, WorkspaceEntityPermission,
] ]
@ -87,41 +327,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.values("count") .values("count")
) )
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__project_projectmember__member=self.request.user)
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .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(cycle_id=F("issue_cycle__cycle_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) 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 # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = (
@ -213,52 +444,9 @@ class GlobalViewIssuesViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet): class ViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewSerializer serializer_class = ViewFavoriteSerializer
model = IssueView model = ViewFavorite
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
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
@ -270,18 +458,18 @@ class IssueViewFavoriteViewSet(BaseViewSet):
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
serializer = IssueViewFavoriteSerializer(data=request.data) serializer = ViewFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(user=request.user, project_id=project_id) serializer.save(user=request.user, project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, view_id): def destroy(self, request, slug, project_id, view_id):
view_favourite = IssueViewFavorite.objects.get( view_favorite = ViewFavorite.objects.get(
project=project_id, project=project_id,
user=request.user, user=request.user,
workspace__slug=slug, workspace__slug=slug,
view_id=view_id, view_id=view_id,
) )
view_favourite.delete() view_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -1,11 +1,12 @@
# Generated by Django 4.2.7 on 2024-01-02 13:15 # 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 from django.db import migrations
def workspace_user_properties(apps, schema_editor): def workspace_user_properties(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember") WorkspaceMember = apps.get_model("db", "WorkspaceMember")
WorkspaceUserProperties = apps.get_model("db", "WorkspaceUserProperties")
updated_workspace_user_properties = [] updated_workspace_user_properties = []
for workspace_members in WorkspaceMember.objects.all(): for workspace_members in WorkspaceMember.objects.all():
updated_workspace_user_properties.append( updated_workspace_user_properties.append(
@ -49,6 +50,7 @@ def project_user_properties(apps, schema_editor):
def issue_view(apps, schema_editor): def issue_view(apps, schema_editor):
GlobalView = apps.get_model("db", "GlobalView") GlobalView = apps.get_model("db", "GlobalView")
IssueView = apps.get_model("db", "IssueView")
updated_issue_views = [] updated_issue_views = []
for global_view in GlobalView.objects.all(): for global_view in GlobalView.objects.all():

View File

@ -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)
]

View File

@ -52,7 +52,7 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
from .view import GlobalView, IssueView, IssueViewFavorite from .view import View, ViewFavorite
from .module import ( from .module import (
Module, Module,

View File

@ -3,7 +3,7 @@ from django.db import models
from django.conf import settings from django.conf import settings
# Module import # Module import
from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel from . import BaseModel, WorkspaceBaseModel
def get_default_filters(): def get_default_filters():
@ -84,7 +84,7 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>" return f"{self.name} <{self.workspace.name}>"
class IssueView(WorkspaceBaseModel): class View(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name") name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True) description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query") query = models.JSONField(verbose_name="View Query")
@ -94,29 +94,44 @@ class IssueView(WorkspaceBaseModel):
default=get_default_display_properties default=get_default_display_properties
) )
access = models.PositiveSmallIntegerField( 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) sort_order = models.FloatField(default=65535)
is_locked = models.BooleanField(default=False)
is_pinned = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name = "Issue View" verbose_name = "View"
verbose_name_plural = "Issue Views" verbose_name_plural = "Views"
db_table = "issue_views" db_table = "views"
ordering = ("-created_at",) 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): def __str__(self):
"""Return name of the View""" """Return name of the View"""
return f"{self.name} <{self.project.name}>" return f"{self.name} <{self.project.name}>"
class IssueViewFavorite(ProjectBaseModel): class ViewFavorite(WorkspaceBaseModel):
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="user_view_favorites", related_name="user_view_favorites",
) )
view = models.ForeignKey( 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: class Meta:

View File

@ -326,7 +326,7 @@ class WorkspaceUserProperties(BaseModel):
unique_together = ["workspace", "user"] unique_together = ["workspace", "user"]
verbose_name = "Workspace User Property" verbose_name = "Workspace User Property"
verbose_name_plural = "Workspace User Property" verbose_name_plural = "Workspace User Property"
db_table = "Workspace_user_properties" db_table = "workspace_user_properties"
ordering = ("-created_at",) ordering = ("-created_at",)
def __str__(self): def __str__(self):

View File

@ -282,10 +282,8 @@ if REDIS_SSL:
redis_url = os.environ.get("REDIS_URL") redis_url = os.environ.get("REDIS_URL")
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}" broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
CELERY_BROKER_URL = broker_url CELERY_BROKER_URL = broker_url
CELERY_RESULT_BACKEND = broker_url
else: else:
CELERY_BROKER_URL = REDIS_URL CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_IMPORTS = ( CELERY_IMPORTS = (
"plane.bgtasks.issue_automation_task", "plane.bgtasks.issue_automation_task",