From fd9dcfa2ec5f0d6f293a1d9d867fe4a808db0ee9 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:52:35 +0530 Subject: [PATCH] feat: my issues filtering (#1666) * feat: my issues filtering * dev: migrations * dev: remove state list endpoint * dev: state group filtering --- apiserver/plane/api/urls.py | 6 + apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/issue.py | 103 +++++++++++++++--- apiserver/plane/api/views/workspace.py | 29 ++++- .../db/migrations/0039_auto_20230723_2203.py | 69 ++++++++++-- apiserver/plane/db/models/workspace.py | 27 ++++- apiserver/plane/utils/issue_filters.py | 15 +++ 7 files changed, 222 insertions(+), 28 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 4280b1e59..3ed9af102 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -45,6 +45,7 @@ from plane.api.views import ( UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, + WorkspaceLabelsEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -385,6 +386,11 @@ urlpatterns = [ ), name="workspace-themes", ), + path( + "workspaces//labels/", + WorkspaceLabelsEndpoint.as_view(), + name="workspace-labels", + ), ## End Workspaces ## # Projects path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 076cdd006..0c8af93cf 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -42,6 +42,7 @@ from .workspace import ( UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, + WorkspaceLabelsEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index fee6d3e69..32bc26071 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -48,6 +48,7 @@ from plane.api.serializers import ( ProjectMemberLiteSerializer, ) from plane.api.permissions import ( + WorkspaceEntityPermission, ProjectEntityPermission, WorkSpaceAdminPermission, ProjectMemberPermission, @@ -157,7 +158,7 @@ class IssueViewSet(BaseViewSet): def list(self, request, slug, project_id): try: filters = issue_filters(request.query_params, "GET") - show_sub_issues = request.GET.get("show_sub_issues", "true") + print(filters) # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", None] @@ -244,12 +245,6 @@ class IssueViewSet(BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issue_queryset = ( - issue_queryset - if show_sub_issues == "true" - else issue_queryset.filter(parent__isnull=True) - ) - issues = IssueLiteSerializer(issue_queryset, many=True).data ## Grouping the results @@ -317,9 +312,17 @@ class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): try: - issues = ( + filters = issue_filters(request.query_params, "GET") + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", None] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( Issue.issue_objects.filter( - assignees__in=[request.user], workspace__slug=slug + (Q(assignees__in=[request.user]) | Q(created_by=request.user)), + workspace__slug=slug, ) .annotate( sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) @@ -333,7 +336,7 @@ class UserWorkSpaceIssues(BaseAPIView): .select_related("parent") .prefetch_related("assignees") .prefetch_related("labels") - .order_by("-created_at") + .order_by(order_by_param) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -348,9 +351,77 @@ class UserWorkSpaceIssues(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .filter(**filters) ) - serializer = IssueLiteSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" if order_by_param.startswith("-") else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + issues = IssueLiteSerializer(issue_queryset, many=True).data + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + return Response( + group_results(issues, group_by), status=status.HTTP_200_OK + ) + + return Response(issues, status=status.HTTP_200_OK) except Exception as e: capture_exception(e) return Response( @@ -635,9 +706,7 @@ class SubIssuesEndpoint(BaseAPIView): def get(self, request, slug, project_id, issue_id): try: sub_issues = ( - Issue.issue_objects.filter( - parent_id=issue_id, workspace__slug=slug - ) + Issue.issue_objects.filter(parent_id=issue_id, workspace__slug=slug) .select_related("project") .select_related("workspace") .select_related("state") @@ -667,9 +736,7 @@ class SubIssuesEndpoint(BaseAPIView): ) state_distribution = ( - State.objects.filter( - ~Q(name="Triage"), workspace__slug=slug - ) + State.objects.filter(~Q(name="Triage"), workspace__slug=slug) .annotate( state_count=Count( "state_issue", diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 305deb525..91fc4f7b9 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -60,8 +60,14 @@ from plane.db.models import ( PageFavorite, Page, IssueViewFavorite, + Label, + State, +) +from plane.api.permissions import ( + WorkSpaceBasePermission, + WorkSpaceAdminPermission, + WorkspaceEntityPermission, ) -from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission from plane.bgtasks.workspace_invitation_task import workspace_invitation @@ -1009,3 +1015,24 @@ class WorkspaceThemeViewSet(BaseViewSet): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class WorkspaceLabelsEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug): + try: + labels = Label.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + ).values("parent", "name", "color", "id", "project_id", "workspace__slug") + return Response(labels, status=status.HTTP_200_OK) + 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/migrations/0039_auto_20230723_2203.py b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py index e6dc81306..45cbedf7a 100644 --- a/apiserver/plane/db/migrations/0039_auto_20230723_2203.py +++ b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py @@ -1,6 +1,8 @@ # Generated by Django 4.2.3 on 2023-07-23 16:33 -from django.db import migrations +from django.db import migrations, models +import plane.db.models.workspace + def rename_field(apps, schema_editor): Model = apps.get_model("db", "IssueActivity") @@ -9,18 +11,69 @@ def rename_field(apps, schema_editor): obj.field = "assignees" updated_activity.append(obj) - Model.objects.bulk_update( - updated_activity, ["field"], batch_size=100 - ) - + Model.objects.bulk_update(updated_activity, ["field"], batch_size=100) + + +def update_workspace_member_props(apps, schema_editor): + Model = apps.get_model("db", "WorkspaceMember") + + updated_workspace_member = [] + + for obj in Model.objects.all(): + if obj.view_props is None: + obj.view_props = { + "filters": {"type": None}, + "groupByProperty": None, + "issueView": "list", + "orderBy": "-created_at", + "properties": { + "assignee": True, + "due_date": True, + "key": True, + "labels": True, + "priority": True, + "state": True, + "sub_issue_count": True, + "attachment_count": True, + "link": True, + "estimate": True, + "created_on": True, + "updated_on": True, + }, + "showEmptyGroups": True, + } + else: + current_view_props = obj.view_props + obj.view_props = { + "filters": {"type": None}, + "groupByProperty": None, + "issueView": "list", + "orderBy": "-created_at", + "showEmptyGroups": True, + "properties": current_view_props, + } + + updated_workspace_member.append(obj) + + Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100) class Migration(migrations.Migration): - dependencies = [ - ('db', '0038_auto_20230720_1505'), + ("db", "0038_auto_20230720_1505"), ] operations = [ - migrations.RunPython(rename_field) + migrations.RunPython(rename_field), + migrations.RunPython(update_workspace_member_props), + migrations.AlterField( + model_name='workspacemember', + name='view_props', + field=models.JSONField(default=plane.db.models.workspace.get_default_props), + ), + migrations.AddField( + model_name='workspacemember', + name='default_props', + field=models.JSONField(default=plane.db.models.workspace.get_default_props), + ), ] diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index 9b9fbb68c..09db42002 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -14,6 +14,30 @@ ROLE_CHOICES = ( ) +def get_default_props(): + return { + "filters": {"type": None}, + "groupByProperty": None, + "issueView": "list", + "orderBy": "-created_at", + "properties": { + "assignee": True, + "due_date": True, + "key": True, + "labels": True, + "priority": True, + "state": True, + "sub_issue_count": True, + "attachment_count": True, + "link": True, + "estimate": True, + "created_on": True, + "updated_on": True, + }, + "showEmptyGroups": True, + } + + class Workspace(BaseModel): name = models.CharField(max_length=80, verbose_name="Workspace Name") logo = models.URLField(verbose_name="Logo", blank=True, null=True) @@ -47,7 +71,8 @@ class WorkspaceMember(BaseModel): ) role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, default=10) company_role = models.TextField(null=True, blank=True) - view_props = models.JSONField(null=True, blank=True) + view_props = models.JSONField(default=get_default_props) + default_props = models.JSONField(default=get_default_props) class Meta: unique_together = ["workspace", "member"] diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 6a9e8b8e8..e37bc94f4 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -12,6 +12,18 @@ def filter_state(params, filter, method): return filter +def filter_state_group(params, filter, method): + if method == "GET": + state_group = params.get("state_group").split(",") + if len(state_group) and "" not in state_group: + filter["state__group__in"] = state_group + else: + if params.get("state_group", None) and len(params.get("state_group")): + filter["state__group__in"] = params.get("state_group") + return filter + + + def filter_estimate_point(params, filter, method): if method == "GET": estimate_points = params.get("estimate_point").split(",") @@ -212,6 +224,7 @@ def filter_issue_state_type(params, filter, method): return filter + def filter_project(params, filter, method): if method == "GET": projects = params.get("project").split(",") @@ -270,9 +283,11 @@ def filter_sub_issue_toggle(params, filter, method): def issue_filters(query_params, method): filter = dict() + print(query_params) ISSUE_FILTER = { "state": filter_state, + "state_group": filter_state_group, "estimate_point": filter_estimate_point, "priority": filter_priority, "parent": filter_parent,