From ff5cddeb95ee85dd5a1d95766cb4a156aaed6af0 Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 5 Apr 2023 00:17:16 +0530 Subject: [PATCH] feat: issue search endpoint (#667) * feat: issue search endpoints * dev: update issue search for blocker and blocked by --- apiserver/plane/api/urls.py | 6 + apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/search.py | 190 ++++++++++++++++++-------- apiserver/plane/utils/issue_search.py | 23 ++++ 4 files changed, 160 insertions(+), 61 deletions(-) create mode 100644 apiserver/plane/utils/issue_search.py diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index d408be37e..800f584c8 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -133,6 +133,7 @@ from plane.api.views import ( ## End importer # Search GlobalSearchEndpoint, + IssueSearchEndpoint, ## End Search # Gpt GPTIntegrationEndpoint, @@ -1170,6 +1171,11 @@ urlpatterns = [ GlobalSearchEndpoint.as_view(), name="global-search", ), + path( + "workspaces//projects//search-issues/", + IssueSearchEndpoint.as_view(), + name="project-issue-search", + ), ## End Search # Gpt path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index b6171d68b..f84e78a16 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -125,7 +125,7 @@ from .page import ( CreatedbyOtherPagesEndpoint, ) -from .search import GlobalSearchEndpoint +from .search import GlobalSearchEndpoint, IssueSearchEndpoint from .gpt import GPTIntegrationEndpoint diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/api/views/search.py index ba75eac91..70b7d0d0a 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/api/views/search.py @@ -12,6 +12,7 @@ from sentry_sdk import capture_exception # Module imports from .base import BaseAPIView from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView +from plane.utils.issue_search import search_issues class GlobalSearchEndpoint(BaseAPIView): @@ -24,20 +25,26 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return Workspace.objects.filter( - q, workspace_member__member=self.request.user - ).distinct().values("name", "id", "slug") + return ( + Workspace.objects.filter(q, workspace_member__member=self.request.user) + .distinct() + .values("name", "id", "slug") + ) def filter_projects(self, query, slug, project_id): fields = ["name"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return Project.objects.filter( - q, - Q(project_projectmember__member=self.request.user) | Q(network=2), - workspace__slug=slug, - ).distinct().values("name", "id", "identifier", "workspace__slug") + return ( + Project.objects.filter( + q, + Q(project_projectmember__member=self.request.user) | Q(network=2), + workspace__slug=slug, + ) + .distinct() + .values("name", "id", "identifier", "workspace__slug") + ) def filter_issues(self, query, slug, project_id): fields = ["name", "sequence_id"] @@ -49,18 +56,22 @@ class GlobalSearchEndpoint(BaseAPIView): q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) - return Issue.objects.filter( - q, - project__project_projectmember__member=self.request.user, - workspace__slug=slug, - project_id=project_id, - ).distinct().values( - "name", - "id", - "sequence_id", - "project__identifier", - "project_id", - "workspace__slug", + return ( + Issue.objects.filter( + q, + project__project_projectmember__member=self.request.user, + workspace__slug=slug, + project_id=project_id, + ) + .distinct() + .values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "workspace__slug", + ) ) def filter_cycles(self, query, slug, project_id): @@ -68,16 +79,20 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return Cycle.objects.filter( - q, - project__project_projectmember__member=self.request.user, - workspace__slug=slug, - project_id=project_id, - ).distinct().values( - "name", - "id", - "project_id", - "workspace__slug", + return ( + Cycle.objects.filter( + q, + project__project_projectmember__member=self.request.user, + workspace__slug=slug, + project_id=project_id, + ) + .distinct() + .values( + "name", + "id", + "project_id", + "workspace__slug", + ) ) def filter_modules(self, query, slug, project_id): @@ -85,16 +100,20 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return Module.objects.filter( - q, - project__project_projectmember__member=self.request.user, - workspace__slug=slug, - project_id=project_id, - ).distinct().values( - "name", - "id", - "project_id", - "workspace__slug", + return ( + Module.objects.filter( + q, + project__project_projectmember__member=self.request.user, + workspace__slug=slug, + project_id=project_id, + ) + .distinct() + .values( + "name", + "id", + "project_id", + "workspace__slug", + ) ) def filter_pages(self, query, slug, project_id): @@ -102,16 +121,20 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return Page.objects.filter( - q, - project__project_projectmember__member=self.request.user, - workspace__slug=slug, - project_id=project_id, - ).distinct().values( - "name", - "id", - "project_id", - "workspace__slug", + return ( + Page.objects.filter( + q, + project__project_projectmember__member=self.request.user, + workspace__slug=slug, + project_id=project_id, + ) + .distinct() + .values( + "name", + "id", + "project_id", + "workspace__slug", + ) ) def filter_views(self, query, slug, project_id): @@ -119,16 +142,20 @@ class GlobalSearchEndpoint(BaseAPIView): q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) - return IssueView.objects.filter( - q, - project__project_projectmember__member=self.request.user, - workspace__slug=slug, - project_id=project_id, - ).distinct().values( - "name", - "id", - "project_id", - "workspace__slug", + return ( + IssueView.objects.filter( + q, + project__project_projectmember__member=self.request.user, + workspace__slug=slug, + project_id=project_id, + ) + .distinct() + .values( + "name", + "id", + "project_id", + "workspace__slug", + ) ) def get(self, request, slug, project_id): @@ -173,3 +200,46 @@ class GlobalSearchEndpoint(BaseAPIView): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueSearchEndpoint(BaseAPIView): + def get(self, request, slug, project_id): + try: + query = request.query_params.get("search", False) + parent = request.query_params.get("parent", False) + blocker_blocked_by = request.query_params.get("blocker_blocked_by", False) + issue_id = request.query_params.get("issue_id", False) + + issues = search_issues(query) + issues = issues.filter( + workspace__slug=slug, + project_id=project_id, + project__project_projectmember__member=self.request.user, + ) + + if parent: + issues.filter(parent__isnull=True) + if blocker_blocked_by and issue_id: + issues.filter(blocker_issues=issue_id, blocked_issues=issue_id) + + return Response( + issues.values( + "name", + "id", + "sequence_id", + "project__identifier", + "project_id", + "workspace__slug", + ), + status=status.HTTP_200_OK, + ) + except Issue.DoesNotExist: + return Response( + {"error": "Issue Does not exist"}, 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/utils/issue_search.py b/apiserver/plane/utils/issue_search.py new file mode 100644 index 000000000..93b0df6da --- /dev/null +++ b/apiserver/plane/utils/issue_search.py @@ -0,0 +1,23 @@ +# Python imports +import re + +# Django imports +from django.db.models import Q + +# Module imports +from plane.db.models import Issue + + +def search_issues(query): + fields = ["name", "sequence_id"] + q = Q() + for field in fields: + if field == "sequence_id": + sequences = re.findall(r"\d+\.\d+|\d+", query) + for sequence_id in sequences: + q |= Q(**{"sequence_id": sequence_id}) + else: + q |= Q(**{f"{field}__icontains": query}) + return Issue.objects.filter( + q, + ).distinct()