diff --git a/README.md b/README.md index 8141bf588..20e34b673 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@
- + - +
diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/api/permissions/__init__.py index 91b3aea35..8b15a9373 100644 --- a/apiserver/plane/api/permissions/__init__.py +++ b/apiserver/plane/api/permissions/__init__.py @@ -1,2 +1,2 @@ -from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission +from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/api/permissions/workspace.py index 7fccc455e..d01b545ee 100644 --- a/apiserver/plane/api/permissions/workspace.py +++ b/apiserver/plane/api/permissions/workspace.py @@ -61,3 +61,13 @@ class WorkspaceEntityPermission(BasePermission): return WorkspaceMember.objects.filter( member=request.user, workspace__slug=view.workspace_slug ).exists() + + +class WorkspaceViewerPermission(BasePermission): + def has_permission(self, request, view): + if request.user.is_anonymous: + return False + + return WorkspaceMember.objects.filter( + member=request.user, workspace__slug=view.workspace_slug, role__gte=10 + ).exists() diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 641edb07c..fa97c5a6d 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -93,6 +93,7 @@ class ProjectDetailSerializer(BaseSerializer): total_cycles = serializers.IntegerField(read_only=True) total_modules = serializers.IntegerField(read_only=True) is_member = serializers.BooleanField(read_only=True) + sort_order = serializers.FloatField(read_only=True) class Meta: model = Project diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index d8da4f7dd..26064d331 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -5,7 +5,7 @@ from datetime import datetime # Django imports from django.core.exceptions import ValidationError from django.db import IntegrityError -from django.db.models import Q, Exists, OuterRef, Func, F +from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery from django.core.validators import validate_email from django.conf import settings @@ -120,9 +120,15 @@ class ProjectViewSet(BaseViewSet): project_id=OuterRef("pk"), workspace__slug=self.kwargs.get("slug"), ) + sort_order_query = ProjectMember.objects.filter( + member=request.user, + project_id=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ).values("sort_order") projects = ( self.get_queryset() .annotate(is_favorite=Exists(subquery)) + .annotate(sort_order=Subquery(sort_order_query)) .order_by("sort_order", "name") .annotate( total_members=ProjectMember.objects.filter( @@ -592,17 +598,26 @@ class AddMemberToProjectEndpoint(BaseAPIView): {"error": "Atleast one member is required"}, status=status.HTTP_400_BAD_REQUEST, ) + bulk_project_members = [] - project_members = ProjectMember.objects.bulk_create( - [ + project_members = ProjectMember.objects.filter( + workspace=self.workspace, member_id__in=[member.get("member_id") for member in members] + ).values("member_id").annotate(sort_order_min=Min("sort_order")) + + for member in members: + sort_order = [project_member.get("sort_order") for project_member in project_members] + bulk_project_members.append( ProjectMember( member_id=member.get("member_id"), role=member.get("role", 10), project_id=project_id, workspace_id=project.workspace_id, + sort_order=sort_order[0] - 10000 if len(sort_order) else 65535 ) - for member in members - ], + ) + + project_members = ProjectMember.objects.bulk_create( + bulk_project_members, batch_size=10, ignore_conflicts=True, ) @@ -845,12 +860,14 @@ class ProjectUserViewsEndpoint(BaseAPIView): view_props = project_member.view_props default_props = project_member.default_props preferences = project_member.preferences + sort_order = project_member.sort_order project_member.view_props = request.data.get("view_props", view_props) project_member.default_props = request.data.get( "default_props", default_props ) project_member.preferences = request.data.get("preferences", preferences) + project_member.sort_order = request.data.get("sort_order", sort_order) project_member.save() diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/api/views/workspace.py index 3404bbf19..51db47c3d 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/api/views/workspace.py @@ -73,12 +73,14 @@ from plane.db.models import ( IssueSubscriber, Project, Label, - State, + WorkspaceMember, + CycleIssue, ) from plane.api.permissions import ( WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, + WorkspaceViewerPermission, ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters @@ -1140,6 +1142,19 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): .count() ) + upcoming_cycles = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__gt=timezone.now().date(), + issue__assignees__in=[user_id,] + ).values("cycle__name", "cycle__id", "cycle__project_id") + + present_cycle = CycleIssue.objects.filter( + workspace__slug=slug, + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), + issue__assignees__in=[user_id,] + ).values("cycle__name", "cycle__id", "cycle__project_id") + return Response( { "state_distribution": state_distribution, @@ -1149,6 +1164,8 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView): "completed_issues": completed_issues_count, "pending_issues": pending_issues_count, "subscribed_issues": subscribed_issues_count, + "present_cycles": present_cycle, + "upcoming_cycles": upcoming_cycles, } ) except Exception as e: @@ -1194,64 +1211,64 @@ class WorkspaceUserActivityEndpoint(BaseAPIView): class WorkspaceUserProfileEndpoint(BaseAPIView): - permission_classes = [ - WorkspaceEntityPermission, - ] def get(self, request, slug, user_id): try: user_data = User.objects.get(pk=user_id) - projects = ( - Project.objects.filter( - workspace__slug=slug, - project_projectmember__member=request.user, - ) - .annotate( - created_issues=Count( - "project_issue", filter=Q(project_issue__created_by_id=user_id) + requesting_workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user) + projects = [] + if requesting_workspace_member.role >= 10: + projects = ( + Project.objects.filter( + workspace__slug=slug, + project_projectmember__member=request.user, + ) + .annotate( + created_issues=Count( + "project_issue", filter=Q(project_issue__created_by_id=user_id) + ) + ) + .annotate( + assigned_issues=Count( + "project_issue", + filter=Q(project_issue__assignees__in=[user_id]), + ) + ) + .annotate( + completed_issues=Count( + "project_issue", + filter=Q( + project_issue__completed_at__isnull=False, + project_issue__assignees__in=[user_id], + ), + ) + ) + .annotate( + pending_issues=Count( + "project_issue", + filter=Q( + project_issue__state__group__in=[ + "backlog", + "unstarted", + "started", + ], + project_issue__assignees__in=[user_id], + ), + ) + ) + .values( + "id", + "name", + "identifier", + "emoji", + "icon_prop", + "created_issues", + "assigned_issues", + "completed_issues", + "pending_issues", ) ) - .annotate( - assigned_issues=Count( - "project_issue", - filter=Q(project_issue__assignees__in=[user_id]), - ) - ) - .annotate( - completed_issues=Count( - "project_issue", - filter=Q( - project_issue__completed_at__isnull=False, - project_issue__assignees__in=[user_id], - ), - ) - ) - .annotate( - pending_issues=Count( - "project_issue", - filter=Q( - project_issue__state__group__in=[ - "backlog", - "unstarted", - "started", - ], - project_issue__assignees__in=[user_id], - ), - ) - ) - .values( - "id", - "name", - "identifier", - "emoji", - "icon_prop", - "created_issues", - "assigned_issues", - "completed_issues", - "pending_issues", - ) - ) return Response( { @@ -1268,6 +1285,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): }, status=status.HTTP_200_OK, ) + except WorkspaceMember.DoesNotExist: + return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) except Exception as e: capture_exception(e) return Response( @@ -1278,7 +1297,7 @@ class WorkspaceUserProfileEndpoint(BaseAPIView): class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): permission_classes = [ - WorkspaceEntityPermission, + WorkspaceViewerPermission, ] def get(self, request, slug, user_id): @@ -1317,7 +1336,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - ) + ).distinct() # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": @@ -1394,9 +1413,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): status=status.HTTP_400_BAD_REQUEST, ) + class WorkspaceLabelsEndpoint(BaseAPIView): permission_classes = [ - WorkspaceEntityPermission, + WorkspaceViewerPermission, ] def get(self, request, slug): diff --git a/apiserver/plane/db/migrations/0039_auto_20230723_2203.py b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py index 78b77521c..8f8700b5d 100644 --- a/apiserver/plane/db/migrations/0039_auto_20230723_2203.py +++ b/apiserver/plane/db/migrations/0039_auto_20230723_2203.py @@ -58,16 +58,16 @@ def update_workspace_member_props(apps, schema_editor): Model.objects.bulk_update(updated_workspace_member, ["view_props"], batch_size=100) -def update_project_sort_order(apps, schema_editor): - Model = apps.get_model("db", "Project") +def update_project_member_sort_order(apps, schema_editor): + Model = apps.get_model("db", "ProjectMember") - updated_projects = [] + updated_project_members = [] for obj in Model.objects.all(): obj.sort_order = random.randint(1, 65536) - updated_projects.append(obj) + updated_project_members.append(obj) - Model.objects.bulk_update(updated_projects, ["sort_order"], batch_size=100) + Model.objects.bulk_update(updated_project_members, ["sort_order"], batch_size=100) class Migration(migrations.Migration): @@ -93,5 +93,5 @@ class Migration(migrations.Migration): name='sort_order', field=models.FloatField(default=65535), ), - migrations.RunPython(update_project_sort_order), + migrations.RunPython(update_project_member_sort_order), ] diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 721b7b698..d700ab5e2 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -91,7 +91,6 @@ class Project(BaseModel): default_state = models.ForeignKey( "db.State", on_delete=models.SET_NULL, null=True, related_name="default_state" ) - sort_order = models.FloatField(default=65535) def __str__(self): """Return name of the project""" diff --git a/apps/app/components/breadcrumbs/index.tsx b/apps/app/components/breadcrumbs/index.tsx index 8a67c92b9..0e5cfb9c4 100644 --- a/apps/app/components/breadcrumbs/index.tsx +++ b/apps/app/components/breadcrumbs/index.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { useRouter } from "next/router"; import Link from "next/link"; // icons -import { ArrowLeftIcon } from "@heroicons/react/24/outline"; import { Icon } from "components/ui"; type BreadcrumbsProps = { @@ -14,7 +13,7 @@ const Breadcrumbs = ({ children }: BreadcrumbsProps) => { return ( <> -