From 2c4541fdb62221af7861c8f2e9617bbef24861a2 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Mon, 30 Dec 2024 14:26:10 +0530 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 bf36eb0102872a3b5413af7644c6569ae23ba8c6 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 31 Dec 2024 17:39:34 +0530 Subject: [PATCH 07/13] feat: endpoint to get recently active items --- apiserver/plane/app/serializers/__init__.py | 3 +- apiserver/plane/app/serializers/workspace.py | 84 ++++++++++++++++++- apiserver/plane/app/urls/workspace.py | 8 +- apiserver/plane/app/views/__init__.py | 1 + .../plane/app/views/workspace/recent_visit.py | 23 +++++ 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 apiserver/plane/app/views/workspace/recent_visit.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 0cbf5938482..134eee891ab 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -19,7 +19,8 @@ WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, WorkspaceUserPropertiesSerializer, - WorkspaceUserLinkSerializer + WorkspaceUserLinkSerializer, + WorkspaceRecentVisitSerializer ) from .project import ( ProjectSerializer, diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 83303c70a99..224cd81aa79 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -1,5 +1,7 @@ # Third party imports from rest_framework import serializers +from rest_framework import status +from rest_framework.response import Response # Module imports from .base import BaseSerializer, DynamicBaseSerializer @@ -11,7 +13,12 @@ WorkspaceMemberInvite, WorkspaceTheme, WorkspaceUserProperties, - WorkspaceUserLink + WorkspaceUserLink, + UserRecentVisit, + Issue, + Page, + Project, + ProjectMember, ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS @@ -132,3 +139,78 @@ def validate_url(self, value): raise serializers.ValidationError({"error": "Invalid URL format."}) return value + +class IssueRecentVisitSerializer(serializers.ModelSerializer): + class Meta: + model = Issue + fields = ["name", "state", "priority", "assignees"] + +class ProjectMemberSerializer(BaseSerializer): + member = UserLiteSerializer(read_only=True) + + class Meta: + model = ProjectMember + fields = ["member"] + +class ProjectRecentVisitSerializer(serializers.ModelSerializer): + project_members = serializers.SerializerMethodField() + + class Meta: + model = Project + fields = ["id", "name", "logo_props", "project_members"] + + def get_project_members(self, obj): + members = ProjectMember.objects.filter(project_id=obj.id) + + serializer = ProjectMemberSerializer(members, many=True) + return serializer.data + +class PageRecentVisitSerializer(serializers.ModelSerializer): + project_id = serializers.SerializerMethodField() + + class Meta: + model = Page + fields = ["id", "name", "logo_props", "project_id", "owned_by"] + + def get_project_id(self, obj): + project = ( + obj.projects.first() + ) + return project.id if project else None + +def get_entity_model_and_serializer(entity_type): + entity_map = { + "issue": (Issue, IssueRecentVisitSerializer), + "page": (Page, PageRecentVisitSerializer), + "project": (Project, ProjectRecentVisitSerializer) + } + return entity_map.get(entity_type, (None, None)) + +class WorkspaceRecentVisitSerializer(BaseSerializer): + entity_data = serializers.SerializerMethodField() + + class Meta: + model = UserRecentVisit + fields = [ + "id", + "entity_name", + "entity_identifier", + "entity_data", + "visited_at" + ] + read_only_fields = ["workspace", "owner", "created_by", "updated_by"] + + def get_entity_data(self, obj): + entity_name = obj.entity_name + entity_identifier = obj.entity_identifier + + entity_model, entity_serializer = get_entity_model_and_serializer(entity_name) + + if entity_model and entity_serializer: + try: + entity = entity_model.objects.get(pk=entity_identifier) + + return entity_serializer(entity).data + except entity_model.DoesNotExist: + return None + return None diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 9637b1eb1d9..e1611a5b105 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -27,7 +27,8 @@ WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, WorkspaceDraftIssueViewSet, - QuickLinkViewSet + QuickLinkViewSet, + UserRecentVisitViewSet ) @@ -225,5 +226,10 @@ "workspaces//quick-links//", QuickLinkViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}), name="workspace-quick-links" + ), + path( + "workspaces//recent-visits/", + UserRecentVisitViewSet.as_view({"get": "list"}), + name="workspace-recent-visits" ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 412510d4fce..77df6d8ffdc 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -45,6 +45,7 @@ WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, ) +from .workspace.recent_visit import UserRecentVisitViewSet from .workspace.member import ( WorkSpaceMemberViewSet, diff --git a/apiserver/plane/app/views/workspace/recent_visit.py b/apiserver/plane/app/views/workspace/recent_visit.py new file mode 100644 index 00000000000..4491a19a8c9 --- /dev/null +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -0,0 +1,23 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +from plane.db.models import UserRecentVisit +from plane.app.serializers import WorkspaceRecentVisitSerializer + +# Modules imports +from ..base import BaseViewSet +from plane.app.permissions import allow_permission, ROLE + +class UserRecentVisitViewSet(BaseViewSet): + model = UserRecentVisit + + def get_serializer_class(self): + return WorkspaceRecentVisitSerializer + + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") + def list(self, request, slug): + user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) + serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) From 3c5c892f84b02306ba77a4b5b0d192f8ab9bcd4a Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 31 Dec 2024 18:03:08 +0530 Subject: [PATCH 08/13] Resolve conflicts --- apiserver/plane/app/serializers/favorite.py | 1 - apiserver/plane/app/serializers/workspace.py | 97 ++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index 940b8ee8284..18f92f3ea2a 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -53,7 +53,6 @@ def get_entity_model_and_serializer(entity_type): } return entity_map.get(entity_type, (None, None)) - class UserFavoriteSerializer(serializers.ModelSerializer): entity_data = serializers.SerializerMethodField() diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index e8bfda1bbd2..224cd81aa79 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -117,3 +117,100 @@ 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 + +class IssueRecentVisitSerializer(serializers.ModelSerializer): + class Meta: + model = Issue + fields = ["name", "state", "priority", "assignees"] + +class ProjectMemberSerializer(BaseSerializer): + member = UserLiteSerializer(read_only=True) + + class Meta: + model = ProjectMember + fields = ["member"] + +class ProjectRecentVisitSerializer(serializers.ModelSerializer): + project_members = serializers.SerializerMethodField() + + class Meta: + model = Project + fields = ["id", "name", "logo_props", "project_members"] + + def get_project_members(self, obj): + members = ProjectMember.objects.filter(project_id=obj.id) + + serializer = ProjectMemberSerializer(members, many=True) + return serializer.data + +class PageRecentVisitSerializer(serializers.ModelSerializer): + project_id = serializers.SerializerMethodField() + + class Meta: + model = Page + fields = ["id", "name", "logo_props", "project_id", "owned_by"] + + def get_project_id(self, obj): + project = ( + obj.projects.first() + ) + return project.id if project else None + +def get_entity_model_and_serializer(entity_type): + entity_map = { + "issue": (Issue, IssueRecentVisitSerializer), + "page": (Page, PageRecentVisitSerializer), + "project": (Project, ProjectRecentVisitSerializer) + } + return entity_map.get(entity_type, (None, None)) + +class WorkspaceRecentVisitSerializer(BaseSerializer): + entity_data = serializers.SerializerMethodField() + + class Meta: + model = UserRecentVisit + fields = [ + "id", + "entity_name", + "entity_identifier", + "entity_data", + "visited_at" + ] + read_only_fields = ["workspace", "owner", "created_by", "updated_by"] + + def get_entity_data(self, obj): + entity_name = obj.entity_name + entity_identifier = obj.entity_identifier + + entity_model, entity_serializer = get_entity_model_and_serializer(entity_name) + + if entity_model and entity_serializer: + try: + entity = entity_model.objects.get(pk=entity_identifier) + + return entity_serializer(entity).data + except entity_model.DoesNotExist: + return None + return None From 8a58ec6c37a1abe63ab601bcbd11014a3a1c6882 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Tue, 31 Dec 2024 18:03:08 +0530 Subject: [PATCH 09/13] Resolve conflicts --- apiserver/plane/app/serializers/favorite.py | 1 - apiserver/plane/app/serializers/workspace.py | 97 ++++++++++++++++++++ apiserver/plane/app/urls/workspace.py | 8 +- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/app/serializers/favorite.py b/apiserver/plane/app/serializers/favorite.py index 940b8ee8284..18f92f3ea2a 100644 --- a/apiserver/plane/app/serializers/favorite.py +++ b/apiserver/plane/app/serializers/favorite.py @@ -53,7 +53,6 @@ def get_entity_model_and_serializer(entity_type): } return entity_map.get(entity_type, (None, None)) - class UserFavoriteSerializer(serializers.ModelSerializer): entity_data = serializers.SerializerMethodField() diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index e8bfda1bbd2..224cd81aa79 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -117,3 +117,100 @@ 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 + +class IssueRecentVisitSerializer(serializers.ModelSerializer): + class Meta: + model = Issue + fields = ["name", "state", "priority", "assignees"] + +class ProjectMemberSerializer(BaseSerializer): + member = UserLiteSerializer(read_only=True) + + class Meta: + model = ProjectMember + fields = ["member"] + +class ProjectRecentVisitSerializer(serializers.ModelSerializer): + project_members = serializers.SerializerMethodField() + + class Meta: + model = Project + fields = ["id", "name", "logo_props", "project_members"] + + def get_project_members(self, obj): + members = ProjectMember.objects.filter(project_id=obj.id) + + serializer = ProjectMemberSerializer(members, many=True) + return serializer.data + +class PageRecentVisitSerializer(serializers.ModelSerializer): + project_id = serializers.SerializerMethodField() + + class Meta: + model = Page + fields = ["id", "name", "logo_props", "project_id", "owned_by"] + + def get_project_id(self, obj): + project = ( + obj.projects.first() + ) + return project.id if project else None + +def get_entity_model_and_serializer(entity_type): + entity_map = { + "issue": (Issue, IssueRecentVisitSerializer), + "page": (Page, PageRecentVisitSerializer), + "project": (Project, ProjectRecentVisitSerializer) + } + return entity_map.get(entity_type, (None, None)) + +class WorkspaceRecentVisitSerializer(BaseSerializer): + entity_data = serializers.SerializerMethodField() + + class Meta: + model = UserRecentVisit + fields = [ + "id", + "entity_name", + "entity_identifier", + "entity_data", + "visited_at" + ] + read_only_fields = ["workspace", "owner", "created_by", "updated_by"] + + def get_entity_data(self, obj): + entity_name = obj.entity_name + entity_identifier = obj.entity_identifier + + entity_model, entity_serializer = get_entity_model_and_serializer(entity_name) + + if entity_model and entity_serializer: + try: + entity = entity_model.objects.get(pk=entity_identifier) + + return entity_serializer(entity).data + except entity_model.DoesNotExist: + return None + return None diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index e1611a5b105..f8cdbbd5b46 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -220,11 +220,15 @@ path( "workspaces//quick-links/", QuickLinkViewSet.as_view({"get": "list", "post": "create"}), - name="workspace-quick-links " + name="workspace-quick-links" ), path( "workspaces//quick-links//", - QuickLinkViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}), + QuickLinkViewSet.as_view({ + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy" + }), name="workspace-quick-links" ), path( From 6fbb36c2654f4eae2ea4dfb7b49365328c34cc49 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Thu, 2 Jan 2025 14:47:52 +0530 Subject: [PATCH 10/13] Add filter to only list required entities --- apiserver/plane/app/serializers/workspace.py | 11 ++++++----- apiserver/plane/app/views/workspace/recent_visit.py | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 224cd81aa79..bdfb403da3d 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -7,6 +7,7 @@ from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer + from plane.db.models import ( Workspace, WorkspaceMember, @@ -18,7 +19,7 @@ Issue, Page, Project, - ProjectMember, + ProjectMember ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS @@ -153,18 +154,18 @@ class Meta: fields = ["member"] class ProjectRecentVisitSerializer(serializers.ModelSerializer): - project_members = serializers.SerializerMethodField() - + project_members = serializers.SerializerMethodField() + class Meta: model = Project fields = ["id", "name", "logo_props", "project_members"] def get_project_members(self, obj): - members = ProjectMember.objects.filter(project_id=obj.id) + members = ProjectMember.objects.filter(project_id=obj.id) serializer = ProjectMemberSerializer(members, many=True) return serializer.data - + class PageRecentVisitSerializer(serializers.ModelSerializer): project_id = serializers.SerializerMethodField() diff --git a/apiserver/plane/app/views/workspace/recent_visit.py b/apiserver/plane/app/views/workspace/recent_visit.py index 4491a19a8c9..e74fbe2b657 100644 --- a/apiserver/plane/app/views/workspace/recent_visit.py +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -17,7 +17,8 @@ def get_serializer_class(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) + user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug).filter(entity_name__in=["issue","page","project"]) + serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) return Response(serializer.data, status=status.HTTP_200_OK) From 3f7033816f5472d5e5d311fcb02354fa15662b44 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Thu, 2 Jan 2025 20:03:57 +0530 Subject: [PATCH 11/13] Return required fields --- apiserver/plane/app/serializers/workspace.py | 23 +++++++++++++++---- .../plane/app/views/workspace/recent_visit.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index bdfb403da3d..5fee459e428 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -142,9 +142,17 @@ def validate_url(self, value): return value class IssueRecentVisitSerializer(serializers.ModelSerializer): + project_identifier = serializers.SerializerMethodField() + class Meta: model = Issue - fields = ["name", "state", "priority", "assignees"] + fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"] + + def get_project_identifier(self, obj): + project = obj.project + + project = Project.objects.get(id=project.id) + return project.identifier if project else None class ProjectMemberSerializer(BaseSerializer): member = UserLiteSerializer(read_only=True) @@ -158,7 +166,7 @@ class ProjectRecentVisitSerializer(serializers.ModelSerializer): class Meta: model = Project - fields = ["id", "name", "logo_props", "project_members"] + fields = ["id", "name", "logo_props", "project_members", "identifier"] def get_project_members(self, obj): members = ProjectMember.objects.filter(project_id=obj.id) @@ -168,16 +176,23 @@ def get_project_members(self, obj): class PageRecentVisitSerializer(serializers.ModelSerializer): project_id = serializers.SerializerMethodField() + project_identifier = serializers.SerializerMethodField() class Meta: model = Page - fields = ["id", "name", "logo_props", "project_id", "owned_by"] + fields = ["id", "name", "logo_props", "project_id", "owned_by", "project_identifier"] def get_project_id(self, obj): project = ( obj.projects.first() - ) + ) return project.id if project else None + + def get_project_identifier(self, obj): + project = obj.projects.first() + + project = Project.objects.get(id=project.id) + return project.identifier if project else None def get_entity_model_and_serializer(entity_type): entity_map = { diff --git a/apiserver/plane/app/views/workspace/recent_visit.py b/apiserver/plane/app/views/workspace/recent_visit.py index e74fbe2b657..32fbc1b8193 100644 --- a/apiserver/plane/app/views/workspace/recent_visit.py +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -14,10 +14,10 @@ class UserRecentVisitViewSet(BaseViewSet): def get_serializer_class(self): return WorkspaceRecentVisitSerializer - + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug).filter(entity_name__in=["issue","page","project"]) + user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) From bd2b140ac247d620f6720875a603ce57af5d333d Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Thu, 2 Jan 2025 20:20:00 +0530 Subject: [PATCH 12/13] Add filter --- .../plane/app/views/workspace/recent_visit.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/workspace/recent_visit.py b/apiserver/plane/app/views/workspace/recent_visit.py index 32fbc1b8193..04742666e3e 100644 --- a/apiserver/plane/app/views/workspace/recent_visit.py +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -17,8 +17,15 @@ def get_serializer_class(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) - - serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) - - return Response(serializer.data, status=status.HTTP_200_OK) + user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) + + entity_name = request.query_params.get("entity_name") + + if entity_name: + user_recent_visits = user_recent_visits.filter(entity_name=entity_name)[:10] + + user_recent_visits = user_recent_visits[:10] + + serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + From 80e21ea9ddabc49f82ba2221edb83a26600ea18f Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Thu, 2 Jan 2025 20:20:00 +0530 Subject: [PATCH 13/13] Add filter --- apiserver/plane/app/serializers/workspace.py | 9 ++------- .../plane/app/views/workspace/recent_visit.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apiserver/plane/app/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py index 5fee459e428..ad45cf1a82a 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -151,7 +151,6 @@ class Meta: def get_project_identifier(self, obj): project = obj.project - project = Project.objects.get(id=project.id) return project.identifier if project else None class ProjectMemberSerializer(BaseSerializer): @@ -169,7 +168,7 @@ class Meta: fields = ["id", "name", "logo_props", "project_members", "identifier"] def get_project_members(self, obj): - members = ProjectMember.objects.filter(project_id=obj.id) + members = ProjectMember.objects.filter(project_id=obj.id).select_related('member') serializer = ProjectMemberSerializer(members, many=True) return serializer.data @@ -183,15 +182,11 @@ class Meta: fields = ["id", "name", "logo_props", "project_id", "owned_by", "project_identifier"] def get_project_id(self, obj): - project = ( - obj.projects.first() - ) - return project.id if project else None + return obj.project_id if hasattr(obj, 'project_id') else obj.projects.values_list('id', flat=True).first() def get_project_identifier(self, obj): project = obj.projects.first() - project = Project.objects.get(id=project.id) return project.identifier if project else None def get_entity_model_and_serializer(entity_type): diff --git a/apiserver/plane/app/views/workspace/recent_visit.py b/apiserver/plane/app/views/workspace/recent_visit.py index 32fbc1b8193..04742666e3e 100644 --- a/apiserver/plane/app/views/workspace/recent_visit.py +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -17,8 +17,15 @@ def get_serializer_class(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def list(self, request, slug): - user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) - - serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) - - return Response(serializer.data, status=status.HTTP_200_OK) + user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug) + + entity_name = request.query_params.get("entity_name") + + if entity_name: + user_recent_visits = user_recent_visits.filter(entity_name=entity_name)[:10] + + user_recent_visits = user_recent_visits[:10] + + serializer = WorkspaceRecentVisitSerializer(user_recent_visits, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) +