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/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 83303c70a99..ad45cf1a82a 100644 --- a/apiserver/plane/app/serializers/workspace.py +++ b/apiserver/plane/app/serializers/workspace.py @@ -1,17 +1,25 @@ # 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 from .user import UserLiteSerializer, UserAdminLiteSerializer + from plane.db.models import ( Workspace, WorkspaceMember, WorkspaceMemberInvite, WorkspaceTheme, WorkspaceUserProperties, - WorkspaceUserLink + WorkspaceUserLink, + UserRecentVisit, + Issue, + Page, + Project, + ProjectMember ) from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS @@ -132,3 +140,88 @@ def validate_url(self, value): raise serializers.ValidationError({"error": "Invalid URL format."}) return value + +class IssueRecentVisitSerializer(serializers.ModelSerializer): + project_identifier = serializers.SerializerMethodField() + + class Meta: + model = Issue + fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"] + + def get_project_identifier(self, obj): + project = obj.project + + return project.identifier if project else None + +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", "identifier"] + + def get_project_members(self, obj): + members = ProjectMember.objects.filter(project_id=obj.id).select_related('member') + + serializer = ProjectMemberSerializer(members, many=True) + return serializer.data + +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", "project_identifier"] + + def get_project_id(self, obj): + 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() + + return project.identifier 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..f8cdbbd5b46 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 ) @@ -219,11 +220,20 @@ 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( + "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 41986db6cb9..19dbd4d8f08 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..175172a8015 --- /dev/null +++ b/apiserver/plane/app/views/workspace/recent_visit.py @@ -0,0 +1,31 @@ +# 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) + + entity_name = request.query_params.get("entity_name") + + if entity_name: + user_recent_visits = user_recent_visits.filter(entity_name=entity_name) + + user_recent_visits = user_recent_visits.filter(entity_name__in=["issue","page","project"]) + + serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True) + return Response(serializer.data, status=status.HTTP_200_OK) +