diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 97d722217..9814ace37 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -45,6 +45,7 @@ from .module import ( ModuleSerializer, ModuleIssueSerializer, ModuleLinkSerializer, + ModuleFavoriteSerializer, ) from .api_token import APITokenSerializer diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index ab52c2ec8..bb317a330 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -7,7 +7,7 @@ from .user import UserLiteSerializer from .project import ProjectSerializer from .issue import IssueStateSerializer -from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink +from plane.db.models import User, Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite class ModuleWriteSerializer(BaseSerializer): @@ -135,6 +135,7 @@ class ModuleSerializer(BaseSerializer): members_detail = UserLiteSerializer(read_only=True, many=True, source="members") issue_module = ModuleIssueSerializer(read_only=True, many=True) link_module = ModuleLinkSerializer(read_only=True, many=True) + is_favorite = serializers.BooleanField(read_only=True) class Meta: model = Module @@ -147,3 +148,15 @@ class ModuleSerializer(BaseSerializer): "created_at", "updated_at", ] + +class ModuleFavoriteSerializer(BaseSerializer): + module_detail = ModuleFlatSerializer(source="module", read_only=True) + + class Meta: + model = ModuleFavorite + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "user", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b5e991088..e75c29c12 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -90,6 +90,7 @@ from plane.api.views import ( # Modules ModuleViewSet, ModuleIssueViewSet, + ModuleFavoriteViewSet, ## End Modules # Api Tokens ApiTokenEndpoint, @@ -802,6 +803,25 @@ urlpatterns = [ ), name="project-issue-module-links", ), + path( + "workspaces//projects//user-favorite-modules/", + ModuleFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-module", + ), + path( + "workspaces//projects//user-favorite-modules//", + ModuleFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-module", + ), ## End Modules # API Tokens path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 480c3b21b..2556fc7d9 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,7 +80,12 @@ from .authentication import ( MagicSignInGenerateEndpoint, ) -from .module import ModuleViewSet, ModuleIssueViewSet, ModuleLinkViewSet +from .module import ( + ModuleViewSet, + ModuleIssueViewSet, + ModuleLinkViewSet, + ModuleFavoriteViewSet, +) from .api_token import ApiTokenEndpoint diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 2e5239768..ce74cfdff 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -3,7 +3,7 @@ import json # Django Imports from django.db import IntegrityError -from django.db.models import Prefetch, F, OuterRef, Func +from django.db.models import Prefetch, F, OuterRef, Func, Exists from django.core import serializers # Third party imports @@ -18,6 +18,7 @@ from plane.api.serializers import ( ModuleSerializer, ModuleIssueSerializer, ModuleLinkSerializer, + ModuleFavoriteSerializer, ) from plane.api.permissions import ProjectEntityPermission from plane.db.models import ( @@ -26,6 +27,7 @@ from plane.db.models import ( Project, Issue, ModuleLink, + ModuleFavorite, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -99,6 +101,23 @@ class ModuleViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + def list(self, request, slug, project_id): + try: + subquery = ModuleFavorite.objects.filter( + user=self.request.user, + module_id=OuterRef("pk"), + project_id=project_id, + workspace__slug=slug, + ) + modules = self.get_queryset().annotate(is_favorite=Exists(subquery)) + return Response(ModuleSerializer(modules, 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, + ) + class ModuleIssueViewSet(BaseViewSet): serializer_class = ModuleIssueSerializer @@ -285,3 +304,65 @@ class ModuleLinkViewSet(BaseViewSet): .filter(project__project_projectmember__member=self.request.user) .distinct() ) + + +class ModuleFavoriteViewSet(BaseViewSet): + serializer_class = ModuleFavoriteSerializer + model = ModuleFavorite + + 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("module") + ) + + def create(self, request, slug, project_id): + try: + serializer = ModuleFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user, project_id=project_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "The module 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_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, + ) + + def destroy(self, request, slug, project_id, module_id): + try: + module_favorite = ModuleFavorite.objects.get( + project=project_id, + user=request.user, + workspace__slug=slug, + module_id=module_id, + ) + module_favorite.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except ModuleFavorite.DoesNotExist: + return Response( + {"error": "Module 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 fe1f70c9d..09b44b422 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -45,7 +45,7 @@ from .shortcut import Shortcut from .view import View -from .module import Module, ModuleMember, ModuleIssue, ModuleLink +from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite from .api_token import APIToken diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index 3371c961b..ec8c401ab 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -100,3 +100,29 @@ class ModuleLink(ProjectBaseModel): def __str__(self): return f"{self.module.name} {self.url}" + + +class ModuleFavorite(ProjectBaseModel): + """_summary_ + ModuleFavorite (model): To store all the module favorite of the user + """ + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="module_favorites", + ) + module = models.ForeignKey( + "db.Module", on_delete=models.CASCADE, related_name="module_favorites" + ) + + class Meta: + unique_together = ["module", "user"] + verbose_name = "Module Favorite" + verbose_name_plural = "Module Favorites" + db_table = "module_favorites" + ordering = ("-created_at",) + + def __str__(self): + """Return user and the module""" + return f"{self.user.email} <{self.module.name}>"