Skip to content

Commit

Permalink
feat: issue filter views (#418)
Browse files Browse the repository at this point in the history
* dev: views initiated

* dev: refactor filtering logic

* dev: move state grouping filter to util function

* dev: view issues create endpoint and update on filters for time

* dev: rename views to issue views

* dev: rename in serilaizer and views

* dev: update issue filters

* dev: update filter

* feat: create issue favorites

* dev: update query keys

* dev: update create and update method
  • Loading branch information
pablohashescobar committed Mar 15, 2023
1 parent 46f6b61 commit b6ee197
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 30 deletions.
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 @@ -79,7 +79,9 @@
ShortCutViewSet,
## End Shortcuts
# Views
ViewViewSet,
IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet,
## End Views
# Cycles
CycleViewSet,
Expand Down Expand Up @@ -474,7 +476,7 @@
# Views
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
Expand All @@ -484,7 +486,7 @@
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/<uuid:pk>/",
ViewViewSet.as_view(
IssueViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
Expand All @@ -494,6 +496,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 @@ -41,7 +41,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 @@ -7,6 +7,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 @@ -46,6 +47,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 @@ -172,18 +174,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 @@ -44,7 +44,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

0 comments on commit b6ee197

Please sign in to comment.