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..9637b1eb1d9 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({"get": "retrieve", "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..30a21ed4594 --- /dev/null +++ b/apiserver/plane/app/views/workspace/quick_link.py @@ -0,0 +1,67 @@ +# 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, 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) + 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, workspace__slug=slug, owner=request.user).first() + + 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) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def retrieve(self, request, slug, pk): + try: + quick_link = WorkspaceUserLink.objects.get( + pk=pk, + workspace__slug=slug, + owner=request.user + ) + serializer = WorkspaceUserLinkSerializer(quick_link) + return Response(serializer.data, status=status.HTTP_200_OK) + except WorkspaceUserLink.DoesNotExist: + return Response( + {"error": "Quick link not found."}, + status=status.HTTP_404_NOT_FOUND + ) + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def destroy(self, request, slug, pk): + 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, owner=request.user) + + serializer = WorkspaceUserLinkSerializer(quick_links, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file