From f0edbb7ec9752dd995052b3b1495fdb4ad2210b9 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Mar 2023 12:09:57 +0530 Subject: [PATCH 01/11] dev: views initiated --- apiserver/plane/api/views/issue.py | 4 ++ apiserver/plane/db/models/view.py | 3 + apiserver/plane/utils/issue_filters.py | 80 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 apiserver/plane/utils/issue_filters.py diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ca40606ec9e..9cb1e96b5bc 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -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 @@ -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): @@ -179,10 +181,12 @@ def list(self, request, slug, project_id): if type == "active": group = ["unstarted", "started"] + filters = issue_filters(request.query_params) 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 diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index c3ea9a86617..26b45f9318a 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -10,6 +10,9 @@ class View(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="View Name") description = models.TextField(verbose_name="View Description", blank=True) query = models.JSONField(verbose_name="View Query") + access = models.PositiveSmallIntegerField( + default=1, choices=((0, "Private"), (1, "Public")) + ) class Meta: verbose_name = "View" diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py new file mode 100644 index 00000000000..82d32517bdb --- /dev/null +++ b/apiserver/plane/utils/issue_filters.py @@ -0,0 +1,80 @@ +from django.utils.timezone import make_aware +from django.utils.dateparse import parse_datetime + + +def issue_filters(query_params): + filter = dict() + + if len(query_params.getlist("state")): + filter["state__in"] = query_params.getlist("state") + if len(query_params.getlist("priority")): + filter["priority__in"] = query_params.getlist("priority") + if len(query_params.getlist("parent")): + filter["parent__in"] = query_params.getlist("parent") + if len(query_params.getlist("labels")): + filter["labels__in"] = query_params.getlist("labels") + if len(query_params.getlist("assignees")): + filter["assignees__in"] = query_params.getlist("assignees") + if len(query_params.getlist("created_by")): + filter["created_by__in"] = query_params.getlist("created_by") + if query_params.get("content", False): + filter["description_html__in"] = query_params.get("description_html") + if len(query_params.getlist("created_at")): + for query in query_params.getlist("created_at"): + created_at_query = query.split(";") + if len(created_at_query) == 2 and "after" in created_at_query: + filter["created_at__gte"] = make_aware( + parse_datetime(created_at_query[0]) + ) + else: + filter["created_at__lte"] = make_aware( + parse_datetime(created_at_query[0]) + ) + if len(query_params.getlist("updated_at")): + for query in query_params.getlist("updated_at"): + updated_at_query = query.split(";") + if len(updated_at_query) == 2 and "after" in updated_at_query: + filter["updated_at__gte"] = make_aware( + parse_datetime(updated_at_query[0]) + ) + else: + filter["updated_at__lte"] = make_aware( + parse_datetime(updated_at_query[0]) + ) + if len(query_params.getlist("start_date")): + for query in query_params.getlist("start_date"): + start_date_query = query.split(";") + if len(start_date_query) == 2 and "after" in start_date_query: + filter["start_date__gte"] = make_aware( + parse_datetime(start_date_query[0]) + ) + else: + filter["start_date__gte"] = make_aware( + parse_datetime(start_date_query[0]) + ) + + if len(query_params.getlist("target_date")): + for query in query_params.getlist("target_date"): + target_date_query = query.split(";") + if len(target_date_query) == 2 and "after" in target_date_query: + filter["target_date__gte"] = make_aware( + parse_datetime(target_date_query[0]) + ) + else: + filter["target_date__gte"] = make_aware( + parse_datetime(target_date_query[0]) + ) + + if len(query_params.getlist("completed_at")): + for query in query_params.getlist("completed_at"): + completed_at_query = query.split(";") + if len(completed_at_query) == 2 and "after" in completed_at_query: + filter["completed_at__gte"] = make_aware( + parse_datetime(completed_at_query[0]) + ) + else: + filter["completed_at__gte"] = make_aware( + parse_datetime(completed_at_query[0]) + ) + + return filter From 5a259ecefcac642d8a28d145ea100b23cee113d6 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Mar 2023 13:12:09 +0530 Subject: [PATCH 02/11] dev: refactor filtering logic --- apiserver/plane/utils/issue_filters.py | 182 +++++++++++++++---------- 1 file changed, 111 insertions(+), 71 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 82d32517bdb..9a412ad99ea 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -2,79 +2,119 @@ from django.utils.dateparse import parse_datetime +def filter_state(params, filter): + filter["state__in"] = params.getlist("state") + return filter + + +def filter_priority(params, filter): + filter["priority__in"] = params.getlist("priority") + return filter + + +def filter_parent(params, filter): + filter["parent__in"] = params.getlist("parent") + return filter + + +def filter_labels(params, filter): + filter["labels__in"] = params.getlist("labels") + return filter + + +def filter_assignees(params, filter): + filter["assignees__in"] = params.getlist("assignees") + return filter + + +def filter_created_by(params, filter): + filter["created_by__in"] = params.getlist("created_by") + return filter + + +def filter_content(params, filter): + filter["description_html__in"] = params.get("description_html") + return filter + + +def filter_created_at(params, filter): + for query in params.getlist("created_at"): + created_at_query = query.split("|") + if len(created_at_query) == 2 and "after" in created_at_query: + filter["created_at__gte"] = make_aware(parse_datetime(created_at_query[0])) + else: + filter["created_at__lte"] = make_aware(parse_datetime(created_at_query[0])) + + return filter + + +def filter_updated_at(params, filter): + for query in params.getlist("updated_at"): + updated_at_query = query.split("|") + if len(updated_at_query) == 2 and "after" in updated_at_query: + filter["updated_at__gte"] = make_aware(parse_datetime(updated_at_query[0])) + else: + filter["updated_at__lte"] = make_aware(parse_datetime(updated_at_query[0])) + return filter + + +def filter_start_date(params, filter): + for query in params.getlist("start_date"): + start_date_query = query.split("|") + if len(start_date_query) == 2 and "after" in start_date_query: + filter["start_date__gte"] = make_aware(parse_datetime(start_date_query[0])) + else: + filter["start_date__lte"] = make_aware(parse_datetime(start_date_query[0])) + return filter + + +def filter_target_date(params, filter): + for query in params.getlist("target_date"): + target_date_query = query.split("|") + if len(target_date_query) == 2 and "after" in target_date_query: + filter["target_date__gte"] = make_aware( + parse_datetime(target_date_query[0]) + ) + else: + filter["target_date__lte"] = make_aware( + parse_datetime(target_date_query[0]) + ) + + +def filter_completed_at(params, filter): + for query in params.getlist("completed_at"): + completed_at_query = query.split("|") + if len(completed_at_query) == 2 and "after" in completed_at_query: + filter["completed_at__gte"] = make_aware( + parse_datetime(completed_at_query[0]) + ) + else: + filter["completed_at__lte"] = make_aware( + parse_datetime(completed_at_query[0]) + ) + + def issue_filters(query_params): filter = dict() - if len(query_params.getlist("state")): - filter["state__in"] = query_params.getlist("state") - if len(query_params.getlist("priority")): - filter["priority__in"] = query_params.getlist("priority") - if len(query_params.getlist("parent")): - filter["parent__in"] = query_params.getlist("parent") - if len(query_params.getlist("labels")): - filter["labels__in"] = query_params.getlist("labels") - if len(query_params.getlist("assignees")): - filter["assignees__in"] = query_params.getlist("assignees") - if len(query_params.getlist("created_by")): - filter["created_by__in"] = query_params.getlist("created_by") - if query_params.get("content", False): - filter["description_html__in"] = query_params.get("description_html") - if len(query_params.getlist("created_at")): - for query in query_params.getlist("created_at"): - created_at_query = query.split(";") - if len(created_at_query) == 2 and "after" in created_at_query: - filter["created_at__gte"] = make_aware( - parse_datetime(created_at_query[0]) - ) - else: - filter["created_at__lte"] = make_aware( - parse_datetime(created_at_query[0]) - ) - if len(query_params.getlist("updated_at")): - for query in query_params.getlist("updated_at"): - updated_at_query = query.split(";") - if len(updated_at_query) == 2 and "after" in updated_at_query: - filter["updated_at__gte"] = make_aware( - parse_datetime(updated_at_query[0]) - ) - else: - filter["updated_at__lte"] = make_aware( - parse_datetime(updated_at_query[0]) - ) - if len(query_params.getlist("start_date")): - for query in query_params.getlist("start_date"): - start_date_query = query.split(";") - if len(start_date_query) == 2 and "after" in start_date_query: - filter["start_date__gte"] = make_aware( - parse_datetime(start_date_query[0]) - ) - else: - filter["start_date__gte"] = make_aware( - parse_datetime(start_date_query[0]) - ) - - if len(query_params.getlist("target_date")): - for query in query_params.getlist("target_date"): - target_date_query = query.split(";") - if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gte"] = make_aware( - parse_datetime(target_date_query[0]) - ) - else: - filter["target_date__gte"] = make_aware( - parse_datetime(target_date_query[0]) - ) - - if len(query_params.getlist("completed_at")): - for query in query_params.getlist("completed_at"): - completed_at_query = query.split(";") - if len(completed_at_query) == 2 and "after" in completed_at_query: - filter["completed_at__gte"] = make_aware( - parse_datetime(completed_at_query[0]) - ) - else: - filter["completed_at__gte"] = make_aware( - parse_datetime(completed_at_query[0]) - ) + ISSUE_FILTER = { + "state": filter_state, + "priority": filter_priority, + "parent": filter_parent, + "labels": filter_labels, + "assignees": filter_assignees, + "created_by": filter_created_by, + "content": filter_content, + "created_at": filter_created_at, + "updated_at": filter_updated_at, + "start_date": filter_start_date, + "target_date": filter_target_date, + "completed_at": filter_completed_at, + } + + for key, value in ISSUE_FILTER.items(): + if key in query_params: + func = value + func(query_params, filter) return filter From b1e1347e1f836527c4a3b4c14644d846f28b1ef0 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Mar 2023 14:41:49 +0530 Subject: [PATCH 03/11] dev: move state grouping filter to util function --- apiserver/plane/api/views/issue.py | 9 --------- apiserver/plane/utils/issue_filters.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 9cb1e96b5bc..7f3bbd8aff0 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -173,19 +173,10 @@ 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) issue_queryset = ( self.get_queryset() .order_by(request.GET.get("order_by", "created_at")) - .filter(state__group__in=group) .filter(**filters) ) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 9a412ad99ea..41d65971ddb 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -94,6 +94,17 @@ def filter_completed_at(params, filter): ) +def filter_issue_state_type(params, filter): + type = params.get("type", "all") + group = ["backlog", "unstarted", "started", "completed", "cancelled"] + if type == "backlog": + group = ["backlog"] + if type == "active": + group = ["unstarted", "started"] + + filter["state__group__in"] = group + + def issue_filters(query_params): filter = dict() @@ -110,6 +121,7 @@ def issue_filters(query_params): "start_date": filter_start_date, "target_date": filter_target_date, "completed_at": filter_completed_at, + "type": filter_issue_state_type, } for key, value in ISSUE_FILTER.items(): From d2c80675eed9a7c0ba50765d9facd13666319a26 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Mar 2023 22:52:51 +0530 Subject: [PATCH 04/11] dev: view issues create endpoint and update on filters for time --- apiserver/plane/api/serializers/view.py | 25 +++ apiserver/plane/api/urls.py | 6 + apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/issue.py | 2 +- apiserver/plane/api/views/view.py | 96 ++++++++++- apiserver/plane/utils/issue_filters.py | 201 ++++++++++++++++-------- 6 files changed, 261 insertions(+), 71 deletions(-) diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index 23ac768efe9..17e0f7ecedd 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -1,7 +1,11 @@ +# Third party imports +from rest_framework import serializers + # Module imports from .base import BaseSerializer from plane.db.models import View +from plane.utils.issue_filters import issue_filters class ViewSerializer(BaseSerializer): @@ -12,3 +16,24 @@ class Meta: "workspace", "project", ] + + def create(self, validated_data): + query_params = validated_data.pop("query", {}) + + if not bool(query_params): + raise serializers.ValidationError( + {"query": ["Query field cannot be empty"]} + ) + + validated_data["query"] = issue_filters(query_params, "POST") + return View.objects.create(**validated_data) + + def update(self, instance, validated_data): + query_params = validated_data.pop("query", {}) + if not bool(query_params): + raise serializers.ValidationError( + {"query": ["Query field cannot be empty"]} + ) + + validated_data["query"] = issue_filters(query_params, "POST") + return super().update(instance, validated_data) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index e75c29c1215..703e356adc4 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -77,6 +77,7 @@ ## End Shortcuts # Views ViewViewSet, + ViewIssuesEndpoint, ## End Views # Cycles CycleViewSet, @@ -472,6 +473,11 @@ ), name="project-view", ), + path( + "workspaces//projects//views//issues/", + ViewIssuesEndpoint.as_view(), + name="project-view-issues", + ), ## End Views ## Cycles path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 2556fc7d976..b4c5edc5e5b 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -39,7 +39,7 @@ ) from .state import StateViewSet from .shortcut import ShortCutViewSet -from .view import ViewViewSet +from .view import ViewViewSet, ViewIssuesEndpoint from .cycle import ( CycleViewSet, CycleIssueViewSet, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 7f3bbd8aff0..ce5a0ee0af6 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -173,7 +173,7 @@ def get_queryset(self): def list(self, request, slug, project_id): try: - filters = issue_filters(request.query_params) + filters = issue_filters(request.query_params, "GET") issue_queryset = ( self.get_queryset() .order_by(request.GET.get("order_by", "created_at")) diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 4ae4ff2c1f3..a823db5492a 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -1,12 +1,26 @@ +# Django imports +from django.db.models import Prefetch + +# 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 ViewSerializer, IssueSerializer from plane.api.permissions import ProjectEntityPermission -from plane.db.models import View +from plane.db.models import ( + View, + Issue, + IssueBlocker, + IssueLink, + CycleIssue, + ModuleIssue, +) class ViewViewSet(BaseViewSet): - serializer_class = ViewSerializer model = View permission_classes = [ @@ -27,3 +41,77 @@ def get_queryset(self): .select_related("workspace") .distinct() ) + + +class ViewIssuesEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id, view_id): + try: + view = View.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 View.DoesNotExist: + return Response( + {"error": "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, + ) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 41d65971ddb..0b8a9bd46cc 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -2,99 +2,169 @@ from django.utils.dateparse import parse_datetime -def filter_state(params, filter): - filter["state__in"] = params.getlist("state") +def filter_state(params, filter, method): + if method == "GET": + if len(params.getlist("state")): + filter["state__in"] = params.getlist("state") + else: + if len(params.get("state")): + filter["state__in"] = params.get("state") return filter -def filter_priority(params, filter): - filter["priority__in"] = params.getlist("priority") +def filter_priority(params, filter, method): + if method == "GET": + if len(params.getlist("priority")): + filter["priority__in"] = params.getlist("priority") + else: + if len(params.get("priority")): + filter["priority__in"] = params.get("priority") return filter -def filter_parent(params, filter): - filter["parent__in"] = params.getlist("parent") +def filter_parent(params, filter, method): + if method == "GET": + if len(params.getlist("parent")): + filter["parent__in"] = params.getlist("parent") + else: + if len(params.get("parent")): + filter["parent__in"] = params.get("parent") return filter -def filter_labels(params, filter): - filter["labels__in"] = params.getlist("labels") +def filter_labels(params, filter, method): + if method == "GET": + if len(params.getlist("labels")): + filter["labels__in"] = params.getlist("labels") + else: + if len(params.get("labels")): + filter["labels__in"] = params.get("labels") return filter -def filter_assignees(params, filter): - filter["assignees__in"] = params.getlist("assignees") +def filter_assignees(params, filter, method): + if method == "GET": + if len(params.getlist("assignees")): + filter["assignees__in"] = params.getlist("assignees") + else: + if len(params.get("assignees")): + filter["assignees__in"] = params.get("assignees") return filter -def filter_created_by(params, filter): - filter["created_by__in"] = params.getlist("created_by") +def filter_created_by(params, filter, method): + if method == "GET": + if len(params.getlist("created_by")): + filter["created_by__in"] = params.getlist("created_by") + else: + if len(params.get("created_by")): + filter["created_by__in"] = params.get("created_by") return filter -def filter_content(params, filter): - filter["description_html__in"] = params.get("description_html") +def filter_name(params, filter, method): + if params.get("name", "") != "": + filter["name__icontains"] = params.get("name") return filter -def filter_created_at(params, filter): - for query in params.getlist("created_at"): - created_at_query = query.split("|") - if len(created_at_query) == 2 and "after" in created_at_query: - filter["created_at__gte"] = make_aware(parse_datetime(created_at_query[0])) - else: - filter["created_at__lte"] = make_aware(parse_datetime(created_at_query[0])) +def filter_created_at(params, filter, method): + if method == "GET": + if len(params.getlist("created_at")): + for query in params.getlist("created_at"): + created_at_query = query.split(",") + if len(created_at_query) == 2 and "after" in created_at_query: + filter["created_at__date__gte"] = created_at_query[0] + else: + filter["created_at__date__lte"] = created_at_query[0] + else: + if len(params.get("created_at")): + for query in params.get("created_at"): + if query.get("timeline", "after") == "after": + filter["created_at__date__gte"] = query.get("datetime") + else: + filter["created_at__date__lte"] = query.get("datetime") + return filter + +def filter_updated_at(params, filter, method): + if method == "GET": + if len(params.getlist("updated_at")): + for query in params.getlist("updated_at"): + updated_at_query = query.split(",") + if len(updated_at_query) == 2 and "after" in updated_at_query: + filter["updated_at__date__gte"] = updated_at_query[0] + else: + filter["updated_at__date__lte"] = updated_at_query[0] + else: + if len(params.get("updated_at")): + for query in params.get("updated_at"): + if query.get("timeline", "after") == "after": + filter["updated_at__date__gte"] = query.get("datetime") + else: + filter["updated_at__date__lte"] = query.get("datetime") return filter -def filter_updated_at(params, filter): - for query in params.getlist("updated_at"): - updated_at_query = query.split("|") - if len(updated_at_query) == 2 and "after" in updated_at_query: - filter["updated_at__gte"] = make_aware(parse_datetime(updated_at_query[0])) - else: - filter["updated_at__lte"] = make_aware(parse_datetime(updated_at_query[0])) +def filter_start_date(params, filter, method): + if method == "GET": + if len(params.getlist("start_date")): + for query in params.getlist("start_date"): + start_date_query = query.split(",") + if len(start_date_query) == 2 and "after" in start_date_query: + filter["start_date__date__gte"] = start_date_query[0] + else: + filter["start_date__date__lte"] = start_date_query[0] + else: + if len(params.get("start_date")): + for query in params.get("start_date"): + if query.get("timeline", "after") == "after": + filter["start_date__date__gte"] = query.get("datetime") + else: + filter["start_date__date__lte"] = query.get("datetime") return filter -def filter_start_date(params, filter): - for query in params.getlist("start_date"): - start_date_query = query.split("|") - if len(start_date_query) == 2 and "after" in start_date_query: - filter["start_date__gte"] = make_aware(parse_datetime(start_date_query[0])) - else: - filter["start_date__lte"] = make_aware(parse_datetime(start_date_query[0])) +def filter_target_date(params, filter, method): + if method == "GET": + if len(params.getlist("target_date")): + for query in params.getlist("target_date"): + target_date_query = query.split(",") + if len(target_date_query) == 2 and "after" in target_date_query: + filter["target_date__date__gte"] = target_date_query[0] + else: + filter["target_date__date__lte"] = target_date_query[0] + else: + if len(params.get("target_date")): + for query in params.get("target_date"): + if query.get("timeline", "after") == "after": + filter["target_date__date__gte"] = query.get("datetime") + else: + filter["target_date__date__lte"] = query.get("datetime") + return filter -def filter_target_date(params, filter): - for query in params.getlist("target_date"): - target_date_query = query.split("|") - if len(target_date_query) == 2 and "after" in target_date_query: - filter["target_date__gte"] = make_aware( - parse_datetime(target_date_query[0]) - ) - else: - filter["target_date__lte"] = make_aware( - parse_datetime(target_date_query[0]) - ) - - -def filter_completed_at(params, filter): - for query in params.getlist("completed_at"): - completed_at_query = query.split("|") - if len(completed_at_query) == 2 and "after" in completed_at_query: - filter["completed_at__gte"] = make_aware( - parse_datetime(completed_at_query[0]) - ) - else: - filter["completed_at__lte"] = make_aware( - parse_datetime(completed_at_query[0]) - ) - - -def filter_issue_state_type(params, filter): +def filter_completed_at(params, filter, method): + if method == "GET": + if len(params.getlist("completed_at")): + for query in params.getlist("completed_at"): + completed_at_query = query.split(",") + if len(completed_at_query) == 2 and "after" in completed_at_query: + filter["completed_at__date__gte"] = completed_at_query[0] + else: + filter["completed_at__lte"] = completed_at_query[0] + else: + if len(params.get("completed_at")): + for query in params.get("completed_at"): + if query.get("timeline", "after") == "after": + filter["completed_at__date__gte"] = query.get("datetime") + else: + filter["completed_at__lte"] = query.get("datetime") + return filter + + +def filter_issue_state_type(params, filter, method): type = params.get("type", "all") group = ["backlog", "unstarted", "started", "completed", "cancelled"] if type == "backlog": @@ -103,9 +173,10 @@ def filter_issue_state_type(params, filter): group = ["unstarted", "started"] filter["state__group__in"] = group + return filter -def issue_filters(query_params): +def issue_filters(query_params, method): filter = dict() ISSUE_FILTER = { @@ -115,7 +186,7 @@ def issue_filters(query_params): "labels": filter_labels, "assignees": filter_assignees, "created_by": filter_created_by, - "content": filter_content, + "name": filter_name, "created_at": filter_created_at, "updated_at": filter_updated_at, "start_date": filter_start_date, @@ -127,6 +198,6 @@ def issue_filters(query_params): for key, value in ISSUE_FILTER.items(): if key in query_params: func = value - func(query_params, filter) + func(query_params, filter, method) return filter From e6bd808138dd1e8d95f5b75e04d634949122eed5 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 11 Mar 2023 10:14:31 +0530 Subject: [PATCH 05/11] dev: rename views to issue views --- apiserver/plane/api/serializers/__init__.py | 2 +- apiserver/plane/api/serializers/view.py | 2 +- apiserver/plane/api/urls.py | 6 +++--- apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/view.py | 2 +- apiserver/plane/db/models/__init__.py | 2 +- apiserver/plane/db/models/view.py | 8 ++++---- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 9814ace37c3..9e8dc49d69a 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -21,7 +21,7 @@ ) from .state import StateSerializer from .shortcut import ShortCutSerializer -from .view import ViewSerializer +from .view import IssueViewSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer from .asset import FileAssetSerializer from .issue import ( diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index 17e0f7ecedd..291bea74b77 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -8,7 +8,7 @@ from plane.utils.issue_filters import issue_filters -class ViewSerializer(BaseSerializer): +class IssueViewSerializer(BaseSerializer): class Meta: model = View fields = "__all__" diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 703e356adc4..039ae2cf629 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -76,7 +76,7 @@ ShortCutViewSet, ## End Shortcuts # Views - ViewViewSet, + IssueViewViewSet, ViewIssuesEndpoint, ## End Views # Cycles @@ -453,7 +453,7 @@ # Views path( "workspaces//projects//views/", - ViewViewSet.as_view( + IssueViewViewSet.as_view( { "get": "list", "post": "create", @@ -463,7 +463,7 @@ ), path( "workspaces//projects//views//", - ViewViewSet.as_view( + IssueViewViewSet.as_view( { "get": "retrieve", "put": "update", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index b4c5edc5e5b..6ebdf7ea147 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -39,7 +39,7 @@ ) from .state import StateViewSet from .shortcut import ShortCutViewSet -from .view import ViewViewSet, ViewIssuesEndpoint +from .view import IssueViewViewSet, ViewIssuesEndpoint from .cycle import ( CycleViewSet, CycleIssueViewSet, diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index a823db5492a..97e20aeedf7 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -20,7 +20,7 @@ ) -class ViewViewSet(BaseViewSet): +class IssueViewViewSet(BaseViewSet): serializer_class = ViewSerializer model = View permission_classes = [ diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 09b44b422e8..5586ef600d9 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -43,7 +43,7 @@ from .shortcut import Shortcut -from .view import View +from .view import IssueView from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 26b45f9318a..7237f928fc1 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -6,7 +6,7 @@ from . import ProjectBaseModel -class View(ProjectBaseModel): +class IssueView(ProjectBaseModel): name = models.CharField(max_length=255, verbose_name="View Name") description = models.TextField(verbose_name="View Description", blank=True) query = models.JSONField(verbose_name="View Query") @@ -15,9 +15,9 @@ class View(ProjectBaseModel): ) class Meta: - verbose_name = "View" - verbose_name_plural = "Views" - db_table = "views" + verbose_name = "Issue View" + verbose_name_plural = "Issue Views" + db_table = "issue_views" ordering = ("-created_at",) def __str__(self): From c6139352f4331cefccbcdfeb42601203c4050cef Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 11 Mar 2023 10:18:14 +0530 Subject: [PATCH 06/11] dev: rename in serilaizer and views --- apiserver/plane/api/serializers/view.py | 6 +++--- apiserver/plane/api/views/view.py | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index 291bea74b77..d00258278b7 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -4,13 +4,13 @@ # Module imports from .base import BaseSerializer -from plane.db.models import View +from plane.db.models import IssueView from plane.utils.issue_filters import issue_filters class IssueViewSerializer(BaseSerializer): class Meta: - model = View + model = IssueView fields = "__all__" read_only_fields = [ "workspace", @@ -26,7 +26,7 @@ def create(self, validated_data): ) validated_data["query"] = issue_filters(query_params, "POST") - return View.objects.create(**validated_data) + return IssueView.objects.create(**validated_data) def update(self, instance, validated_data): query_params = validated_data.pop("query", {}) diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 97e20aeedf7..4b424a6802b 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -8,10 +8,10 @@ # Module imports from . import BaseViewSet, BaseAPIView -from plane.api.serializers import ViewSerializer, IssueSerializer +from plane.api.serializers import IssueViewSerializer, IssueSerializer from plane.api.permissions import ProjectEntityPermission from plane.db.models import ( - View, + IssueView, Issue, IssueBlocker, IssueLink, @@ -21,8 +21,8 @@ class IssueViewViewSet(BaseViewSet): - serializer_class = ViewSerializer - model = View + serializer_class = IssueViewSerializer + model = IssueView permission_classes = [ ProjectEntityPermission, ] @@ -50,7 +50,7 @@ class ViewIssuesEndpoint(BaseAPIView): def get(self, request, slug, project_id, view_id): try: - view = View.objects.get(pk=view_id) + view = IssueView.objects.get(pk=view_id) queries = view.query issues = ( @@ -105,9 +105,9 @@ def get(self, request, slug, project_id, view_id): serializer = IssueSerializer(issues, many=True) return Response(serializer.data, status=status.HTTP_200_OK) - except View.DoesNotExist: + except IssueView.DoesNotExist: return Response( - {"error": "View does not exist"}, status=status.HTTP_404_NOT_FOUND + {"error": "Issue View does not exist"}, status=status.HTTP_404_NOT_FOUND ) except Exception as e: capture_exception(e) From 1637e3d9b30994af95b478e15cd4a98612cfd7a1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 13 Mar 2023 16:35:53 +0530 Subject: [PATCH 07/11] dev: update issue filters --- apiserver/plane/utils/issue_filters.py | 65 +++++++++++++++----------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 0b8a9bd46cc..74af91e5bef 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -4,8 +4,9 @@ def filter_state(params, filter, method): if method == "GET": - if len(params.getlist("state")): - filter["state__in"] = params.getlist("state") + states = params.get("state").split(",") + if len(states): + filter["state__in"] = states else: if len(params.get("state")): filter["state__in"] = params.get("state") @@ -14,8 +15,9 @@ def filter_state(params, filter, method): def filter_priority(params, filter, method): if method == "GET": - if len(params.getlist("priority")): - filter["priority__in"] = params.getlist("priority") + priorties = params.get("priority").split(",") + if len(priorties): + filter["priority__in"] = priorties else: if len(params.get("priority")): filter["priority__in"] = params.get("priority") @@ -24,8 +26,9 @@ def filter_priority(params, filter, method): def filter_parent(params, filter, method): if method == "GET": - if len(params.getlist("parent")): - filter["parent__in"] = params.getlist("parent") + parents = params.get("parent").split(",") + if len(parents): + filter["parent__in"] = parents else: if len(params.get("parent")): filter["parent__in"] = params.get("parent") @@ -34,8 +37,9 @@ def filter_parent(params, filter, method): def filter_labels(params, filter, method): if method == "GET": - if len(params.getlist("labels")): - filter["labels__in"] = params.getlist("labels") + labels = params.get("labels").split(",") + if len(labels): + filter["labels__in"] = labels else: if len(params.get("labels")): filter["labels__in"] = params.get("labels") @@ -44,8 +48,9 @@ def filter_labels(params, filter, method): def filter_assignees(params, filter, method): if method == "GET": - if len(params.getlist("assignees")): - filter["assignees__in"] = params.getlist("assignees") + assignees = params.get("assignees").split(",") + if len(assignees): + filter["assignees__in"] = assignees else: if len(params.get("assignees")): filter["assignees__in"] = params.get("assignees") @@ -54,8 +59,9 @@ def filter_assignees(params, filter, method): def filter_created_by(params, filter, method): if method == "GET": - if len(params.getlist("created_by")): - filter["created_by__in"] = params.getlist("created_by") + created_bys = params.get("created_by").split(",") + if len(created_bys): + filter["created_by__in"] = created_bys else: if len(params.get("created_by")): filter["created_by__in"] = params.get("created_by") @@ -70,9 +76,10 @@ def filter_name(params, filter, method): def filter_created_at(params, filter, method): if method == "GET": - if len(params.getlist("created_at")): - for query in params.getlist("created_at"): - created_at_query = query.split(",") + created_ats = params.get("created_at").split(",") + if len(created_ats): + for query in created_ats: + created_at_query = query.split(";") if len(created_at_query) == 2 and "after" in created_at_query: filter["created_at__date__gte"] = created_at_query[0] else: @@ -89,9 +96,10 @@ def filter_created_at(params, filter, method): def filter_updated_at(params, filter, method): if method == "GET": - if len(params.getlist("updated_at")): - for query in params.getlist("updated_at"): - updated_at_query = query.split(",") + updated_bys = params.get("updated_at").split(",") + if len(updated_bys): + for query in updated_bys: + updated_at_query = query.split(";") if len(updated_at_query) == 2 and "after" in updated_at_query: filter["updated_at__date__gte"] = updated_at_query[0] else: @@ -108,9 +116,10 @@ def filter_updated_at(params, filter, method): def filter_start_date(params, filter, method): if method == "GET": - if len(params.getlist("start_date")): - for query in params.getlist("start_date"): - start_date_query = query.split(",") + start_dates = params.get("start_date").split(";") + if len(start_dates): + for query in start_dates: + start_date_query = query.split(";") if len(start_date_query) == 2 and "after" in start_date_query: filter["start_date__date__gte"] = start_date_query[0] else: @@ -127,9 +136,10 @@ def filter_start_date(params, filter, method): def filter_target_date(params, filter, method): if method == "GET": - if len(params.getlist("target_date")): - for query in params.getlist("target_date"): - target_date_query = query.split(",") + target_dates = params.get("target_date").split(";") + if len(target_dates): + for query in target_dates: + target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: filter["target_date__date__gte"] = target_date_query[0] else: @@ -147,9 +157,10 @@ def filter_target_date(params, filter, method): def filter_completed_at(params, filter, method): if method == "GET": - if len(params.getlist("completed_at")): - for query in params.getlist("completed_at"): - completed_at_query = query.split(",") + completed_ats = params.get("completed_at").split(",") + if len(completed_ats): + for query in completed_ats: + completed_at_query = query.split(";") if len(completed_at_query) == 2 and "after" in completed_at_query: filter["completed_at__date__gte"] = completed_at_query[0] else: From 127e3d7e6435783b0bbc50e4e6712d4b7d53301e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 13 Mar 2023 16:43:49 +0530 Subject: [PATCH 08/11] dev: update filter --- apiserver/plane/utils/issue_filters.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 74af91e5bef..2baf7bdc189 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -5,7 +5,7 @@ def filter_state(params, filter, method): if method == "GET": states = params.get("state").split(",") - if len(states): + if len(states) and "" not in states: filter["state__in"] = states else: if len(params.get("state")): @@ -16,7 +16,7 @@ def filter_state(params, filter, method): def filter_priority(params, filter, method): if method == "GET": priorties = params.get("priority").split(",") - if len(priorties): + if len(priorties) and "" not in priorties: filter["priority__in"] = priorties else: if len(params.get("priority")): @@ -27,7 +27,7 @@ def filter_priority(params, filter, method): def filter_parent(params, filter, method): if method == "GET": parents = params.get("parent").split(",") - if len(parents): + if len(parents) and "" not in parents: filter["parent__in"] = parents else: if len(params.get("parent")): @@ -38,7 +38,7 @@ def filter_parent(params, filter, method): def filter_labels(params, filter, method): if method == "GET": labels = params.get("labels").split(",") - if len(labels): + if len(labels) and "" not in labels: filter["labels__in"] = labels else: if len(params.get("labels")): @@ -49,7 +49,7 @@ def filter_labels(params, filter, method): def filter_assignees(params, filter, method): if method == "GET": assignees = params.get("assignees").split(",") - if len(assignees): + if len(assignees) and "" not in assignees: filter["assignees__in"] = assignees else: if len(params.get("assignees")): @@ -60,7 +60,7 @@ def filter_assignees(params, filter, method): def filter_created_by(params, filter, method): if method == "GET": created_bys = params.get("created_by").split(",") - if len(created_bys): + if len(created_bys) and "" not in created_bys: filter["created_by__in"] = created_bys else: if len(params.get("created_by")): @@ -77,7 +77,7 @@ def filter_name(params, filter, method): def filter_created_at(params, filter, method): if method == "GET": created_ats = params.get("created_at").split(",") - if len(created_ats): + if len(created_ats) and "" not in created_ats: for query in created_ats: created_at_query = query.split(";") if len(created_at_query) == 2 and "after" in created_at_query: @@ -97,7 +97,7 @@ def filter_created_at(params, filter, method): def filter_updated_at(params, filter, method): if method == "GET": updated_bys = params.get("updated_at").split(",") - if len(updated_bys): + if len(updated_bys) and "" not in updated_bys: for query in updated_bys: updated_at_query = query.split(";") if len(updated_at_query) == 2 and "after" in updated_at_query: @@ -117,7 +117,7 @@ def filter_updated_at(params, filter, method): def filter_start_date(params, filter, method): if method == "GET": start_dates = params.get("start_date").split(";") - if len(start_dates): + if len(start_dates) and "" not in start_dates: for query in start_dates: start_date_query = query.split(";") if len(start_date_query) == 2 and "after" in start_date_query: @@ -137,7 +137,7 @@ def filter_start_date(params, filter, method): def filter_target_date(params, filter, method): if method == "GET": target_dates = params.get("target_date").split(";") - if len(target_dates): + if len(target_dates) and "" not in target_dates: for query in target_dates: target_date_query = query.split(";") if len(target_date_query) == 2 and "after" in target_date_query: @@ -158,7 +158,7 @@ def filter_target_date(params, filter, method): def filter_completed_at(params, filter, method): if method == "GET": completed_ats = params.get("completed_at").split(",") - if len(completed_ats): + if len(completed_ats) and "" not in completed_ats: for query in completed_ats: completed_at_query = query.split(";") if len(completed_at_query) == 2 and "after" in completed_at_query: From 6d08ab9b31d6f5642e9942cb055d99ca53b9df0a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 14 Mar 2023 10:42:45 +0530 Subject: [PATCH 09/11] feat: create issue favorites --- apiserver/plane/api/serializers/__init__.py | 2 +- apiserver/plane/api/serializers/view.py | 17 ++++- apiserver/plane/api/urls.py | 20 ++++++ apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/view.py | 79 ++++++++++++++++++++- apiserver/plane/db/models/__init__.py | 2 +- apiserver/plane/db/models/view.py | 24 ++++++- 7 files changed, 139 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 9e8dc49d69a..dc55019c042 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -21,7 +21,7 @@ ) from .state import StateSerializer from .shortcut import ShortCutSerializer -from .view import IssueViewSerializer +from .view import IssueViewSerializer, IssueViewFavoriteSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer from .asset import FileAssetSerializer from .issue import ( diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index d00258278b7..b5b46bad682 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -4,11 +4,13 @@ # Module imports from .base import BaseSerializer -from plane.db.models import IssueView +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__" @@ -37,3 +39,16 @@ def update(self, instance, validated_data): validated_data["query"] = issue_filters(query_params, "POST") return super().update(instance, validated_data) + + +class IssueViewFavoriteSerializer(BaseSerializer): + view_detail = IssueViewSerializer(source="issue_view", read_only=True) + + class Meta: + model = IssueViewFavorite + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "user", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 039ae2cf629..7cd6f12533c 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -78,6 +78,7 @@ # Views IssueViewViewSet, ViewIssuesEndpoint, + IssueViewFavoriteViewSet, ## End Views # Cycles CycleViewSet, @@ -478,6 +479,25 @@ ViewIssuesEndpoint.as_view(), name="project-view-issues", ), + path( + "workspaces//projects//user-favorite-views/", + IssueViewFavoriteViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="user-favorite-view", + ), + path( + "workspaces//projects//user-favorite-views//", + IssueViewFavoriteViewSet.as_view( + { + "delete": "destroy", + } + ), + name="user-favorite-view", + ), ## End Views ## Cycles path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 6ebdf7ea147..59c381205a6 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -39,7 +39,7 @@ ) from .state import StateViewSet from .shortcut import ShortCutViewSet -from .view import IssueViewViewSet, ViewIssuesEndpoint +from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet from .cycle import ( CycleViewSet, CycleIssueViewSet, diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/api/views/view.py index 4b424a6802b..6c4241d7e8b 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/api/views/view.py @@ -1,5 +1,6 @@ # Django imports -from django.db.models import Prefetch +from django.db import IntegrityError +from django.db.models import Prefetch, OuterRef, Exists # Third party imports from rest_framework.response import Response @@ -8,7 +9,11 @@ # Module imports from . import BaseViewSet, BaseAPIView -from plane.api.serializers import IssueViewSerializer, IssueSerializer +from plane.api.serializers import ( + IssueViewSerializer, + IssueSerializer, + IssueViewFavoriteSerializer, +) from plane.api.permissions import ProjectEntityPermission from plane.db.models import ( IssueView, @@ -17,6 +22,7 @@ IssueLink, CycleIssue, ModuleIssue, + IssueViewFavorite, ) @@ -31,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() @@ -39,6 +51,7 @@ def get_queryset(self): .filter(project__project_projectmember__member=self.request.user) .select_related("project") .select_related("workspace") + .annotate(is_favorite=Exists(subquery)) .distinct() ) @@ -115,3 +128,65 @@ def get(self, request, slug, project_id, view_id): {"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, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 5586ef600d9..3ed90f1e712 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -43,7 +43,7 @@ from .shortcut import Shortcut -from .view import IssueView +from .view import IssueView, IssueViewFavorite from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 7237f928fc1..9bb1947c070 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -1,6 +1,6 @@ # Django imports from django.db import models - +from django.conf import settings # Module import from . import ProjectBaseModel @@ -23,3 +23,25 @@ class Meta: def __str__(self): """Return name of the View""" return f"{self.name} <{self.project.name}>" + + +class IssueViewFavorite(ProjectBaseModel): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="user_view_favorites", + ) + view = models.ForeignKey( + "db.IssueView", on_delete=models.CASCADE, related_name="view_favorites" + ) + + class Meta: + unique_together = ["view", "user"] + verbose_name = "View Favorite" + verbose_name_plural = "View Favorites" + db_table = "view_favorites" + ordering = ("-created_at",) + + def __str__(self): + """Return user and the view""" + return f"{self.user.email} <{self.view.name}>" From d101b48d6c7858bfddf813a704df9adedcaf7eb6 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Mar 2023 13:10:58 +0530 Subject: [PATCH 10/11] dev: update query keys --- apiserver/plane/api/serializers/view.py | 11 ++++++----- apiserver/plane/db/models/view.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index b5b46bad682..e2b60dafce2 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -17,27 +17,28 @@ class Meta: read_only_fields = [ "workspace", "project", + "query", ] def create(self, validated_data): - query_params = validated_data.pop("query", {}) + query_params = validated_data.pop("query_data", {}) if not bool(query_params): raise serializers.ValidationError( - {"query": ["Query field cannot be empty"]} + {"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.pop("query", {}) + query_params = validated_data.pop("query_data", {}) if not bool(query_params): raise serializers.ValidationError( - {"query": ["Query field cannot be empty"]} + {"query_data": ["Query data field cannot be empty"]} ) - validated_data["query"] = issue_filters(query_params, "POST") + validated_data["query"] = issue_filters(query_params, "PATCH") return super().update(instance, validated_data) diff --git a/apiserver/plane/db/models/view.py b/apiserver/plane/db/models/view.py index 9bb1947c070..6a968af5345 100644 --- a/apiserver/plane/db/models/view.py +++ b/apiserver/plane/db/models/view.py @@ -13,6 +13,7 @@ class IssueView(ProjectBaseModel): access = models.PositiveSmallIntegerField( default=1, choices=((0, "Private"), (1, "Public")) ) + query_data = models.JSONField(default=dict) class Meta: verbose_name = "Issue View" From 8a3d4cd970a3d3ee712ae3657bc81f63a7809c20 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Mar 2023 13:13:48 +0530 Subject: [PATCH 11/11] dev: update create and update method --- apiserver/plane/api/serializers/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/api/serializers/view.py index e2b60dafce2..b998aace321 100644 --- a/apiserver/plane/api/serializers/view.py +++ b/apiserver/plane/api/serializers/view.py @@ -21,7 +21,7 @@ class Meta: ] def create(self, validated_data): - query_params = validated_data.pop("query_data", {}) + query_params = validated_data.get("query_data", {}) if not bool(query_params): raise serializers.ValidationError( @@ -32,7 +32,7 @@ def create(self, validated_data): return IssueView.objects.create(**validated_data) def update(self, instance, validated_data): - query_params = validated_data.pop("query_data", {}) + query_params = validated_data.get("query_data", {}) if not bool(query_params): raise serializers.ValidationError( {"query_data": ["Query data field cannot be empty"]}