diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 2b40af672..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,23 +0,0 @@ -version = 1 - -exclude_patterns = [ - "bin/**", - "**/node_modules/", - "**/*.min.js" -] - -[[analyzers]] -name = "shell" - -[[analyzers]] -name = "javascript" - - [analyzers.meta] - plugins = ["react"] - environment = ["nodejs"] - -[[analyzers]] -name = "python" - - [analyzers.meta] - runtime_version = "3.x.x" \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 92b44af5b..d7b94d245 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,9 @@ name: "CodeQL" on: + workflow_dispatch: push: - branches: ["master"] + branches: ["develop", "preview", "master"] pull_request: branches: ["develop", "preview", "master"] schedule: diff --git a/README.md b/README.md index 6834199ff..11db5ceba 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@
- Website • - Releases • - Twitter • - Documentation + Website • + Releases • + Twitter • + Documentation
@@ -40,15 +40,15 @@
-Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘♀️ +Meet [Plane](https://dub.sh/plane-website-readme). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘♀️ -> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases. +> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve in our upcoming releases. ## ⚡ Installation The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users. -If you want more control over your data prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). +If you want more control over your data, prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose). | Installation Methods | Documentation Link | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -59,9 +59,9 @@ If you want more control over your data prefer to self-host Plane, please refer ## 🚀 Features -- **Issues**: Quickly create issues and add details using a powerful, rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking. +- **Issues**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to problems for better organization and tracking. -- **Cycles** +- **Cycles**: Keep up your team's momentum with Cycles. Gain insights into your project's progress with burn-down charts and other valuable features. - **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to track and plan your project's progress easily. @@ -74,11 +74,11 @@ If you want more control over your data prefer to self-host Plane, please refer - **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution. -## 🛠️ Contributors Quick Start +## 🛠️ Quick start for contributors > Development system must have docker engine installed and running. -Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute +Setting up local environment is extremely easy and straight forward. Follow the below step and you will be ready to contribute - 1. Clone the code locally using: ``` diff --git a/apiserver/back_migration.py b/apiserver/back_migration.py index a0e45416a..328b9db2b 100644 --- a/apiserver/back_migration.py +++ b/apiserver/back_migration.py @@ -182,7 +182,7 @@ def update_label_color(): labels = Label.objects.filter(color="") updated_labels = [] for label in labels: - label.color = "#" + "%06x" % random.randint(0, 0xFFFFFF) + label.color = f"#{random.randint(0, 0xFFFFFF+1):06X}" updated_labels.append(label) Label.objects.bulk_update(updated_labels, ["color"], batch_size=100) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 0f5aab3e3..447a9c65e 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,31 +1,32 @@ -from lxml import html +from django.core.exceptions import ValidationError +from django.core.validators import URLValidator # Django imports from django.utils import timezone -from django.core.validators import URLValidator -from django.core.exceptions import ValidationError +from lxml import html # Third party imports from rest_framework import serializers # Module imports from plane.db.models import ( - User, Issue, - State, + IssueActivity, IssueAssignee, - Label, + IssueComment, IssueLabel, IssueLink, - IssueComment, - IssueActivity, + Label, ProjectMember, + State, + User, ) + from .base import BaseSerializer -from .cycle import CycleSerializer, CycleLiteSerializer -from .module import ModuleSerializer, ModuleLiteSerializer -from .user import UserLiteSerializer +from .cycle import CycleLiteSerializer, CycleSerializer +from .module import ModuleLiteSerializer, ModuleSerializer from .state import StateLiteSerializer +from .user import UserLiteSerializer class IssueSerializer(BaseSerializer): @@ -78,7 +79,7 @@ class IssueSerializer(BaseSerializer): data["description_html"] = parsed_str except Exception as e: - raise serializers.ValidationError(f"Invalid HTML: {str(e)}") + raise serializers.ValidationError("Invalid HTML passed") # Validate assignees are from project if data.get("assignees", []): @@ -293,7 +294,7 @@ class IssueLinkSerializer(BaseSerializer): raise serializers.ValidationError("Invalid URL format.") # Check URL scheme - if not value.startswith(('http://', 'https://')): + if not value.startswith(("http://", "https://")): raise serializers.ValidationError("Invalid URL scheme.") return value @@ -349,7 +350,7 @@ class IssueCommentSerializer(BaseSerializer): data["comment_html"] = parsed_str except Exception as e: - raise serializers.ValidationError(f"Invalid HTML: {str(e)}") + raise serializers.ValidationError("Invalid HTML passed") return data diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index ee9dc5df1..3aedbcad2 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -504,8 +504,8 @@ class IssueReactionLiteSerializer(DynamicBaseSerializer): model = IssueReaction fields = [ "id", - "actor_id", - "issue_id", + "actor", + "issue", "reaction", ] diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 8d8b54929..d323a0f63 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -101,59 +101,69 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) ) .annotate(is_favorite=Exists(favorite_subquery)) + .annotate( + total_issues=Count( + "issue_cycle__issue__id", + distinct=True, + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) .annotate( completed_issues=Count( - "issue_cycle__issue__state__group", + "issue_cycle__issue__id", + distinct=True, filter=Q( issue_cycle__issue__state__group="completed", issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, ), - distinct=True, ) ) .annotate( cancelled_issues=Count( - "issue_cycle__issue__state__group", + "issue_cycle__issue__id", + distinct=True, filter=Q( issue_cycle__issue__state__group="cancelled", issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, ), - distinct=True, ) ) .annotate( started_issues=Count( - "issue_cycle__issue__state__group", + "issue_cycle__issue__id", + distinct=True, filter=Q( issue_cycle__issue__state__group="started", issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, ), - distinct=True, ) ) .annotate( unstarted_issues=Count( - "issue_cycle__issue__state__group", + "issue_cycle__issue__id", + distinct=True, filter=Q( issue_cycle__issue__state__group="unstarted", issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, ), - distinct=True, ) ) .annotate( backlog_issues=Count( - "issue_cycle__issue__state__group", + "issue_cycle__issue__id", + distinct=True, filter=Q( issue_cycle__issue__state__group="backlog", issue_cycle__issue__archived_at__isnull=True, issue_cycle__issue__is_draft=False, ), - distinct=True, ) ) .annotate( @@ -182,9 +192,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): distinct=True, filter=~Q( issue_cycle__issue__assignees__id__isnull=True - ) - & Q( - issue_cycle__issue__assignees__member_project__is_active=True ), ), Value([], output_field=ArrayField(UUIDField())), @@ -195,19 +202,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) def list(self, request, slug, project_id): - queryset = ( - self.get_queryset() - .filter(archived_at__isnull=True) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - ) + queryset = self.get_queryset().filter(archived_at__isnull=True) cycle_view = request.GET.get("cycle_view", "all") # Update the order by @@ -361,8 +356,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "external_id", "progress_snapshot", # meta fields - "total_issues", "is_favorite", + "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -409,6 +404,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): # meta fields "is_favorite", "cancelled_issues", + "total_issues", "completed_issues", "started_issues", "unstarted_issues", @@ -484,6 +480,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", + "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -497,32 +494,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet): def retrieve(self, request, slug, project_id, pk): queryset = ( - self.get_queryset() - .filter(archived_at__isnull=True) - .filter(pk=pk) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) + self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk) ) data = ( self.get_queryset() .filter(pk=pk) - .annotate( - total_issues=Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - parent__isnull=True, - issue_cycle__cycle_id=pk, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) .annotate( sub_issues=Issue.issue_objects.filter( project_id=self.kwargs.get("project_id"), @@ -880,7 +856,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): ) cycle.archived_at = timezone.now() cycle.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(cycle.archived_at)}, + status=status.HTTP_200_OK, + ) def delete(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py index 557c2018f..c5dc35809 100644 --- a/apiserver/plane/app/views/issue/label.py +++ b/apiserver/plane/app/views/issue/label.py @@ -87,7 +87,7 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView): Label( name=label.get("name", "Migrated"), description=label.get("description", "Migrated Issue"), - color="#" + "%06x" % random.randint(0, 0xFFFFFF), + color=f"#{random.randint(0, 0xFFFFFF+1):06X}", project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 8d3e0235d..3fe3a078a 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -8,9 +8,11 @@ from django.db.models import ( Exists, F, Func, + IntegerField, OuterRef, Prefetch, Q, + Subquery, UUIDField, Value, ) @@ -72,6 +74,59 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) + cancelled_issues = ( + Issue.issue_objects.filter( + state__group="cancelled", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + completed_issues = ( + Issue.issue_objects.filter( + state__group="completed", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + started_issues = ( + Issue.issue_objects.filter( + state__group="started", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + unstarted_issues = ( + Issue.issue_objects.filter( + state__group="unstarted", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + backlog_issues = ( + Issue.issue_objects.filter( + state__group="backlog", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) + total_issues = ( + Issue.issue_objects.filter( + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate(cnt=Count("pk")) + .values("cnt") + ) return ( super() .get_queryset() @@ -91,68 +146,39 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ) ) .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, - ), - ) - .annotate( - completed_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="completed", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, + completed_issues=Coalesce( + Subquery(completed_issues[:1]), + Value(0, output_field=IntegerField()), ) ) .annotate( - cancelled_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="cancelled", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, + cancelled_issues=Coalesce( + Subquery(cancelled_issues[:1]), + Value(0, output_field=IntegerField()), ) ) .annotate( - started_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="started", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, + started_issues=Coalesce( + Subquery(started_issues[:1]), + Value(0, output_field=IntegerField()), ) ) .annotate( - unstarted_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="unstarted", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, + unstarted_issues=Coalesce( + Subquery(unstarted_issues[:1]), + Value(0, output_field=IntegerField()), ) ) .annotate( - backlog_issues=Count( - "issue_module__issue__state__group", - filter=Q( - issue_module__issue__state__group="backlog", - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - distinct=True, + backlog_issues=Coalesce( + Subquery(backlog_issues[:1]), + Value(0, output_field=IntegerField()), + ) + ) + .annotate( + total_issues=Coalesce( + Subquery(total_issues[:1]), + Value(0, output_field=IntegerField()), ) ) .annotate( @@ -202,6 +228,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "is_favorite", "cancelled_issues", "completed_issues", + "total_issues", "started_issues", "unstarted_issues", "backlog_issues", @@ -257,16 +284,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): self.get_queryset() .filter(archived_at__isnull=True) .filter(pk=pk) - .annotate( - total_issues=Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id"), - parent__isnull=True, - issue_module__module_id=pk, - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) .annotate( sub_issues=Issue.issue_objects.filter( project_id=self.kwargs.get("project_id"), @@ -378,9 +395,11 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "completion_chart": {}, } - if queryset.first().start_date and queryset.first().target_date: + # Fetch the modules + modules = queryset.first() + if modules and modules.start_date and modules.target_date: data["distribution"]["completion_chart"] = burndown_plot( - queryset=queryset.first(), + queryset=modules, slug=slug, project_id=project_id, module_id=pk, @@ -429,6 +448,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "cancelled_issues", "completed_issues", "started_issues", + "total_issues", "unstarted_issues", "backlog_issues", "created_at", @@ -642,7 +662,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): ) module.archived_at = timezone.now() module.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(module.archived_at)}, + status=status.HTTP_200_OK, + ) def delete(self, request, slug, project_id, module_id): module = Module.objects.get( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 1c9c78e77..0ae951770 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -441,7 +441,10 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() project.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(project.archived_at)}, + status=status.HTTP_200_OK, + ) def delete(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) diff --git a/apiserver/plane/app/views/workspace/estimate.py b/apiserver/plane/app/views/workspace/estimate.py index 6b64d8c90..59a23d867 100644 --- a/apiserver/plane/app/views/workspace/estimate.py +++ b/apiserver/plane/app/views/workspace/estimate.py @@ -3,15 +3,10 @@ from rest_framework import status from rest_framework.response import Response # Module imports +from plane.app.permissions import WorkspaceEntityPermission from plane.app.serializers import WorkspaceEstimateSerializer from plane.app.views.base import BaseAPIView -from plane.db.models import Project, Estimate -from plane.app.permissions import WorkspaceEntityPermission - -# Django imports -from django.db.models import ( - Prefetch, -) +from plane.db.models import Estimate, Project from plane.utils.cache import cache_response @@ -25,15 +20,11 @@ class WorkspaceEstimatesEndpoint(BaseAPIView): estimate_ids = Project.objects.filter( workspace__slug=slug, estimate__isnull=False ).values_list("estimate_id", flat=True) - estimates = Estimate.objects.filter( - pk__in=estimate_ids - ).prefetch_related( - Prefetch( - "points", - queryset=Project.objects.select_related( - "estimate", "workspace", "project" - ), - ) + estimates = ( + Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug) + .prefetch_related("points") + .select_related("workspace", "project") ) + serializer = WorkspaceEstimateSerializer(estimates, many=True) return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 28b45adbe..2b7d383ba 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,6 +1,6 @@ # base requirements -Django==4.2.10 +Django==4.2.11 psycopg==3.1.12 djangorestframework==3.14.0 redis==4.6.0 diff --git a/apiserver/templates/emails/invitations/project_invitation.html b/apiserver/templates/emails/invitations/project_invitation.html index 630a5eab3..def576601 100644 --- a/apiserver/templates/emails/invitations/project_invitation.html +++ b/apiserver/templates/emails/invitations/project_invitation.html @@ -1,349 +1,1815 @@ - - - - - - -
-
|
+