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",