# Python imports import re # Django imports from django.db import models from django.db.models import ( Q, OuterRef, Subquery, Value, UUIDField, CharField, When, Case, ) from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models.functions import Coalesce, Concat from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response # Module imports from plane.app.views.base import BaseAPIView from plane.db.models import ( Workspace, Project, Issue, Cycle, Module, Page, IssueView, ProjectMember, ProjectPage, WorkspaceMember, ) class GlobalSearchEndpoint(BaseAPIView): """Endpoint to search across multiple fields in the workspace and also show related workspace if found """ def filter_workspaces(self, query, slug, project_id, workspace_search): fields = ["name"] 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") ) def filter_projects(self, query, slug, project_id, workspace_search): fields = ["name", "identifier"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) return ( Project.objects.filter( q, project_projectmember__member=self.request.user, project_projectmember__is_active=True, archived_at__isnull=True, workspace__slug=slug, ) .distinct() .values("name", "id", "identifier", "workspace__slug") ) def filter_issues(self, query, slug, project_id, workspace_search): fields = ["name", "sequence_id", "project__identifier"] q = Q() for field in fields: if field == "sequence_id": # Match whole integers only (exclude decimal numbers) sequences = re.findall(r"\b\d+\b", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) issues = Issue.issue_objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, project__archived_at__isnull=True, workspace__slug=slug, ) if workspace_search == "false" and project_id: issues = issues.filter(project_id=project_id) return issues.distinct().values( "name", "id", "sequence_id", "project__identifier", "project_id", "workspace__slug", )[:100] def filter_cycles(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) cycles = Cycle.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, project__archived_at__isnull=True, workspace__slug=slug, ) if workspace_search == "false" and project_id: cycles = cycles.filter(project_id=project_id) return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") def filter_modules(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) modules = Module.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, project__archived_at__isnull=True, workspace__slug=slug, ) if workspace_search == "false" and project_id: modules = modules.filter(project_id=project_id) return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") def filter_pages(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) pages = ( Page.objects.filter( q, projects__project_projectmember__member=self.request.user, projects__project_projectmember__is_active=True, projects__archived_at__isnull=True, workspace__slug=slug, ) .annotate( project_ids=Coalesce( ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)), Value([], output_field=ArrayField(UUIDField())), ) ) .annotate( project_identifiers=Coalesce( ArrayAgg( "projects__identifier", distinct=True, filter=~Q(projects__id=True), ), Value([], output_field=ArrayField(CharField())), ) ) ) if workspace_search == "false" and project_id: project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list( "project_id", flat=True )[:1] pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id) return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug") def filter_views(self, query, slug, project_id, workspace_search): fields = ["name"] q = Q() for field in fields: q |= Q(**{f"{field}__icontains": query}) issue_views = IssueView.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, project__archived_at__isnull=True, workspace__slug=slug, ) if workspace_search == "false" and project_id: issue_views = issue_views.filter(project_id=project_id) return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug") def get(self, request, slug): query = request.query_params.get("search", False) workspace_search = request.query_params.get("workspace_search", "false") project_id = request.query_params.get("project_id", False) if not query: return Response( { "results": { "workspace": [], "project": [], "issue": [], "cycle": [], "module": [], "issue_view": [], "page": [], } }, status=status.HTTP_200_OK, ) MODELS_MAPPER = { "workspace": self.filter_workspaces, "project": self.filter_projects, "issue": self.filter_issues, "cycle": self.filter_cycles, "module": self.filter_modules, "issue_view": self.filter_views, "page": self.filter_pages, } results = {} for model in MODELS_MAPPER.keys(): func = MODELS_MAPPER.get(model, None) results[model] = func(query, slug, project_id, workspace_search) return Response({"results": results}, status=status.HTTP_200_OK) class SearchEndpoint(BaseAPIView): def get(self, request, slug): query = request.query_params.get("query", False) query_types = request.query_params.get("query_type", "user_mention").split(",") query_types = [qt.strip() for qt in query_types] count = int(request.query_params.get("count", 5)) project_id = request.query_params.get("project_id", None) issue_id = request.query_params.get("issue_id", None) response_data = {} if project_id: for query_type in query_types: if query_type == "user_mention": fields = [ "member__first_name", "member__last_name", "member__display_name", ] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) users = ( ProjectMember.objects.filter( q, is_active=True, workspace__slug=slug, member__is_bot=False, project_id=project_id, ) .annotate( member__avatar_url=Case( When( member__avatar_asset__isnull=False, then=Concat( Value("/api/assets/v2/static/"), "member__avatar_asset", Value("/"), ), ), When( member__avatar_asset__isnull=True, then="member__avatar", ), default=Value(None), output_field=CharField(), ) ) .order_by("-created_at") ) if issue_id: issue_created_by = ( Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first() ) users = ( users.filter(Q(role__gt=10) | Q(member_id=issue_created_by)) .distinct() .values( "member__avatar_url", "member__display_name", "member__id", ) ) else: users = ( users.filter(Q(role__gt=10)) .distinct() .values( "member__avatar_url", "member__display_name", "member__id", ) ) response_data["user_mention"] = list(users[:count]) elif query_type == "project": fields = ["name", "identifier"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) projects = ( Project.objects.filter( q, Q(project_projectmember__member=self.request.user) | Q(network=2), workspace__slug=slug, ) .order_by("-created_at") .distinct() .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count] ) response_data["project"] = list(projects) elif query_type == "issue": fields = ["name", "sequence_id", "project__identifier"] q = Q() if query: for field in fields: if field == "sequence_id": sequences = re.findall(r"\b\d+\b", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) issues = ( Issue.issue_objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, project_id=project_id, ) .order_by("-created_at") .distinct() .values( "name", "id", "sequence_id", "project__identifier", "project_id", "priority", "state_id", "type_id", )[:count] ) response_data["issue"] = list(issues) elif query_type == "cycle": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) cycles = ( Cycle.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, project_id=project_id, ) .annotate( status=Case( When( Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), then=Value("CURRENT"), ), When( start_date__gt=timezone.now(), then=Value("UPCOMING"), ), When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), then=Value("DRAFT"), ), default=Value("DRAFT"), output_field=CharField(), ) ) .order_by("-created_at") .distinct() .values( "name", "id", "project_id", "project__identifier", "status", "workspace__slug", )[:count] ) response_data["cycle"] = list(cycles) elif query_type == "module": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) modules = ( Module.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, project_id=project_id, ) .order_by("-created_at") .distinct() .values( "name", "id", "project_id", "project__identifier", "status", "workspace__slug", )[:count] ) response_data["module"] = list(modules) elif query_type == "page": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) pages = ( Page.objects.filter( q, projects__project_projectmember__member=self.request.user, projects__project_projectmember__is_active=True, projects__id=project_id, workspace__slug=slug, access=0, ) .order_by("-created_at") .distinct() .values( "name", "id", "logo_props", "projects__id", "workspace__slug", )[:count] ) response_data["page"] = list(pages) return Response(response_data, status=status.HTTP_200_OK) else: for query_type in query_types: if query_type == "user_mention": fields = [ "member__first_name", "member__last_name", "member__display_name", ] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) users = ( WorkspaceMember.objects.filter( q, is_active=True, workspace__slug=slug, member__is_bot=False, ) .annotate( member__avatar_url=Case( When( member__avatar_asset__isnull=False, then=Concat( Value("/api/assets/v2/static/"), "member__avatar_asset", Value("/"), ), ), When( member__avatar_asset__isnull=True, then="member__avatar", ), default=Value(None), output_field=models.CharField(), ) ) .order_by("-created_at") .values("member__avatar_url", "member__display_name", "member__id")[:count] ) response_data["user_mention"] = list(users) elif query_type == "project": fields = ["name", "identifier"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) projects = ( Project.objects.filter( q, Q(project_projectmember__member=self.request.user) | Q(network=2), workspace__slug=slug, ) .order_by("-created_at") .distinct() .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count] ) response_data["project"] = list(projects) elif query_type == "issue": fields = ["name", "sequence_id", "project__identifier"] q = Q() if query: for field in fields: if field == "sequence_id": sequences = re.findall(r"\b\d+\b", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: q |= Q(**{f"{field}__icontains": query}) issues = ( Issue.issue_objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, ) .order_by("-created_at") .distinct() .values( "name", "id", "sequence_id", "project__identifier", "project_id", "priority", "state_id", "type_id", )[:count] ) response_data["issue"] = list(issues) elif query_type == "cycle": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) cycles = ( Cycle.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, ) .annotate( status=Case( When( Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), then=Value("CURRENT"), ), When( start_date__gt=timezone.now(), then=Value("UPCOMING"), ), When(end_date__lt=timezone.now(), then=Value("COMPLETED")), When( Q(start_date__isnull=True) & Q(end_date__isnull=True), then=Value("DRAFT"), ), default=Value("DRAFT"), output_field=CharField(), ) ) .order_by("-created_at") .distinct() .values( "name", "id", "project_id", "project__identifier", "status", "workspace__slug", )[:count] ) response_data["cycle"] = list(cycles) elif query_type == "module": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) modules = ( Module.objects.filter( q, project__project_projectmember__member=self.request.user, project__project_projectmember__is_active=True, workspace__slug=slug, ) .order_by("-created_at") .distinct() .values( "name", "id", "project_id", "project__identifier", "status", "workspace__slug", )[:count] ) response_data["module"] = list(modules) elif query_type == "page": fields = ["name"] q = Q() if query: for field in fields: q |= Q(**{f"{field}__icontains": query}) pages = ( Page.objects.filter( q, projects__project_projectmember__member=self.request.user, projects__project_projectmember__is_active=True, workspace__slug=slug, access=0, is_global=True, ) .order_by("-created_at") .distinct() .values( "name", "id", "logo_props", "projects__id", "workspace__slug", )[:count] ) response_data["page"] = list(pages) return Response(response_data, status=status.HTTP_200_OK)