Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: issue filter views #418

Merged
merged 11 commits into from
Mar 15, 2023
2 changes: 1 addition & 1 deletion apiserver/plane/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
)
from .state import StateSerializer
from .shortcut import ShortCutSerializer
from .view import ViewSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer
from .asset import FileAssetSerializer
from .issue import (
Expand Down
47 changes: 44 additions & 3 deletions apiserver/plane/api/serializers/view.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
# Third party imports
from rest_framework import serializers

# Module imports
from .base import BaseSerializer

from plane.db.models import View
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters


class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)

class Meta:
model = IssueView
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"query",
]

def create(self, validated_data):
query_params = validated_data.get("query_data", {})

if not bool(query_params):
raise serializers.ValidationError(
{"query_data": ["Query data field cannot be empty"]}
)

validated_data["query"] = issue_filters(query_params, "POST")
return IssueView.objects.create(**validated_data)

def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if not bool(query_params):
raise serializers.ValidationError(
{"query_data": ["Query data field cannot be empty"]}
)

validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)


class IssueViewFavoriteSerializer(BaseSerializer):
view_detail = IssueViewSerializer(source="issue_view", read_only=True)

class ViewSerializer(BaseSerializer):
class Meta:
model = View
model = IssueViewFavorite
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"user",
]
32 changes: 29 additions & 3 deletions apiserver/plane/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
ShortCutViewSet,
## End Shortcuts
# Views
ViewViewSet,
IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet,
## End Views
# Cycles
CycleViewSet,
Expand Down Expand Up @@ -452,7 +454,7 @@
# Views
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
Expand All @@ -462,7 +464,7 @@
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
Expand All @@ -472,6 +474,30 @@
),
name="project-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:view_id>/issues/",
ViewIssuesEndpoint.as_view(),
name="project-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
),
## End Views
## Cycles
path(
Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
)
from .state import StateViewSet
from .shortcut import ShortCutViewSet
from .view import ViewViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
Expand Down
13 changes: 4 additions & 9 deletions apiserver/plane/api/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.models import Prefetch, OuterRef, Func, F, Q
from django.core.serializers.json import DjangoJSONEncoder


# Third Party imports
from rest_framework.response import Response
from rest_framework import status
Expand Down Expand Up @@ -45,6 +46,7 @@
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters


class IssueViewSet(BaseViewSet):
Expand Down Expand Up @@ -171,18 +173,11 @@ def get_queryset(self):

def list(self, request, slug, project_id):
try:
# Issue State groups
type = request.GET.get("type", "all")
group = ["backlog", "unstarted", "started", "completed", "cancelled"]
if type == "backlog":
group = ["backlog"]
if type == "active":
group = ["unstarted", "started"]

filters = issue_filters(request.query_params, "GET")
issue_queryset = (
self.get_queryset()
.order_by(request.GET.get("order_by", "created_at"))
.filter(state__group__in=group)
.filter(**filters)
)

issues = IssueSerializer(issue_queryset, many=True).data
Expand Down
177 changes: 170 additions & 7 deletions apiserver/plane/api/views/view.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
# Django imports
from django.db import IntegrityError
from django.db.models import Prefetch, OuterRef, Exists

# Third party imports
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception

# Module imports
from . import BaseViewSet
from plane.api.serializers import ViewSerializer
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
IssueViewSerializer,
IssueSerializer,
IssueViewFavoriteSerializer,
)
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import View

from plane.db.models import (
IssueView,
Issue,
IssueBlocker,
IssueLink,
CycleIssue,
ModuleIssue,
IssueViewFavorite,
)

class ViewViewSet(BaseViewSet):

serializer_class = ViewSerializer
model = View
class IssueViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
ProjectEntityPermission,
]
Expand All @@ -17,6 +37,12 @@ def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))

def get_queryset(self):
subquery = IssueViewFavorite.objects.filter(
user=self.request.user,
view_id=OuterRef("pk"),
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
Expand All @@ -25,5 +51,142 @@ def get_queryset(self):
.filter(project__project_projectmember__member=self.request.user)
.select_related("project")
.select_related("workspace")
.annotate(is_favorite=Exists(subquery))
.distinct()
)


class ViewIssuesEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]

def get(self, request, slug, project_id, view_id):
try:
view = IssueView.objects.get(pk=view_id)
queries = view.query

issues = (
Issue.objects.filter(
**queries, project_id=project_id, workspace__slug=slug
)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocked_issues",
queryset=IssueBlocker.objects.select_related(
"blocked_by", "block"
),
)
)
.prefetch_related(
Prefetch(
"blocker_issues",
queryset=IssueBlocker.objects.select_related(
"block", "blocked_by"
),
)
)
.prefetch_related(
Prefetch(
"issue_cycle",
queryset=CycleIssue.objects.select_related("cycle", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related(
"module", "issue"
).prefetch_related("module__members"),
),
)
.prefetch_related(
Prefetch(
"issue_link",
queryset=IssueLink.objects.select_related(
"issue"
).select_related("created_by"),
)
)
)

serializer = IssueSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except IssueView.DoesNotExist:
return Response(
{"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)


class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer
model = IssueViewFavorite

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("view")
)

def create(self, request, slug, project_id):
try:
serializer = IssueViewFavoriteSerializer(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 view 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, view_id):
try:
view_favourite = IssueViewFavorite.objects.get(
project=project_id,
user=request.user,
workspace__slug=slug,
view_id=view_id,
)
view_favourite.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except IssueViewFavorite.DoesNotExist:
return Response(
{"error": "View 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,
)
2 changes: 1 addition & 1 deletion apiserver/plane/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

from .shortcut import Shortcut

from .view import View
from .view import IssueView, IssueViewFavorite

from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite

Expand Down
Loading