Skip to content

Commit

Permalink
Merge pull request #176 from makeplane/preview
Browse files Browse the repository at this point in the history
release: v0.19.1
  • Loading branch information
sriramveeraghanta committed Apr 25, 2024
2 parents 5f8550b + 4180a50 commit e95aa81
Show file tree
Hide file tree
Showing 82 changed files with 1,424 additions and 1,045 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/create-sync-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:

jobs:
sync_changes:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
permissions:
pull-requests: write
contents: read
Expand Down
6 changes: 6 additions & 0 deletions apiserver/plane/api/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
IssueLinkAPIEndpoint,
IssueCommentAPIEndpoint,
IssueActivityAPIEndpoint,
WorkspaceIssueAPIEndpoint,
)

urlpatterns = [
path(
"workspaces/<str:slug>/issues/<str:project__identifier>-<str:issue__identifier>/",
WorkspaceIssueAPIEndpoint.as_view(),
name="issue-by-identifier",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueAPIEndpoint.as_view(),
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .state import StateAPIEndpoint

from .issue import (
WorkspaceIssueAPIEndpoint,
IssueAPIEndpoint,
LabelAPIEndpoint,
IssueLinkAPIEndpoint,
Expand Down
62 changes: 61 additions & 1 deletion apiserver/plane/api/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
LabelSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
Expand All @@ -51,6 +52,65 @@
from .base import BaseAPIView, WebhookMixin



class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset provides `retrieveByIssueId` on workspace level
"""

model = Issue
webhook_event = "issue"
permission_classes = [
ProjectEntityPermission
]
serializer_class = IssueSerializer


@property
def project__identifier(self):
return self.kwargs.get("project__identifier", None)

def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__identifier=self.kwargs.get("project__identifier"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()

def get(self, request, slug, project__identifier=None, issue__identifier=None):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)

class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
Expand Down Expand Up @@ -282,7 +342,7 @@ def patch(self, request, slug, project_id, pk=None):
)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
request.data.get("external_id")
and (issue.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
Expand Down
10 changes: 10 additions & 0 deletions apiserver/plane/app/permissions/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ def has_permission(self, request, view):
if request.user.is_anonymous:
return False

# Handle requests based on project__identifier
if hasattr(view, "project__identifier") and view.project__identifier:
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project__identifier=view.project__identifier,
is_active=True,
).exists()

## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
Expand Down
6 changes: 6 additions & 0 deletions apiserver/plane/app/urls/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from plane.app.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
SearchEndpoint,
)


Expand All @@ -18,4 +19,9 @@
IssueSearchEndpoint.as_view(),
name="project-issue-search",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/search/",
SearchEndpoint.as_view(),
name="search",
),
]
2 changes: 1 addition & 1 deletion apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@
SubPagesEndpoint,
)

from .search import GlobalSearchEndpoint, IssueSearchEndpoint
from .search import GlobalSearchEndpoint, IssueSearchEndpoint, SearchEndpoint


from .external.base import (
Expand Down
201 changes: 200 additions & 1 deletion apiserver/plane/app/views/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Module,
Page,
IssueView,
ProjectMember,
)
from plane.utils.issue_search import search_issues

Expand Down Expand Up @@ -249,7 +250,7 @@ def get(self, request, slug, project_id):
workspace__slug=slug,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True
project__archived_at__isnull=True,
)

if workspace_search == "false":
Expand Down Expand Up @@ -300,3 +301,201 @@ def get(self, request, slug, project_id):
),
status=status.HTTP_200_OK,
)


class SearchEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
query = request.query_params.get("query", False)
query_type = request.query_params.get("query_type", "issue")
count = int(request.query_params.get("count", 5))

if query_type == "mention":
fields = ["member__first_name", "member__last_name"]
q = Q()

if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})
users = (
ProjectMember.objects.filter(
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project_id=project_id,
workspace__slug=slug,
)
.order_by("-created_at")
.values(
"member__first_name",
"member__last_name",
"member__avatar",
"member__display_name",
"member__id",
)[:count]
)

fields = ["name"]
q = Q()

if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})

pages = (
Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
workspace__slug=slug,
access=0,
)
.order_by("-created_at")
.values("name", "id")[:count]
)
return Response(
{"users": users, "pages": pages}, status=status.HTTP_200_OK
)

if 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", "workspace__slug")[:count]
)
return Response(projects, status=status.HTTP_200_OK)

if query_type == "issue":
fields = ["name", "sequence_id", "project__identifier"]
q = Q()

if query:
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,
workspace__slug=slug,
project_id=project_id,
)
.order_by("-created_at")
.distinct()
.values(
"name",
"id",
"sequence_id",
"project__identifier",
"project_id",
"priority",
"state_id",
)[:count]
)
return Response(issues, status=status.HTTP_200_OK)

if 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,
)
.order_by("-created_at")
.distinct()
.values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)[:count]
)
return Response(cycles, status=status.HTTP_200_OK)

if 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",
"workspace__slug",
)[:count]
)
return Response(modules, status=status.HTTP_200_OK)

if query_type == "page":
fields = ["name"]
q = Q()

if query:
for field in fields:
q |= Q(**{f"{field}__icontains": query})

pages = (
Page.objects.filter(
q,
project__project_projectmember__member=self.request.user,
project__project_projectmember__is_active=True,
project_id=project_id,
workspace__slug=slug,
access=0,
)
.order_by("-created_at")
.distinct()
.values(
"name",
"id",
"project_id",
"project__identifier",
"workspace__slug",
)[:count]
)
return Response(pages, status=status.HTTP_200_OK)

return Response(
{"error": "Please provide a valid query"},
status=status.HTTP_400_BAD_REQUEST,
)
4 changes: 2 additions & 2 deletions apiserver/plane/app/views/workspace/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)

@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)

Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/app/views/workspace/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def get(self, request, slug):
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
is_triage=False,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
Loading

0 comments on commit e95aa81

Please sign in to comment.