diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 3040930b4..e01eb113b 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -17,6 +17,7 @@ from .project import ( ProjectMemberSerializer, ProjectMemberInviteSerializer, ProjectIdentifierSerializer, + ProjectFavoriteSerializer, ) from .state import StateSerializer from .shortcut import ShortCutSerializer diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index cdc9adf36..61d09b4a8 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -13,6 +13,7 @@ from plane.db.models import ( ProjectMember, ProjectMemberInvite, ProjectIdentifier, + ProjectFavorite, ) @@ -44,7 +45,6 @@ class ProjectSerializer(BaseSerializer): return project def update(self, instance, validated_data): - identifier = validated_data.get("identifier", "").strip().upper() # If identifier is not passed update the project and return @@ -73,10 +73,10 @@ class ProjectSerializer(BaseSerializer): class ProjectDetailSerializer(BaseSerializer): - workspace = WorkSpaceSerializer(read_only=True) default_assignee = UserLiteSerializer(read_only=True) project_lead = UserLiteSerializer(read_only=True) + is_favorite = serializers.BooleanField(read_only=True) class Meta: model = Project @@ -84,7 +84,6 @@ class ProjectDetailSerializer(BaseSerializer): class ProjectMemberSerializer(BaseSerializer): - workspace = WorkSpaceSerializer(read_only=True) project = ProjectSerializer(read_only=True) member = UserLiteSerializer(read_only=True) @@ -95,7 +94,6 @@ class ProjectMemberSerializer(BaseSerializer): class ProjectMemberInviteSerializer(BaseSerializer): - project = ProjectSerializer(read_only=True) workspace = WorkSpaceSerializer(read_only=True) @@ -108,3 +106,15 @@ class ProjectIdentifierSerializer(BaseSerializer): class Meta: model = ProjectIdentifier fields = "__all__" + + +class ProjectFavoriteSerializer(BaseSerializer): + project_detail = ProjectSerializer(source="project", read_only=True) + + class Meta: + model = ProjectFavorite + fields = "__all__" + read_only_fields = [ + "workspace", + "user", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 77e628a9a..5e5db9e1a 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -52,6 +52,7 @@ from plane.api.views import ( ProjectJoinEndpoint, UserProjectInvitationsViewset, ProjectIdentifierEndpoint, + ProjectFavoritesViewSet, ## End Projects # Issues IssueViewSet, @@ -378,6 +379,25 @@ urlpatterns = [ ProjectMemberUserEndpoint.as_view(), name="project-view", ), + path( + "workspaces//user-favorite-projects/", + ProjectFavoritesViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project", + ), + path( + "workspaces//user-favorite-projects//", + ProjectFavoritesViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project", + ), # End Projects # States path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 4ac45d287..6186c3795 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -11,6 +11,7 @@ from .project import ( ProjectJoinEndpoint, ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, + ProjectFavoritesViewSet, ) from .people import ( UserEndpoint, diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index e24477ecd..4cee2ad1b 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -5,7 +5,7 @@ from datetime import datetime # Django imports from django.core.exceptions import ValidationError from django.db import IntegrityError -from django.db.models import Q +from django.db.models import Q, Exists, OuterRef from django.core.validators import validate_email from django.conf import settings @@ -22,6 +22,7 @@ from plane.api.serializers import ( ProjectMemberSerializer, ProjectDetailSerializer, ProjectMemberInviteSerializer, + ProjectFavoriteSerializer, ) from plane.api.permissions import ProjectBasePermission @@ -35,6 +36,7 @@ from plane.db.models import ( WorkspaceMember, State, TeamMember, + ProjectFavorite, ) from plane.db.models import ( @@ -73,6 +75,22 @@ class ProjectViewSet(BaseViewSet): .distinct() ) + def list(self, request, slug): + try: + subquery = ProjectFavorite.objects.filter( + user=self.request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ) + projects = self.get_queryset().annotate(is_favorite=Exists(subquery)) + return Response(ProjectDetailSerializer(projects, many=True).data) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -659,3 +677,69 @@ class ProjectMemberUserEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class ProjectFavoritesViewSet(BaseViewSet): + serializer_class = ProjectFavoriteSerializer + model = ProjectFavorite + + def get_queryset(self): + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(user=self.request.user) + .select_related( + "project", "project__project_lead", "project__default_assignee" + ) + .select_related("workspace", "workspace__owner") + ) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def create(self, request, slug): + try: + serializer = ProjectFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + print(str(e)) + if "already exists" in str(e): + return Response( + {"error": "The project is already added to favorites"}, + status=status.HTTP_410_GONE, + ) + else: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_410_GONE, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def destroy(self, request, slug, project_id): + try: + project_favorite = ProjectFavorite.objects.get( + project=project_id, user=request.user, workspace__slug=slug + ) + project_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ProjectFavorite.DoesNotExist: + return Response( + {"error": "Project is not in favorites"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index ce8cf950b..c113ed7b6 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -16,6 +16,7 @@ from .project import ( ProjectBaseModel, ProjectMemberInvite, ProjectIdentifier, + ProjectFavorite, ) from .issue import ( diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 254724c98..e7f265c76 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -155,3 +155,22 @@ class ProjectIdentifier(AuditModel): verbose_name_plural = "Project Identifiers" db_table = "project_identifiers" ordering = ("-created_at",) + + +class ProjectFavorite(ProjectBaseModel): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="project_favorites", + ) + + class Meta: + unique_together = ["project", "user"] + verbose_name = "Project Favorite" + verbose_name_plural = "Project Favorites" + db_table = "project_favorites" + ordering = ("-created_at",) + + def __str__(self): + """Return user of the project""" + return f"{self.user.email} <{self.project.name}>"