forked from github/plane
feat: issue search endpoint (#667)
* feat: issue search endpoints * dev: update issue search for blocker and blocked by
This commit is contained in:
parent
8cbf75ad6c
commit
ff5cddeb95
@ -133,6 +133,7 @@ from plane.api.views import (
|
|||||||
## End importer
|
## End importer
|
||||||
# Search
|
# Search
|
||||||
GlobalSearchEndpoint,
|
GlobalSearchEndpoint,
|
||||||
|
IssueSearchEndpoint,
|
||||||
## End Search
|
## End Search
|
||||||
# Gpt
|
# Gpt
|
||||||
GPTIntegrationEndpoint,
|
GPTIntegrationEndpoint,
|
||||||
@ -1170,6 +1171,11 @@ urlpatterns = [
|
|||||||
GlobalSearchEndpoint.as_view(),
|
GlobalSearchEndpoint.as_view(),
|
||||||
name="global-search",
|
name="global-search",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/search-issues/",
|
||||||
|
IssueSearchEndpoint.as_view(),
|
||||||
|
name="project-issue-search",
|
||||||
|
),
|
||||||
## End Search
|
## End Search
|
||||||
# Gpt
|
# Gpt
|
||||||
path(
|
path(
|
||||||
|
@ -125,7 +125,7 @@ from .page import (
|
|||||||
CreatedbyOtherPagesEndpoint,
|
CreatedbyOtherPagesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .search import GlobalSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .gpt import GPTIntegrationEndpoint
|
from .gpt import GPTIntegrationEndpoint
|
||||||
|
@ -12,6 +12,7 @@ from sentry_sdk import capture_exception
|
|||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
|
from plane.db.models import Workspace, Project, Issue, Cycle, Module, Page, IssueView
|
||||||
|
from plane.utils.issue_search import search_issues
|
||||||
|
|
||||||
|
|
||||||
class GlobalSearchEndpoint(BaseAPIView):
|
class GlobalSearchEndpoint(BaseAPIView):
|
||||||
@ -24,20 +25,26 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Workspace.objects.filter(
|
return (
|
||||||
q, workspace_member__member=self.request.user
|
Workspace.objects.filter(q, workspace_member__member=self.request.user)
|
||||||
).distinct().values("name", "id", "slug")
|
.distinct()
|
||||||
|
.values("name", "id", "slug")
|
||||||
|
)
|
||||||
|
|
||||||
def filter_projects(self, query, slug, project_id):
|
def filter_projects(self, query, slug, project_id):
|
||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Project.objects.filter(
|
return (
|
||||||
q,
|
Project.objects.filter(
|
||||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
q,
|
||||||
workspace__slug=slug,
|
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||||
).distinct().values("name", "id", "identifier", "workspace__slug")
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.values("name", "id", "identifier", "workspace__slug")
|
||||||
|
)
|
||||||
|
|
||||||
def filter_issues(self, query, slug, project_id):
|
def filter_issues(self, query, slug, project_id):
|
||||||
fields = ["name", "sequence_id"]
|
fields = ["name", "sequence_id"]
|
||||||
@ -49,18 +56,22 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q |= Q(**{"sequence_id": sequence_id})
|
q |= Q(**{"sequence_id": sequence_id})
|
||||||
else:
|
else:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Issue.objects.filter(
|
return (
|
||||||
q,
|
Issue.objects.filter(
|
||||||
project__project_projectmember__member=self.request.user,
|
q,
|
||||||
workspace__slug=slug,
|
project__project_projectmember__member=self.request.user,
|
||||||
project_id=project_id,
|
workspace__slug=slug,
|
||||||
).distinct().values(
|
project_id=project_id,
|
||||||
"name",
|
)
|
||||||
"id",
|
.distinct()
|
||||||
"sequence_id",
|
.values(
|
||||||
"project__identifier",
|
"name",
|
||||||
"project_id",
|
"id",
|
||||||
"workspace__slug",
|
"sequence_id",
|
||||||
|
"project__identifier",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_cycles(self, query, slug, project_id):
|
def filter_cycles(self, query, slug, project_id):
|
||||||
@ -68,16 +79,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Cycle.objects.filter(
|
return (
|
||||||
q,
|
Cycle.objects.filter(
|
||||||
project__project_projectmember__member=self.request.user,
|
q,
|
||||||
workspace__slug=slug,
|
project__project_projectmember__member=self.request.user,
|
||||||
project_id=project_id,
|
workspace__slug=slug,
|
||||||
).distinct().values(
|
project_id=project_id,
|
||||||
"name",
|
)
|
||||||
"id",
|
.distinct()
|
||||||
"project_id",
|
.values(
|
||||||
"workspace__slug",
|
"name",
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_modules(self, query, slug, project_id):
|
def filter_modules(self, query, slug, project_id):
|
||||||
@ -85,16 +100,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Module.objects.filter(
|
return (
|
||||||
q,
|
Module.objects.filter(
|
||||||
project__project_projectmember__member=self.request.user,
|
q,
|
||||||
workspace__slug=slug,
|
project__project_projectmember__member=self.request.user,
|
||||||
project_id=project_id,
|
workspace__slug=slug,
|
||||||
).distinct().values(
|
project_id=project_id,
|
||||||
"name",
|
)
|
||||||
"id",
|
.distinct()
|
||||||
"project_id",
|
.values(
|
||||||
"workspace__slug",
|
"name",
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_pages(self, query, slug, project_id):
|
def filter_pages(self, query, slug, project_id):
|
||||||
@ -102,16 +121,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return Page.objects.filter(
|
return (
|
||||||
q,
|
Page.objects.filter(
|
||||||
project__project_projectmember__member=self.request.user,
|
q,
|
||||||
workspace__slug=slug,
|
project__project_projectmember__member=self.request.user,
|
||||||
project_id=project_id,
|
workspace__slug=slug,
|
||||||
).distinct().values(
|
project_id=project_id,
|
||||||
"name",
|
)
|
||||||
"id",
|
.distinct()
|
||||||
"project_id",
|
.values(
|
||||||
"workspace__slug",
|
"name",
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_views(self, query, slug, project_id):
|
def filter_views(self, query, slug, project_id):
|
||||||
@ -119,16 +142,20 @@ class GlobalSearchEndpoint(BaseAPIView):
|
|||||||
q = Q()
|
q = Q()
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
q |= Q(**{f"{field}__icontains": query})
|
||||||
return IssueView.objects.filter(
|
return (
|
||||||
q,
|
IssueView.objects.filter(
|
||||||
project__project_projectmember__member=self.request.user,
|
q,
|
||||||
workspace__slug=slug,
|
project__project_projectmember__member=self.request.user,
|
||||||
project_id=project_id,
|
workspace__slug=slug,
|
||||||
).distinct().values(
|
project_id=project_id,
|
||||||
"name",
|
)
|
||||||
"id",
|
.distinct()
|
||||||
"project_id",
|
.values(
|
||||||
"workspace__slug",
|
"name",
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"workspace__slug",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
@ -173,3 +200,46 @@ class GlobalSearchEndpoint(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 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,
|
||||||
|
)
|
||||||
|
23
apiserver/plane/utils/issue_search.py
Normal file
23
apiserver/plane/utils/issue_search.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user