From 9b0949148f47b5fbfdb40ef0b0ba145eea98c395 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 8 Apr 2024 18:46:05 +0530 Subject: [PATCH] [WEB - 883] fix: external apis (#3975) * fix: external apis * dev: remove descrypt in email password * dev: add email as field in user serializer * dev: fix linting errors * dev: push commit to enable build triggers * fix: formatting errors * dev: remove instance value extraction --- apiserver/plane/api/serializers/base.py | 8 +- apiserver/plane/api/serializers/project.py | 1 + apiserver/plane/api/serializers/user.py | 1 + apiserver/plane/api/views/cycle.py | 249 +++++++++++++++++++-- apiserver/plane/api/views/inbox.py | 17 +- apiserver/plane/api/views/module.py | 27 +-- apiserver/plane/api/views/state.py | 15 +- apiserver/plane/app/views/cycle/base.py | 2 +- apiserver/plane/app/views/issue/base.py | 74 +++--- apiserver/plane/app/views/issue/draft.py | 1 - apiserver/plane/utils/analytics_plot.py | 25 ++- 11 files changed, 327 insertions(+), 93 deletions(-) diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/api/serializers/base.py index da8b96964..5b68a7113 100644 --- a/apiserver/plane/api/serializers/base.py +++ b/apiserver/plane/api/serializers/base.py @@ -66,11 +66,11 @@ class BaseSerializer(serializers.ModelSerializer): if expand in self.fields: # Import all the expandable serializers from . import ( - WorkspaceLiteSerializer, - ProjectLiteSerializer, - UserLiteSerializer, - StateLiteSerializer, IssueSerializer, + ProjectLiteSerializer, + StateLiteSerializer, + UserLiteSerializer, + WorkspaceLiteSerializer, ) # Expansion mapper diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 9dd4c9b85..ce354ba5f 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -7,6 +7,7 @@ from plane.db.models import ( ProjectIdentifier, WorkspaceMember, ) + from .base import BaseSerializer diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index ea50440e0..e853b90c2 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -11,6 +11,7 @@ class UserLiteSerializer(BaseSerializer): "id", "first_name", "last_name", + "email", "avatar", "display_name", "email", diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 637d713c3..c2155c15e 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -2,29 +2,31 @@ import json # Django imports -from django.db.models import Q, Count, Sum, F, OuterRef, Func -from django.utils import timezone from django.core import serializers +from django.db.models import Count, F, Func, OuterRef, Q, Sum +from django.utils import timezone # Third party imports -from rest_framework.response import Response from rest_framework import status +from rest_framework.response import Response # Module imports -from .base import BaseAPIView, WebhookMixin -from plane.db.models import ( - Cycle, - Issue, - CycleIssue, - IssueLink, - IssueAttachment, +from plane.api.serializers import ( + CycleIssueSerializer, + CycleSerializer, ) from plane.app.permissions import ProjectEntityPermission -from plane.api.serializers import ( - CycleSerializer, - CycleIssueSerializer, -) from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + Cycle, + CycleIssue, + Issue, + IssueAttachment, + IssueLink, +) +from plane.utils.analytics_plot import burndown_plot + +from .base import BaseAPIView, WebhookMixin class CycleAPIEndpoint(WebhookMixin, BaseAPIView): @@ -551,7 +553,21 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): .distinct() ) - def get(self, request, slug, project_id, cycle_id): + def get(self, request, slug, project_id, cycle_id, issue_id=None): + # Get + if issue_id: + cycle_issue = CycleIssue.objects.get( + workspace__slug=slug, + project_id=project_id, + cycle_id=cycle_id, + issue_id=issue_id, + ) + serializer = CycleIssueSerializer( + cycle_issue, fields=self.fields, expand=self.expand + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + # List order_by = request.GET.get("order_by", "created_at") issues = ( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) @@ -748,6 +764,209 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): workspace__slug=slug, project_id=project_id, pk=new_cycle_id ) + old_cycle = ( + Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + completed_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="completed", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + cancelled_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="cancelled", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + started_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="started", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + unstarted_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="unstarted", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + .annotate( + backlog_issues=Count( + "issue_cycle__issue__state__group", + filter=Q( + issue_cycle__issue__state__group="backlog", + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + ) + + # Pass the new_cycle queryset to burndown_plot + completion_chart = burndown_plot( + queryset=old_cycle.first(), + slug=slug, + project_id=project_id, + cycle_id=cycle_id, + ) + + # Get the assignee distribution + assignee_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_issues=Count( + "id", + filter=Q(archived_at__isnull=True, is_draft=False), + ), + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") + ) + # assignee distribution serialized + assignee_distribution_data = [ + { + "display_name": item["display_name"], + "assignee_id": ( + str(item["assignee_id"]) if item["assignee_id"] else None + ), + "avatar": item["avatar"], + "total_issues": item["total_issues"], + "completed_issues": item["completed_issues"], + "pending_issues": item["pending_issues"], + } + for item in assignee_distribution + ] + + # Get the label distribution + label_distribution = ( + Issue.objects.filter( + issue_cycle__cycle_id=cycle_id, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_issues=Count( + "id", + filter=Q(archived_at__isnull=True, is_draft=False), + ) + ) + .annotate( + completed_issues=Count( + "id", + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_issues=Count( + "id", + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + + # Label distribution serilization + label_distribution_data = [ + { + "label_name": item["label_name"], + "color": item["color"], + "label_id": ( + str(item["label_id"]) if item["label_id"] else None + ), + "total_issues": item["total_issues"], + "completed_issues": item["completed_issues"], + "pending_issues": item["pending_issues"], + } + for item in label_distribution + ] + + current_cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, pk=cycle_id + ).first() + + if current_cycle: + current_cycle.progress_snapshot = { + "total_issues": old_cycle.first().total_issues, + "completed_issues": old_cycle.first().completed_issues, + "cancelled_issues": old_cycle.first().cancelled_issues, + "started_issues": old_cycle.first().started_issues, + "unstarted_issues": old_cycle.first().unstarted_issues, + "backlog_issues": old_cycle.first().backlog_issues, + "distribution": { + "labels": label_distribution_data, + "assignees": assignee_distribution_data, + "completion_chart": completion_chart, + }, + } + # Save the snapshot of the current cycle + current_cycle.save(update_fields=["progress_snapshot"]) + if ( new_cycle.end_date is not None and new_cycle.end_date < timezone.now().date() diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index fb36ea2a9..53248a21a 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -2,27 +2,28 @@ import json # Django improts -from django.utils import timezone -from django.db.models import Q from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import Q +from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response # Module imports -from .base import BaseAPIView -from plane.app.permissions import ProjectLitePermission from plane.api.serializers import InboxIssueSerializer, IssueSerializer +from plane.app.permissions import ProjectLitePermission +from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( + Inbox, InboxIssue, Issue, - State, - ProjectMember, Project, - Inbox, + ProjectMember, + State, ) -from plane.bgtasks.issue_activites_task import issue_activity + +from .base import BaseAPIView class InboxIssueAPIEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index 643221dca..00bc3f1fe 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -2,32 +2,33 @@ import json # Django imports -from django.db.models import Count, Prefetch, Q, F, Func, OuterRef -from django.utils import timezone from django.core import serializers +from django.db.models import Count, F, Func, OuterRef, Prefetch, Q +from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response # Module imports -from .base import BaseAPIView, WebhookMixin +from plane.api.serializers import ( + IssueSerializer, + ModuleIssueSerializer, + ModuleSerializer, +) from plane.app.permissions import ProjectEntityPermission +from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( - Project, - Module, - ModuleLink, Issue, - ModuleIssue, IssueAttachment, IssueLink, + Module, + ModuleIssue, + ModuleLink, + Project, ) -from plane.api.serializers import ( - ModuleSerializer, - ModuleIssueSerializer, - IssueSerializer, -) -from plane.bgtasks.issue_activites_task import issue_activity + +from .base import BaseAPIView, WebhookMixin class ModuleAPIEndpoint(WebhookMixin, BaseAPIView): diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 4ee899831..28181fffb 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -1,16 +1,17 @@ # Django imports from django.db import IntegrityError from django.db.models import Q +from rest_framework import status # Third party imports from rest_framework.response import Response -from rest_framework import status + +from plane.api.serializers import StateSerializer +from plane.app.permissions import ProjectEntityPermission +from plane.db.models import Issue, State # Module imports from .base import BaseAPIView -from plane.api.serializers import StateSerializer -from plane.app.permissions import ProjectEntityPermission -from plane.db.models import State, Issue class StateAPIEndpoint(BaseAPIView): @@ -86,7 +87,11 @@ class StateAPIEndpoint(BaseAPIView): def get(self, request, slug, project_id, state_id=None): if state_id: - serializer = StateSerializer(self.get_queryset().get(pk=state_id)) + serializer = StateSerializer( + self.get_queryset().get(pk=state_id), + fields=self.fields, + expand=self.expand, + ) return Response(serializer.data, status=status.HTTP_200_OK) return self.paginate( request=request, diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 0b1cc94f3..b4a6250a0 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -21,9 +21,9 @@ from django.db.models import ( ) from django.db.models.functions import Coalesce from django.utils import timezone -from rest_framework import status # Third party imports +from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ( diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index a27f52c74..23df58540 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -1,57 +1,59 @@ # Python imports import json -# Django imports -from django.utils import timezone -from django.db.models import ( - Prefetch, - OuterRef, - Func, - F, - Q, - Case, - Value, - CharField, - When, - Exists, - Max, -) -from django.core.serializers.json import DjangoJSONEncoder -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField -from django.db.models import UUIDField +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import ( + Case, + CharField, + Exists, + F, + Func, + Max, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, + When, +) from django.db.models.functions import Coalesce +# Django imports +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page +from rest_framework import status + # Third Party imports from rest_framework.response import Response -from rest_framework import status -# Module imports -from .. import BaseViewSet, BaseAPIView, WebhookMixin -from plane.app.serializers import ( - IssuePropertySerializer, - IssueSerializer, - IssueCreateSerializer, - IssueDetailSerializer, -) from plane.app.permissions import ( ProjectEntityPermission, ProjectLitePermission, ) -from plane.db.models import ( - Project, - Issue, - IssueProperty, - IssueLink, - IssueAttachment, - IssueSubscriber, - IssueReaction, +from plane.app.serializers import ( + IssueCreateSerializer, + IssueDetailSerializer, + IssuePropertySerializer, + IssueSerializer, ) from plane.bgtasks.issue_activites_task import issue_activity +from plane.db.models import ( + Issue, + IssueAttachment, + IssueLink, + IssueProperty, + IssueReaction, + IssueSubscriber, + Project, +) from plane.utils.issue_filters import issue_filters +# Module imports +from .. import BaseAPIView, BaseViewSet, WebhookMixin + class IssueListEndpoint(BaseAPIView): diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py index e1c6962d8..62a0aa25c 100644 --- a/apiserver/plane/app/views/issue/draft.py +++ b/apiserver/plane/app/views/issue/draft.py @@ -120,7 +120,6 @@ class IssueDraftViewSet(BaseViewSet): @method_decorator(gzip_page) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] state_order = [ diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 948eb1b91..cd57690c6 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -1,18 +1,18 @@ # Python imports -from itertools import groupby from datetime import timedelta +from itertools import groupby # Django import from django.db import models -from django.utils import timezone -from django.db.models.functions import TruncDate -from django.db.models import Count, F, Sum, Value, Case, When, CharField +from django.db.models import Case, CharField, Count, F, Sum, Value, When from django.db.models.functions import ( Coalesce, + Concat, ExtractMonth, ExtractYear, - Concat, + TruncDate, ) +from django.utils import timezone # Module imports from plane.db.models import Issue @@ -115,11 +115,16 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None): total_issues = queryset.total_issues if cycle_id: - # Get all dates between the two dates - date_range = [ - queryset.start_date + timedelta(days=x) - for x in range((queryset.end_date - queryset.start_date).days + 1) - ] + if queryset.end_date and queryset.start_date: + # Get all dates between the two dates + date_range = [ + queryset.start_date + timedelta(days=x) + for x in range( + (queryset.end_date - queryset.start_date).days + 1 + ) + ] + else: + date_range = [] chart_data = {str(date): 0 for date in date_range}