From 2c4541fdb62221af7861c8f2e9617bbef24861a2 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 14:26:10 +0530 Subject: [PATCH 1/7] Crud for wuick links --- apiserver/plane/app/serializers/__init__.py | 1 + apiserver/plane/app/serializers/workspace.py | 26 +++++++++++++++ apiserver/plane/app/urls/workspace.py | 13 ++++++++ apiserver/plane/app/views/__init__.py | 1 + .../plane/app/views/workspace/quick_link.py | 33 +++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 apiserver/plane/app/views/workspace/quick_link.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index cd9adb939ee..0cbf5938482 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -19,6 +19,7 @@ WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, WorkspaceUserPropertiesSerializer, + WorkspaceUserLinkSerializer ) from .project import ( ProjectSerializer, diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 49cd55bf7f7..83303c70a99 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -11,9 +11,13 @@ WorkspaceMemberInvite, WorkspaceTheme, WorkspaceUserProperties, + WorkspaceUserLink ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS +# Django imports +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError class WorkSpaceSerializer(DynamicBaseSerializer): owner = UserLiteSerializer(read_only=True) @@ -106,3 +110,25 @@ class Meta: model = WorkspaceUserProperties fields = "__all__" read_only_fields = ["workspace", "user"] + +class WorkspaceUserLinkSerializer(BaseSerializer): + class Meta: + model = WorkspaceUserLink + fields = "__all__" + read_only_fields = ["workspace", "owner"] + + def to_internal_value(self, data): + url = data.get("url", "") + if url and not url.startswith(("http://", "https://")): + data["url"] = "http://" + url + + return super().to_internal_value(data) + + def validate_url(self, value): + url_validator = URLValidator() + try: + url_validator(value) + except ValidationError: + raise serializers.ValidationError({"error": "Invalid URL format."}) + + return value diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index d91fdb60bca..85538478e42 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -27,6 +27,7 @@ WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, WorkspaceDraftIssueViewSet, + QuickLinkViewSet ) @@ -213,4 +214,16 @@ WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}), name="workspace-drafts-issues", ), + + # quick link + path( + "workspaces//quick-links/", + QuickLinkViewSet.as_view({"get": "list", "post": "create"}), + name="workspace-quick-links " + ), + path( + "workspaces//quick-links//", + QuickLinkViewSet.as_view({"patch": "partial_update", "delete": "destroy"}), + name="workspace-quick-links" + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 56ea78b4130..412510d4fce 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -72,6 +72,7 @@ from .workspace.estimate import WorkspaceEstimatesEndpoint from .workspace.module import WorkspaceModulesEndpoint from .workspace.cycle import WorkspaceCyclesEndpoint +from .workspace.quick_link import QuickLinkViewSet from .state.base import StateViewSet from .view.base import ( diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py new file mode 100644 index 00000000000..6d75aaf6a14 --- /dev/null +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -0,0 +1,33 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.db.models import (WorkspaceUserLink, Workspace) +from plane.app.serializers import WorkspaceUserLinkSerializer +from ..base import BaseViewSet +from plane.app.permissions import allow_permission, ROLE + +class QuickLinkViewSet(BaseViewSet): + model = WorkspaceUserLink + + def get_serializer_class(self): + return WorkspaceUserLinkSerializer + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def create(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + serializer = WorkspaceUserLinkSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(workspace_id=workspace.id, owner=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def partial_update(self, request, slug, pk): + quick_link = WorkspaceUserLink.objects.filter(pk=pk).first() + serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 274612cd4a8d9efb1a110f66da630ec0c57786fa Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 14:43:20 +0530 Subject: [PATCH 2/7] Validate quick link existence --- .../plane/app/views/workspace/quick_link.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index 6d75aaf6a14..54d6a8ed3ab 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -23,11 +23,18 @@ def create(self, request, slug): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def partial_update(self, request, slug, pk): quick_link = WorkspaceUserLink.objects.filter(pk=pk).first() - serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + if quick_link: + serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND) + + + From ecfbec8e2b9afcfcb4a984bd0d74afec10f4e25b Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 16:26:29 +0530 Subject: [PATCH 3/7] Add custom method for destroy and retrieve --- apiserver/plane/app/urls/workspace.py | 2 +- .../plane/app/views/workspace/quick_link.py | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 85538478e42..9637b1eb1d9 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -223,7 +223,7 @@ ), path( "workspaces//quick-links//", - QuickLinkViewSet.as_view({"patch": "partial_update", "delete": "destroy"}), + QuickLinkViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}), name="workspace-quick-links" ) ] diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index 54d6a8ed3ab..3b98f97255f 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -13,7 +13,7 @@ class QuickLinkViewSet(BaseViewSet): def get_serializer_class(self): return WorkspaceUserLinkSerializer - + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def create(self, request, slug): workspace = Workspace.objects.get(slug=slug) @@ -36,5 +36,28 @@ def partial_update(self, request, slug, pk): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def retrieve(self, request, slug, pk): + print("Print retrieve method") + quick_link = WorkspaceUserLink.objects.get(pk=pk) + if not quick_link: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = WorkspaceUserLinkSerializer(quick_link) + return Response(serializer.data, status=status.HTTP_200_OK) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def destroy(self, request, slug, pk): + quick_link = WorkspaceUserLink.objects.filter(pk=pk) + + if quick_link: + quick_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND) + + From a6a0da0817583351267b510371298aec65dbd650 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 16:46:48 +0530 Subject: [PATCH 4/7] Add List method --- .../plane/app/views/workspace/quick_link.py | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index 3b98f97255f..ccbce1d7f23 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -26,7 +26,7 @@ def create(self, request, slug): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def partial_update(self, request, slug, pk): - quick_link = WorkspaceUserLink.objects.filter(pk=pk).first() + quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug).first() if quick_link: serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True) @@ -38,26 +38,31 @@ def partial_update(self, request, slug, pk): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def retrieve(self, request, slug, pk): - print("Print retrieve method") - quick_link = WorkspaceUserLink.objects.get(pk=pk) - - if not quick_link: + try: + quick_link = WorkspaceUserLink.objects.get( + pk=pk, + workspace__slug=slug + ) + serializer = WorkspaceUserLinkSerializer(quick_link) + return Response(serializer.data, status=status.HTTP_200_OK) + except WorkspaceUserLink.DoesNotExist: return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, + {"error": "Quick link not found."}, + status=status.HTTP_404_NOT_FOUND ) - serializer = WorkspaceUserLinkSerializer(quick_link) - return Response(serializer.data, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def destroy(self, request, slug, pk): - quick_link = WorkspaceUserLink.objects.filter(pk=pk) + quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug).first() - if quick_link: + if quick_link: quick_link.delete() return Response(status=status.HTTP_204_NO_CONTENT) return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND) - - + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def list(self, request, slug): + quick_links = WorkspaceUserLink.objects.all() + + serializer = WorkspaceUserLinkSerializer(quick_links, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file From 749f1ee39dd3306ccfb3b564572fdf1ca8c67838 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 16:54:46 +0530 Subject: [PATCH 5/7] Remove print statements --- apiserver/plane/app/views/workspace/quick_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index ccbce1d7f23..1948b7da6c3 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -62,7 +62,7 @@ def destroy(self, request, slug, pk): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - quick_links = WorkspaceUserLink.objects.all() + quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug) serializer = WorkspaceUserLinkSerializer(quick_links, many=True) return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file From 0eee395e72fa53d399b354b7e8b2be4f9076d198 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 16:57:25 +0530 Subject: [PATCH 6/7] List all the workspace quick links --- apiserver/plane/app/views/workspace/quick_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index 1948b7da6c3..40249828922 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -63,6 +63,6 @@ def destroy(self, request, slug, pk): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug) - + serializer = WorkspaceUserLinkSerializer(quick_links, many=True) return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file From ab0e8fe9a706df35b5b22b6541d4ad5aa36d4989 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 31 Dec 2024 14:35:03 +0530 Subject: [PATCH 7/7] Filter by user --- .../plane/app/views/workspace/quick_link.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/app/views/workspace/quick_link.py b/apiserver/plane/app/views/workspace/quick_link.py index 40249828922..30a21ed4594 100644 --- a/apiserver/plane/app/views/workspace/quick_link.py +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -3,7 +3,7 @@ from rest_framework.response import Response # Module imports -from plane.db.models import (WorkspaceUserLink, Workspace) +from plane.db.models import WorkspaceUserLink, Workspace from plane.app.serializers import WorkspaceUserLinkSerializer from ..base import BaseViewSet from plane.app.permissions import allow_permission, ROLE @@ -16,8 +16,9 @@ def get_serializer_class(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def create(self, request, slug): - workspace = Workspace.objects.get(slug=slug) + workspace = Workspace.objects.get(slug=slug, owner=request.user) serializer = WorkspaceUserLinkSerializer(data=request.data) + if serializer.is_valid(): serializer.save(workspace_id=workspace.id, owner=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -26,7 +27,7 @@ def create(self, request, slug): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def partial_update(self, request, slug, pk): - quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug).first() + quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug, owner=request.user).first() if quick_link: serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True) @@ -41,7 +42,8 @@ def retrieve(self, request, slug, pk): try: quick_link = WorkspaceUserLink.objects.get( pk=pk, - workspace__slug=slug + workspace__slug=slug, + owner=request.user ) serializer = WorkspaceUserLinkSerializer(quick_link) return Response(serializer.data, status=status.HTTP_200_OK) @@ -53,16 +55,13 @@ def retrieve(self, request, slug, pk): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def destroy(self, request, slug, pk): - quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug).first() - - if quick_link: - quick_link.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND) + quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user) + quick_link.delete() + return Response(status=status.HTTP_204_NO_CONTENT) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug) + quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug, owner=request.user) serializer = WorkspaceUserLinkSerializer(quick_links, many=True) return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file