From 3870ad667cbeec3938e7d6ee19d025e23b8782a8 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 26 Jul 2023 16:01:22 +0530 Subject: [PATCH 01/16] dev: profile page endpoints --- apiserver/plane/api/urls.py | 12 +++ apiserver/plane/api/views/__init__.py | 2 + apiserver/plane/api/views/workspace.py | 102 ++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 4280b1e592c..b9f177e5dfe 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -45,6 +45,8 @@ UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, + WorkspaceUserProfileEndpoint, + WorkspaceUserActivityEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -385,6 +387,16 @@ ), name="workspace-themes", ), + path( + "workspaces//user-profile//", + WorkspaceUserProfileEndpoint.as_view(), + name="workspace-user-profile", + ), + path( + "workspaces//user-activity//", + WorkspaceUserActivityEndpoint.as_view(), + name="workspace-user-profile", + ), ## End Workspaces ## # Projects path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 076cdd0069e..13e6a163c1e 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -42,6 +42,8 @@ UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, + WorkspaceUserProfileEndpoint, + WorkspaceUserActivityEndpoint, ) from .state import StateViewSet from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 305deb525f2..4a16bcd6a89 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -39,6 +39,7 @@ UserLiteSerializer, ProjectMemberSerializer, WorkspaceThemeSerializer, + IssueActivitySerializer, ) from plane.api.views.base import BaseAPIView from . import BaseViewSet @@ -61,7 +62,11 @@ Page, IssueViewFavorite, ) -from plane.api.permissions import WorkSpaceBasePermission, WorkSpaceAdminPermission +from plane.api.permissions import ( + WorkSpaceBasePermission, + WorkSpaceAdminPermission, + WorkspaceEntityPermission, +) from plane.bgtasks.workspace_invitation_task import workspace_invitation @@ -1009,3 +1014,98 @@ def create(self, request, slug): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class WorkspaceUserProfileEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + try: + state_distribution = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .annotate(state_group=F("state__group")) + .values("state_group") + .annotate(state_count=Count("state_group")) + .order_by("state_group") + ) + + created_issues = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + created_by_id=user_id, + ).count() + + assigned_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ).count() + + pending_issues_count = Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ).count() + + completed_issues_count = Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + ).count() + + return Response( + { + "state_distribution": state_distribution, + "created_issues": created_issues, + "assigned_issues": assigned_issues_count, + "completed_issues": completed_issues_count, + "pending_issues": pending_issues_count, + } + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class WorkspaceUserActivityEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + try: + queryset = IssueActivity.objects.filter( + workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, + ) + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda issue_activities: IssueActivitySerializer( + issue_activities, many=True + ).data, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class UserProfilePageProjectSegregationEndpoint(BaseAPIView): + + permission_classes = [ + WorkspaceEntityPermission, + ] \ No newline at end of file From c029dc1faf06f81ca2f483a620fbfd88b7e067fc Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 26 Jul 2023 16:02:49 +0530 Subject: [PATCH 02/16] dev: workspace projects endpoint --- apiserver/plane/api/views/workspace.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 4a16bcd6a89..1a25dad40af 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1035,6 +1035,8 @@ def get(self, request, slug, user_id): .order_by("state_group") ) + + created_issues = Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[user_id], @@ -1108,4 +1110,8 @@ class UserProfilePageProjectSegregationEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, - ] \ No newline at end of file + ] + + def get(self, request, slug, user_id): + try: + From 1a4725b0b00de1f55cc34022fcf996725e08c234 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 26 Jul 2023 17:37:33 +0530 Subject: [PATCH 03/16] dev: user profile page endpoints --- apiserver/plane/api/urls.py | 12 ++ apiserver/plane/api/views/__init__.py | 2 + apiserver/plane/api/views/issue.py | 2 +- apiserver/plane/api/views/workspace.py | 208 ++++++++++++++++++++++++- 4 files changed, 218 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b9f177e5dfe..8b57aaf812d 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -47,6 +47,8 @@ WorkspaceThemeViewSet, WorkspaceUserProfileEndpoint, WorkspaceUserActivityEndpoint, + UserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfileIssuesEndpoint, ## End Workspaces # File Assets FileAssetEndpoint, @@ -397,6 +399,16 @@ WorkspaceUserActivityEndpoint.as_view(), name="workspace-user-profile", ), + path( + "workspaces//user-projects-segregate//", + UserProfilePageProjectSegregationEndpoint.as_view(), + name="workspace-user-profile-page", + ), + path( + "workspaces//user-issues//", + WorkspaceUserProfileIssuesEndpoint.as_view(), + name="workspace-user-profile-page", + ), ## End Workspaces ## # Projects path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 13e6a163c1e..d184efb87e7 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -44,6 +44,8 @@ WorkspaceThemeViewSet, WorkspaceUserProfileEndpoint, WorkspaceUserActivityEndpoint, + UserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfileIssuesEndpoint, ) 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 fee6d3e694f..31e40612fac 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -262,7 +262,7 @@ def list(self, request, slug, project_id): return Response(issues, status=status.HTTP_200_OK) except Exception as e: - print(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/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 1a25dad40af..b7ef48d47a2 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -13,12 +13,18 @@ from django.core.validators import validate_email from django.contrib.sites.shortcuts import get_current_site from django.db.models import ( - CharField, - Count, + Prefetch, OuterRef, Func, F, Q, + Count, + Case, + Value, + CharField, + When, + Exists, + Max, ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField @@ -40,6 +46,7 @@ ProjectMemberSerializer, WorkspaceThemeSerializer, IssueActivitySerializer, + IssueLiteSerializer, ) from plane.api.views.base import BaseAPIView from . import BaseViewSet @@ -61,6 +68,8 @@ PageFavorite, Page, IssueViewFavorite, + IssueLink, + IssueAttachment, ) from plane.api.permissions import ( WorkSpaceBasePermission, @@ -68,7 +77,8 @@ WorkspaceEntityPermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation - +from plane.utils.issue_filters import issue_filters +from plane.utils.grouper import group_results class WorkSpaceViewSet(BaseViewSet): model = Workspace @@ -1035,7 +1045,15 @@ def get(self, request, slug, user_id): .order_by("state_group") ) - + priority_distribution = ( + Issue.objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .annotate(priority_count=Count("priority")) + .order_by("priority") + ) created_issues = Issue.issue_objects.filter( workspace__slug=slug, @@ -1071,6 +1089,7 @@ def get(self, request, slug, user_id): "assigned_issues": assigned_issues_count, "completed_issues": completed_issues_count, "pending_issues": pending_issues_count, + "priority_distribution": priority_distribution, } ) except Exception as e: @@ -1089,7 +1108,9 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: queryset = IssueActivity.objects.filter( - workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, + workspace__slug=slug, + project__project_projectmember__member=request.user, + actor=user_id, ) return self.paginate( request=request, @@ -1107,11 +1128,188 @@ def get(self, request, slug, user_id): class UserProfilePageProjectSegregationEndpoint(BaseAPIView): + permission_classes = [ + WorkspaceEntityPermission, + ] + + def get(self, request, slug, user_id): + try: + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + created_by_id=user_id, + project__project_projectmember__member=request.user, + ) + .values("project_id", "project__name", "project__identifier") + .annotate(issue_count=Count("project_id")) + .order_by("project_id") + ) + + assigned_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .values("project_id", "project__name", "project__identifier") + .annotate(issue_count=Count("project_id")) + .order_by("project_id") + ) + + completed_issues = ( + Issue.objects.filter( + workspace__slug=slug, + project__project_projectmember__member=request.user, + assignees__in=[user_id], + state__group="completed", + ) + .values("project_id", "project__name", "project__identifier") + .annotate(issue_count=Count("project_id")) + .order_by("project_id") + ) + + pending_issues = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .values("project_id", "project__name", "project__identifier") + .annotate(issue_count=Count("project_id")) + .order_by("project_id") + ) + return Response( + { + "created_issues": created_issues, + "assigned_issues": assigned_issues, + "completed_issues": completed_issues, + "pending_issues": pending_issues, + }, + 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, + ) + + +class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, ] def get(self, request, slug, user_id): try: + filters = issue_filters(request.query_params, "GET") + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = ( + Issue.issue_objects.filter( + Q(assignees__in=[user_id]) | Q(created_by_id=user_id), + workspace__slug=slug, + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .order_by("-created_at") + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) + + # 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( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) From 92e66207cd84da58f77c13890539d3c32d192067 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 11:37:09 +0530 Subject: [PATCH 04/16] dev: profile page endpoints --- apiserver/plane/api/views/workspace.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index b7ef48d47a2..325516f54d2 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -70,6 +70,7 @@ IssueViewFavorite, IssueLink, IssueAttachment, + IssueSubscriber, ) from plane.api.permissions import ( WorkSpaceBasePermission, @@ -1082,6 +1083,12 @@ def get(self, request, slug, user_id): project__project_projectmember__member=request.user, ).count() + subscribed_issues_count = IssueSubscriber.objects.filter( + workspace__slug=slug, + subscriber_id=user_id, + project__project_projectmember__member=request.user, + ).count() + return Response( { "state_distribution": state_distribution, @@ -1090,6 +1097,7 @@ def get(self, request, slug, user_id): "completed_issues": completed_issues_count, "pending_issues": pending_issues_count, "priority_distribution": priority_distribution, + "subscribed_issues": subscribed_issues_count } ) except Exception as e: From d227fcdc2c95fb8913f6d715c5231ef328b096b2 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 12:10:14 +0530 Subject: [PATCH 05/16] dev: project filters --- apiserver/plane/api/views/workspace.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 325516f54d2..3e395da9592 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1034,12 +1034,15 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: + filters = issue_filters(request.query_params, "GET") + state_distribution = ( Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, ) + .filter(**filters) .annotate(state_group=F("state__group")) .values("state_group") .annotate(state_count=Count("state_group")) @@ -1052,6 +1055,7 @@ def get(self, request, slug, user_id): assignees__in=[user_id], project__project_projectmember__member=request.user, ) + .filter(**filters) .annotate(priority_count=Count("priority")) .order_by("priority") ) @@ -1061,42 +1065,42 @@ def get(self, request, slug, user_id): assignees__in=[user_id], project__project_projectmember__member=request.user, created_by_id=user_id, - ).count() + ).filter(**filters).count() assigned_issues_count = Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, - ).count() + ).filter(**filters).count() pending_issues_count = Issue.issue_objects.filter( ~Q(state__group__in=["completed", "cancelled"]), workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, - ).count() + ).filter(**filters).count() completed_issues_count = Issue.issue_objects.filter( workspace__slug=slug, assignees__in=[user_id], state__group="completed", project__project_projectmember__member=request.user, - ).count() + ).filter(**filters).count() subscribed_issues_count = IssueSubscriber.objects.filter( workspace__slug=slug, subscriber_id=user_id, project__project_projectmember__member=request.user, - ).count() + ).filter(**filters).count() return Response( { "state_distribution": state_distribution, + "priority_distribution": priority_distribution, "created_issues": created_issues, "assigned_issues": assigned_issues_count, "completed_issues": completed_issues_count, "pending_issues": pending_issues_count, - "priority_distribution": priority_distribution, "subscribed_issues": subscribed_issues_count } ) From ab355eb8f7a06d621bc7be2460cf349c5acf886a Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 12:46:30 +0530 Subject: [PATCH 06/16] dev: fix priority distribution --- apiserver/plane/api/views/workspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 3e395da9592..e474bd945ef 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1056,6 +1056,7 @@ def get(self, request, slug, user_id): project__project_projectmember__member=request.user, ) .filter(**filters) + .values("priority") .annotate(priority_count=Count("priority")) .order_by("priority") ) From cff83fe3964980524c1f8ca0b37660c7292a819c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 13:59:54 +0530 Subject: [PATCH 07/16] dev: issue subscriptions --- apiserver/plane/api/views/workspace.py | 2 +- apiserver/plane/utils/issue_filters.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index e474bd945ef..742cff09084 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1221,7 +1221,7 @@ def get(self, request, slug, user_id): order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( Issue.issue_objects.filter( - Q(assignees__in=[user_id]) | Q(created_by_id=user_id), + Q(assignees__in=[user_id]) | Q(created_by_id=user_id) | Q(issue_subscribers__subscriber_id=user_id), workspace__slug=slug, project__project_projectmember__member=request.user, diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 6a9e8b8e868..f28f5496f8f 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -268,6 +268,17 @@ def filter_sub_issue_toggle(params, filter, method): return filter +def filter_subscribed_issues(params, filter, method): + if method == "GET": + subscribers = params.getlist("subscriber") + if len(subscribers) and "" not in subscribers: + filter["issue_subscribers__subscriber_id__in"] = subscribers + else: + if params.get("subscriber", None) and len(params.get("subscriber")): + filter["issue_subscribers__subscriber_id__in"] = params.get("subscriber") + return filter + + def issue_filters(query_params, method): filter = dict() @@ -291,6 +302,7 @@ def issue_filters(query_params, method): "module": filter_module, "inbox_status": filter_inbox_status, "sub_issue": filter_sub_issue_toggle, + "subscriber": filter_subscribed_issues, } for key, value in ISSUE_FILTER.items(): From 346b384e14731554a6489c32ef024bc8dfc3a5cf Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 16:15:06 +0530 Subject: [PATCH 08/16] dev: issue priority distribution and issue activity api optimization --- apiserver/plane/api/views/workspace.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 742cff09084..607ce8e5ce4 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -25,6 +25,7 @@ When, Exists, Max, + IntegerField, ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField @@ -1049,6 +1050,8 @@ def get(self, request, slug, user_id): .order_by("state_group") ) + priority_order = ["urgent", "high", "medium", "low", None] + priority_distribution = ( Issue.objects.filter( workspace__slug=slug, @@ -1058,7 +1061,14 @@ def get(self, request, slug, user_id): .filter(**filters) .values("priority") .annotate(priority_count=Count("priority")) - .order_by("priority") + .annotate( + priority_order=Case( + *[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)], + default=Value(len(priority_order)), + output_field=IntegerField(), + ) + ) + .order_by("priority_order") ) created_issues = Issue.issue_objects.filter( @@ -1124,7 +1134,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, - ) + ).select_related("actor", "workspace") return self.paginate( request=request, queryset=queryset, From 4f36b17670af365f2af6a5920d4d89437a4e5fb5 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 16:58:11 +0530 Subject: [PATCH 09/16] dev: user data in profile endpoints --- apiserver/plane/api/urls.py | 4 +- apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/workspace.py | 103 +++++++++++++++++-------- apiserver/plane/db/models/user.py | 1 + 4 files changed, 73 insertions(+), 37 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 8b57aaf812d..6dbdcc57564 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -47,7 +47,7 @@ WorkspaceThemeViewSet, WorkspaceUserProfileEndpoint, WorkspaceUserActivityEndpoint, - UserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfilePageProjectSegregationEndpoint, WorkspaceUserProfileIssuesEndpoint, ## End Workspaces # File Assets @@ -401,7 +401,7 @@ ), path( "workspaces//user-projects-segregate//", - UserProfilePageProjectSegregationEndpoint.as_view(), + WorkspaceUserProfilePageProjectSegregationEndpoint.as_view(), name="workspace-user-profile-page", ), path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index d184efb87e7..0d6438f2637 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -44,7 +44,7 @@ WorkspaceThemeViewSet, WorkspaceUserProfileEndpoint, WorkspaceUserActivityEndpoint, - UserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfilePageProjectSegregationEndpoint, WorkspaceUserProfileIssuesEndpoint, ) from .state import StateViewSet diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 607ce8e5ce4..d5da744d40f 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -23,7 +23,6 @@ Value, CharField, When, - Exists, Max, IntegerField, ) @@ -82,6 +81,7 @@ from plane.utils.issue_filters import issue_filters from plane.utils.grouper import group_results + class WorkSpaceViewSet(BaseViewSet): model = Workspace serializer_class = WorkSpaceSerializer @@ -1063,7 +1063,10 @@ def get(self, request, slug, user_id): .annotate(priority_count=Count("priority")) .annotate( priority_order=Case( - *[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)], + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], default=Value(len(priority_order)), output_field=IntegerField(), ) @@ -1071,38 +1074,58 @@ def get(self, request, slug, user_id): .order_by("priority_order") ) - created_issues = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - created_by_id=user_id, - ).filter(**filters).count() + created_issues = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + created_by_id=user_id, + ) + .filter(**filters) + .count() + ) - assigned_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - ).filter(**filters).count() + assigned_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .count() + ) - pending_issues_count = Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, - ).filter(**filters).count() + pending_issues_count = ( + Issue.issue_objects.filter( + ~Q(state__group__in=["completed", "cancelled"]), + workspace__slug=slug, + assignees__in=[user_id], + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .count() + ) - completed_issues_count = Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - state__group="completed", - project__project_projectmember__member=request.user, - ).filter(**filters).count() + completed_issues_count = ( + Issue.issue_objects.filter( + workspace__slug=slug, + assignees__in=[user_id], + state__group="completed", + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .count() + ) - subscribed_issues_count = IssueSubscriber.objects.filter( - workspace__slug=slug, - subscriber_id=user_id, - project__project_projectmember__member=request.user, - ).filter(**filters).count() + subscribed_issues_count = ( + IssueSubscriber.objects.filter( + workspace__slug=slug, + subscriber_id=user_id, + project__project_projectmember__member=request.user, + ) + .filter(**filters) + .count() + ) return Response( { @@ -1112,7 +1135,7 @@ def get(self, request, slug, user_id): "assigned_issues": assigned_issues_count, "completed_issues": completed_issues_count, "pending_issues": pending_issues_count, - "subscribed_issues": subscribed_issues_count + "subscribed_issues": subscribed_issues_count, } ) except Exception as e: @@ -1150,13 +1173,23 @@ def get(self, request, slug, user_id): ) -class UserProfilePageProjectSegregationEndpoint(BaseAPIView): +class WorkspaceUserProfilePageProjectSegregationEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, ] def get(self, request, slug, user_id): try: + user_data = User.objects.get(pk=user_id).values( + "email", + "first_name", + "last_name", + "avatar", + "cover_image", + "date_joined", + "user_timezone", + ) + created_issues = ( Issue.issue_objects.filter( workspace__slug=slug, @@ -1209,6 +1242,7 @@ def get(self, request, slug, user_id): "assigned_issues": assigned_issues, "completed_issues": completed_issues, "pending_issues": pending_issues, + "user_data": user_data, }, status=status.HTTP_200_OK, ) @@ -1231,10 +1265,11 @@ def get(self, request, slug, user_id): order_by_param = request.GET.get("order_by", "-created_at") issue_queryset = ( Issue.issue_objects.filter( - Q(assignees__in=[user_id]) | Q(created_by_id=user_id) | Q(issue_subscribers__subscriber_id=user_id), + Q(assignees__in=[user_id]) + | Q(created_by_id=user_id) + | Q(issue_subscribers__subscriber_id=user_id), workspace__slug=slug, project__project_projectmember__member=request.user, - ) .filter(**filters) .annotate( diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 36b3a1f6b04..0b643271e64 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -38,6 +38,7 @@ class User(AbstractBaseUser, PermissionsMixin): first_name = models.CharField(max_length=255, blank=True) last_name = models.CharField(max_length=255, blank=True) avatar = models.CharField(max_length=255, blank=True) + cover_image = models.URLField(blank=True, null=True, max_length=800) # tracking metrics date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Created At") From b09544e56a1166e791478ad232ef738604809d1d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 17:02:15 +0530 Subject: [PATCH 10/16] dev: profile page data --- apiserver/plane/api/views/workspace.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index d5da744d40f..b9754bddada 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1180,15 +1180,7 @@ class WorkspaceUserProfilePageProjectSegregationEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: - user_data = User.objects.get(pk=user_id).values( - "email", - "first_name", - "last_name", - "avatar", - "cover_image", - "date_joined", - "user_timezone", - ) + user_data = User.objects.get(pk=user_id) created_issues = ( Issue.issue_objects.filter( @@ -1242,7 +1234,15 @@ def get(self, request, slug, user_id): "assigned_issues": assigned_issues, "completed_issues": completed_issues, "pending_issues": pending_issues, - "user_data": user_data, + "user_data": { + "email": user_data.email, + "first_name": user_data.first_name, + "last_name": user_data.last_name, + "avatar": user_data.avatar, + "cover_image": user_data.cover_image, + "date_joined": user_data.date_joined, + "user_timezone": user_data.user_timezone, + }, }, status=status.HTTP_200_OK, ) From 459a9f2117055714883206075a3b1182792856cb Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 20:31:42 +0530 Subject: [PATCH 11/16] dev: project list endpoint --- apiserver/plane/api/views/workspace.py | 87 +++++++++++++------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index b9754bddada..b5b0c6b9fda 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -71,6 +71,7 @@ IssueLink, IssueAttachment, IssueSubscriber, + Project, ) from plane.api.permissions import ( WorkSpaceBasePermission, @@ -1182,58 +1183,56 @@ def get(self, request, slug, user_id): try: user_data = User.objects.get(pk=user_id) - created_issues = ( - Issue.issue_objects.filter( + projects = ( + Project.objects.filter( workspace__slug=slug, - created_by_id=user_id, - project__project_projectmember__member=request.user, + project_projectmember__member=request.user, ) - .values("project_id", "project__name", "project__identifier") - .annotate(issue_count=Count("project_id")) - .order_by("project_id") - ) - - assigned_issues = ( - Issue.issue_objects.filter( - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, + .annotate( + created_issues=Count( + "project_issue", filter=Q(project_issue__created_by_id=user_id) + ) ) - .values("project_id", "project__name", "project__identifier") - .annotate(issue_count=Count("project_id")) - .order_by("project_id") - ) - - completed_issues = ( - Issue.objects.filter( - workspace__slug=slug, - project__project_projectmember__member=request.user, - assignees__in=[user_id], - state__group="completed", + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q(project_issue__assignees__in=[user_id]), + ) ) - .values("project_id", "project__name", "project__identifier") - .annotate(issue_count=Count("project_id")) - .order_by("project_id") - ) - - pending_issues = ( - Issue.issue_objects.filter( - ~Q(state__group__in=["completed", "cancelled"]), - workspace__slug=slug, - assignees__in=[user_id], - project__project_projectmember__member=request.user, + .annotate( + completed_issues=Count( + "project_issue", + filter=Q(project_issue__completed_at__isnull=False), + ) + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ] + ), + ) + ) + .values( + "id", + "name", + "identifier", + "cover_image", + "icon_prop", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", ) - .values("project_id", "project__name", "project__identifier") - .annotate(issue_count=Count("project_id")) - .order_by("project_id") ) return Response( { - "created_issues": created_issues, - "assigned_issues": assigned_issues, - "completed_issues": completed_issues, - "pending_issues": pending_issues, + "project_data": projects, "user_data": { "email": user_data.email, "first_name": user_data.first_name, @@ -1247,7 +1246,7 @@ def get(self, request, slug, user_id): status=status.HTTP_200_OK, ) except Exception as e: - capture_exception(e) + print(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, From 6408732661266ace0af176bfea71c1490ad77138 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 21:26:03 +0530 Subject: [PATCH 12/16] dev: project emojis --- apiserver/plane/api/views/workspace.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index b5b0c6b9fda..876609bd7d3 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1202,7 +1202,10 @@ def get(self, request, slug, user_id): .annotate( completed_issues=Count( "project_issue", - filter=Q(project_issue__completed_at__isnull=False), + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + ), ) ) .annotate( @@ -1213,7 +1216,8 @@ def get(self, request, slug, user_id): "backlog", "unstarted", "started", - ] + ], + project_issue__assignees__in=[user_id], ), ) ) @@ -1221,7 +1225,7 @@ def get(self, request, slug, user_id): "id", "name", "identifier", - "cover_image", + "emoji", "icon_prop", "created_issues", "assigned_issues", From 3a23673c6df96b487162d9849507eea04096dc26 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 21:41:42 +0530 Subject: [PATCH 13/16] dev: capture exception --- apiserver/plane/api/views/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 876609bd7d3..465324b299d 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1250,7 +1250,7 @@ def get(self, request, slug, user_id): status=status.HTTP_200_OK, ) except Exception as e: - print(e) + capture_exception(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, From 16cded151825fe16b6b21b74f9235d47077af741 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 21:45:17 +0530 Subject: [PATCH 14/16] dev: update workspace user profile urls --- apiserver/plane/api/urls.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 6dbdcc57564..cfe048e641f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -390,17 +390,17 @@ name="workspace-themes", ), path( - "workspaces//user-profile//", + "workspaces//user-stats//", WorkspaceUserProfileEndpoint.as_view(), - name="workspace-user-profile", + name="workspace-user-stats", ), path( "workspaces//user-activity//", WorkspaceUserActivityEndpoint.as_view(), - name="workspace-user-profile", + name="workspace-user-activity", ), path( - "workspaces//user-projects-segregate//", + " //", WorkspaceUserProfilePageProjectSegregationEndpoint.as_view(), name="workspace-user-profile-page", ), From 252873f972781dae00fc4f1f06282645fb49911b Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 27 Jul 2023 21:48:50 +0530 Subject: [PATCH 15/16] dev: user profile endpoints rename and activity filter --- apiserver/plane/api/urls.py | 12 ++++++------ apiserver/plane/api/views/__init__.py | 4 ++-- apiserver/plane/api/views/workspace.py | 11 +++++++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index cfe048e641f..393020b8d76 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -45,9 +45,9 @@ UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, - WorkspaceUserProfileEndpoint, + WorkspaceUserProfileStatsEndpoint, WorkspaceUserActivityEndpoint, - WorkspaceUserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, ## End Workspaces # File Assets @@ -391,7 +391,7 @@ ), path( "workspaces//user-stats//", - WorkspaceUserProfileEndpoint.as_view(), + WorkspaceUserProfileStatsEndpoint.as_view(), name="workspace-user-stats", ), path( @@ -400,14 +400,14 @@ name="workspace-user-activity", ), path( - " //", - WorkspaceUserProfilePageProjectSegregationEndpoint.as_view(), + "workspaces//user-profile//", + WorkspaceUserProfileEndpoint.as_view(), name="workspace-user-profile-page", ), path( "workspaces//user-issues//", WorkspaceUserProfileIssuesEndpoint.as_view(), - name="workspace-user-profile-page", + name="workspace-user-profile-issues", ), ## End Workspaces ## # Projects diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 0d6438f2637..4a6d4518bf3 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -42,9 +42,9 @@ UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, WorkspaceThemeViewSet, - WorkspaceUserProfileEndpoint, + WorkspaceUserProfileStatsEndpoint, WorkspaceUserActivityEndpoint, - WorkspaceUserProfilePageProjectSegregationEndpoint, + WorkspaceUserProfileEndpoint, WorkspaceUserProfileIssuesEndpoint, ) from .state import StateViewSet diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 465324b299d..803a2bdc8c3 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -1029,7 +1029,7 @@ def create(self, request, slug): ) -class WorkspaceUserProfileEndpoint(BaseAPIView): +class WorkspaceUserProfileStatsEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, ] @@ -1154,11 +1154,18 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): def get(self, request, slug, user_id): try: + + projects = request.query_params.getlist("project", []) + queryset = IssueActivity.objects.filter( workspace__slug=slug, project__project_projectmember__member=request.user, actor=user_id, ).select_related("actor", "workspace") + + if projects: + queryset = queryset.filter(project__in=projects) + return self.paginate( request=request, queryset=queryset, @@ -1174,7 +1181,7 @@ def get(self, request, slug, user_id): ) -class WorkspaceUserProfilePageProjectSegregationEndpoint(BaseAPIView): +class WorkspaceUserProfileEndpoint(BaseAPIView): permission_classes = [ WorkspaceEntityPermission, ] From 5df50404ae0fb49b0b37e9d6d494c4f8255c1ebc Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 28 Jul 2023 13:06:22 +0530 Subject: [PATCH 16/16] dev: fix subscriber issues filtering --- apiserver/plane/utils/issue_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index f28f5496f8f..abe71731d0b 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -270,7 +270,7 @@ def filter_sub_issue_toggle(params, filter, method): def filter_subscribed_issues(params, filter, method): if method == "GET": - subscribers = params.getlist("subscriber") + subscribers = params.get("subscriber").split(",") if len(subscribers) and "" not in subscribers: filter["issue_subscribers__subscriber_id__in"] = subscribers else: