diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index ba22e25f9..7a78b6664 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -48,11 +48,6 @@ class CycleSerializer(BaseSerializer): if not project_id: raise serializers.ValidationError("Project ID is required") - is_start_date_end_date_equal = ( - True - if str(data.get("start_date")) == str(data.get("end_date")) - else False - ) data["start_date"] = convert_to_utc( date=str(data.get("start_date").date()), project_id=project_id, @@ -61,7 +56,6 @@ class CycleSerializer(BaseSerializer): data["end_date"] = convert_to_utc( date=str(data.get("end_date", None).date()), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) return data diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 3e27ffdc4..9005821f3 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -788,6 +788,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -799,6 +800,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -847,6 +849,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ) ) ) + old_cycle = old_cycle.first() estimate_type = Project.objects.filter( workspace__slug=slug, @@ -966,7 +969,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ) estimate_completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="points", @@ -1114,7 +1117,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): # Pass the new_cycle queryset to burndown_plot completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="issues", @@ -1126,12 +1129,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView): ).first() 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, + "total_issues": old_cycle.total_issues, + "completed_issues": old_cycle.completed_issues, + "cancelled_issues": old_cycle.cancelled_issues, + "started_issues": old_cycle.started_issues, + "unstarted_issues": old_cycle.unstarted_issues, + "backlog_issues": old_cycle.backlog_issues, "distribution": { "labels": label_distribution_data, "assignees": assignee_distribution_data, diff --git a/apiserver/plane/app/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py index b56b08350..b3b69e375 100644 --- a/apiserver/plane/app/serializers/cycle.py +++ b/apiserver/plane/app/serializers/cycle.py @@ -25,11 +25,6 @@ class CycleWriteSerializer(BaseSerializer): or (self.instance and self.instance.project_id) or self.context.get("project_id", None) ) - is_start_date_end_date_equal = ( - True - if str(data.get("start_date")) == str(data.get("end_date")) - else False - ) data["start_date"] = convert_to_utc( date=str(data.get("start_date").date()), project_id=project_id, @@ -38,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer): data["end_date"] = convert_to_utc( date=str(data.get("end_date", None).date()), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) return data diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index e88acaf82..60b051b40 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -117,6 +117,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -129,6 +130,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -141,6 +143,7 @@ class CycleViewSet(BaseViewSet): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -266,9 +269,7 @@ class CycleViewSet(BaseViewSet): "created_by", ) datetime_fields = ["start_date", "end_date"] - data = user_timezone_converter( - data, datetime_fields, project_timezone - ) + data = user_timezone_converter(data, datetime_fields, project_timezone) return Response(data, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @@ -415,9 +416,7 @@ class CycleViewSet(BaseViewSet): project_timezone = project.timezone datetime_fields = ["start_date", "end_date"] - cycle = user_timezone_converter( - cycle, datetime_fields, project_timezone - ) + cycle = user_timezone_converter(cycle, datetime_fields, project_timezone) # Send the model activity model_activity.delay( @@ -574,16 +573,12 @@ class CycleDateCheckEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) - is_start_date_end_date_equal = ( - True if str(start_date) == str(end_date) else False - ) start_date = convert_to_utc( date=str(start_date), project_id=project_id, is_start_date=True ) end_date = convert_to_utc( date=str(end_date), project_id=project_id, - is_start_date_end_date_equal=is_start_date_end_date_equal, ) # Check if any cycle intersects in the given interval @@ -668,6 +663,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) @@ -732,6 +728,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) ) ) + old_cycle = old_cycle.first() estimate_type = Project.objects.filter( workspace__slug=slug, @@ -850,7 +847,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) estimate_completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="points", @@ -997,7 +994,7 @@ class TransferCycleIssueEndpoint(BaseAPIView): # Pass the new_cycle queryset to burndown_plot completion_chart = burndown_plot( - queryset=old_cycle.first(), + queryset=old_cycle, slug=slug, project_id=project_id, plot_type="issues", @@ -1009,12 +1006,12 @@ class TransferCycleIssueEndpoint(BaseAPIView): ).first() 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, + "total_issues": old_cycle.total_issues, + "completed_issues": old_cycle.completed_issues, + "cancelled_issues": old_cycle.cancelled_issues, + "started_issues": old_cycle.started_issues, + "unstarted_issues": old_cycle.unstarted_issues, + "backlog_issues": old_cycle.backlog_issues, "distribution": { "labels": label_distribution_data, "assignees": assignee_distribution_data, @@ -1122,6 +1119,14 @@ class CycleUserPropertiesEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def get(self, request, slug, project_id, cycle_id): + + cycle = Cycle.objects.filter( + workspace__slug=slug, project_id=project_id, id=cycle_id + ).first() + if not cycle: + return Response( + {"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND + ) aggregate_estimates = ( Issue.issue_objects.filter( estimate_point__estimate__type="points", @@ -1172,53 +1177,60 @@ class CycleProgressEndpoint(BaseAPIView): ), ) ) + if cycle.progress_snapshot: + backlog_issues = cycle.progress_snapshot.get("backlog_issues", 0) + unstarted_issues = cycle.progress_snapshot.get("unstarted_issues", 0) + started_issues = cycle.progress_snapshot.get("started_issues", 0) + cancelled_issues = cycle.progress_snapshot.get("cancelled_issues", 0) + completed_issues = cycle.progress_snapshot.get("completed_issues", 0) + total_issues = cycle.progress_snapshot.get("total_issues", 0) + else: + backlog_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="backlog", + ).count() - backlog_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="backlog", - ).count() + unstarted_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="unstarted", + ).count() - unstarted_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="unstarted", - ).count() + started_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="started", + ).count() - started_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="started", - ).count() + cancelled_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="cancelled", + ).count() - cancelled_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="cancelled", - ).count() + completed_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + state__group="completed", + ).count() - completed_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - state__group="completed", - ).count() - - total_issues = Issue.issue_objects.filter( - issue_cycle__cycle_id=cycle_id, - issue_cycle__deleted_at__isnull=True, - workspace__slug=slug, - project_id=project_id, - ).count() + total_issues = Issue.issue_objects.filter( + issue_cycle__cycle_id=cycle_id, + issue_cycle__deleted_at__isnull=True, + workspace__slug=slug, + project_id=project_id, + ).count() return Response( { @@ -1279,6 +1291,25 @@ class CycleAnalyticsEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + # this will tell whether the issues were transferred to the new cycle + """ + if the issues were transferred to the new cycle, then the progress_snapshot will be present + return the progress_snapshot data in the analytics for each date + + else issues were not transferred to the new cycle then generate the stats from the cycle isssue bridge tables + """ + + if cycle.progress_snapshot: + distribution = cycle.progress_snapshot.get("distribution", {}) + return Response( + { + "labels": distribution.get("labels", []), + "assignees": distribution.get("assignees", []), + "completion_chart": distribution.get("completion_chart", {}), + }, + status=status.HTTP_200_OK, + ) + estimate_type = Project.objects.filter( workspace__slug=slug, pk=project_id, diff --git a/apiserver/plane/app/views/workspace/cycle.py b/apiserver/plane/app/views/workspace/cycle.py index a9398a91d..3dce746ea 100644 --- a/apiserver/plane/app/views/workspace/cycle.py +++ b/apiserver/plane/app/views/workspace/cycle.py @@ -29,6 +29,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView): issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, issue_cycle__deleted_at__isnull=True, + issue_cycle__issue__deleted_at__isnull=True, ), ) ) diff --git a/apiserver/plane/utils/timezone_converter.py b/apiserver/plane/utils/timezone_converter.py index 40480b4f6..e4252422a 100644 --- a/apiserver/plane/utils/timezone_converter.py +++ b/apiserver/plane/utils/timezone_converter.py @@ -36,7 +36,7 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone): def convert_to_utc( - date, project_id, is_start_date=False, is_start_date_end_date_equal=False + date, project_id, is_start_date=False ): """ Converts a start date string to the project's local timezone at 12:00 AM @@ -82,10 +82,8 @@ def convert_to_utc( return utc_datetime else: - # If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds - # to make it the end of the day - if is_start_date_end_date_equal: - localized_datetime += timedelta(hours=23, minutes=59, seconds=59) + # the cycle end date is the last minute of the day + localized_datetime += timedelta(hours=23, minutes=59, seconds=0) # Convert the localized datetime to UTC utc_datetime = localized_datetime.astimezone(pytz.utc)