feat: cycle favourites for user (#369)

* feat: cycle favourites for user

* chore: update nomenclature

* chore: update on nomenclature

* feat: add favorites for completed and current cycle endpoints
This commit is contained in:
pablohashescobar 2023-03-06 18:59:47 +05:30 committed by GitHub
parent 79d7b6fec3
commit cb8b6b43dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 13 deletions

View File

@ -22,7 +22,7 @@ from .project import (
from .state import StateSerializer from .state import StateSerializer
from .shortcut import ShortCutSerializer from .shortcut import ShortCutSerializer
from .view import ViewSerializer from .view import ViewSerializer
from .cycle import CycleSerializer, CycleIssueSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer from .asset import FileAssetSerializer
from .issue import ( from .issue import (
IssueCreateSerializer, IssueCreateSerializer,

View File

@ -5,12 +5,12 @@ from rest_framework import serializers
from .base import BaseSerializer from .base import BaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .issue import IssueStateSerializer from .issue import IssueStateSerializer
from plane.db.models import Cycle, CycleIssue from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleSerializer(BaseSerializer): class CycleSerializer(BaseSerializer):
owned_by = UserLiteSerializer(read_only=True) owned_by = UserLiteSerializer(read_only=True)
is_favorite = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Cycle model = Cycle
@ -23,7 +23,6 @@ class CycleSerializer(BaseSerializer):
class CycleIssueSerializer(BaseSerializer): class CycleIssueSerializer(BaseSerializer):
issue_detail = IssueStateSerializer(read_only=True, source="issue") issue_detail = IssueStateSerializer(read_only=True, source="issue")
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
@ -35,3 +34,16 @@ class CycleIssueSerializer(BaseSerializer):
"project", "project",
"cycle", "cycle",
] ]
class CycleFavoriteSerializer(BaseSerializer):
cycle_detail = CycleSerializer(source="cycle", read_only=True)
class Meta:
model = CycleFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]

View File

@ -84,6 +84,7 @@ from plane.api.views import (
CycleDateCheckEndpoint, CycleDateCheckEndpoint,
CurrentUpcomingCyclesEndpoint, CurrentUpcomingCyclesEndpoint,
CompletedCyclesEndpoint, CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint, DraftCyclesEndpoint,
## End Cycles ## End Cycles
# Modules # Modules
@ -536,6 +537,25 @@ urlpatterns = [
DraftCyclesEndpoint.as_view(), DraftCyclesEndpoint.as_view(),
name="project-cycle-draft", name="project-cycle-draft",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-cycle",
),
## End Cycles ## End Cycles
# Issue # Issue
path( path(

View File

@ -46,6 +46,7 @@ from .cycle import (
CycleDateCheckEndpoint, CycleDateCheckEndpoint,
CurrentUpcomingCyclesEndpoint, CurrentUpcomingCyclesEndpoint,
CompletedCyclesEndpoint, CompletedCyclesEndpoint,
CycleFavoriteViewSet,
DraftCyclesEndpoint, DraftCyclesEndpoint,
) )
from .asset import FileAssetEndpoint from .asset import FileAssetEndpoint

View File

@ -2,7 +2,8 @@
import json import json
# Django imports # Django imports
from django.db.models import OuterRef, Func, F, Q from django.db import IntegrityError
from django.db.models import OuterRef, Func, F, Q, Exists, OuterRef
from django.core import serializers from django.core import serializers
from django.utils import timezone from django.utils import timezone
@ -13,9 +14,13 @@ from sentry_sdk import capture_exception
# Module imports # Module imports
from . import BaseViewSet, BaseAPIView from . import BaseViewSet, BaseAPIView
from plane.api.serializers import CycleSerializer, CycleIssueSerializer from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Cycle, CycleIssue, Issue from plane.db.models import Cycle, CycleIssue, Issue, CycleFavorite
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -45,6 +50,23 @@ class CycleViewSet(BaseViewSet):
.distinct() .distinct()
) )
def list(self, request, slug, project_id):
try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
cycles = self.get_queryset().annotate(is_favorite=Exists(subquery))
return Response(CycleSerializer(cycles, 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, project_id): def create(self, request, slug, project_id):
try: try:
if ( if (
@ -274,18 +296,24 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CurrentUpcomingCyclesEndpoint(BaseAPIView): class CurrentUpcomingCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try: try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
current_cycle = Cycle.objects.filter( current_cycle = Cycle.objects.filter(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
start_date__lte=timezone.now(), start_date__lte=timezone.now(),
end_date__gte=timezone.now(), end_date__gte=timezone.now(),
) ).annotate(is_favorite=Exists(subquery))
upcoming_cycle = Cycle.objects.filter( upcoming_cycle = Cycle.objects.filter(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
start_date__gt=timezone.now(), start_date__gt=timezone.now(),
) ).annotate(is_favorite=Exists(subquery))
return Response( return Response(
{ {
@ -306,11 +334,17 @@ class CurrentUpcomingCyclesEndpoint(BaseAPIView):
class CompletedCyclesEndpoint(BaseAPIView): class CompletedCyclesEndpoint(BaseAPIView):
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
try: try:
subquery = CycleFavorite.objects.filter(
user=self.request.user,
cycle_id=OuterRef("pk"),
project_id=project_id,
workspace__slug=slug,
)
completed_cycles = Cycle.objects.filter( completed_cycles = Cycle.objects.filter(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
end_date__lt=timezone.now(), end_date__lt=timezone.now(),
) ).annotate(is_favorite=Exists(subquery))
return Response( return Response(
{ {
@ -349,3 +383,65 @@ class DraftCyclesEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
class CycleFavoriteViewSet(BaseViewSet):
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
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("cycle", "cycle__owned_by")
)
def create(self, request, slug, project_id):
try:
serializer = CycleFavoriteSerializer(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 cycle 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, cycle_id):
try:
cycle_favorite = CycleFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
cycle_id=cycle_id,
)
cycle_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except CycleFavorite.DoesNotExist:
return Response(
{"error": "Cycle 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,
)

View File

@ -39,7 +39,7 @@ from .social_connection import SocialLoginConnection
from .state import State from .state import State
from .cycle import Cycle, CycleIssue from .cycle import Cycle, CycleIssue, CycleFavorite
from .shortcut import Shortcut from .shortcut import Shortcut

View File

@ -7,7 +7,6 @@ from . import ProjectBaseModel
class Cycle(ProjectBaseModel): class Cycle(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Cycle Name") name = models.CharField(max_length=255, verbose_name="Cycle Name")
description = models.TextField(verbose_name="Cycle Description", blank=True) description = models.TextField(verbose_name="Cycle Description", blank=True)
start_date = models.DateField(verbose_name="Start Date", blank=True, null=True) start_date = models.DateField(verbose_name="Start Date", blank=True, null=True)
@ -18,7 +17,6 @@ class Cycle(ProjectBaseModel):
related_name="owned_by_cycle", related_name="owned_by_cycle",
) )
class Meta: class Meta:
verbose_name = "Cycle" verbose_name = "Cycle"
verbose_name_plural = "Cycles" verbose_name_plural = "Cycles"
@ -50,3 +48,29 @@ class CycleIssue(ProjectBaseModel):
def __str__(self): def __str__(self):
return f"{self.cycle}" return f"{self.cycle}"
class CycleFavorite(ProjectBaseModel):
"""_summary_
CycleFavorite (model): To store all the cycle favorite of the user
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="cycle_favorites",
)
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_favorites"
)
class Meta:
unique_together = ["cycle", "user"]
verbose_name = "Cycle Favorite"
verbose_name_plural = "Cycle Favorites"
db_table = "cycle_favorites"
ordering = ("-created_at",)
def __str__(self):
"""Return user and the cycle"""
return f"{self.user.email} <{self.cycle.name}>"