diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 826ccaa189..6ee199dc86 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -116,25 +116,23 @@ class IssueCreateSerializer(BaseSerializer): data["label_ids"] = label_ids if label_ids else [] return data - def validate(self, data): + def validate(self, attrs): if ( - data.get("start_date", None) is not None - and data.get("target_date", None) is not None - and data.get("start_date", None) > data.get("target_date", None) + attrs.get("start_date", None) is not None + and attrs.get("target_date", None) is not None + and attrs.get("start_date", None) > attrs.get("target_date", None) ): raise serializers.ValidationError("Start date cannot exceed target date") - return data - def get_valid_assignees(self, assignees, project_id): - if not assignees: - return [] + if attrs.get("assignee_ids", []): + attrs["assignee_ids"] = ProjectMember.objects.filter( + project_id=self.context["project_id"], + role__gte=15, + is_active=True, + member_id__in=attrs["assignee_ids"], + ).values_list("member_id", flat=True) - return ProjectMember.objects.filter( - project_id=project_id, - role__gte=15, - is_active=True, - member_id__in=assignees - ).values_list('member_id', flat=True) + return attrs def create(self, validated_data): assignees = validated_data.pop("assignee_ids", None) @@ -164,20 +162,19 @@ class IssueCreateSerializer(BaseSerializer): created_by_id = issue.created_by_id updated_by_id = issue.updated_by_id - valid_assignee_ids = self.get_valid_assignees(assignees, project_id) - if valid_assignee_ids is not None and len(valid_assignee_ids): + if assignees is not None and len(assignees): try: IssueAssignee.objects.bulk_create( [ IssueAssignee( - assignee_id=user_id, + assignee_id=assignee_id, issue=issue, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for user_id in valid_assignee_ids + for assignee_id in assignees ], batch_size=10, ) @@ -185,12 +182,15 @@ class IssueCreateSerializer(BaseSerializer): pass else: # Then assign it to default assignee, if it is a valid assignee - if default_assignee_id is not None and ProjectMember.objects.filter( - member_id=default_assignee_id, - project_id=project_id, - role__gte=15, - is_active=True - ).exists(): + if ( + default_assignee_id is not None + and ProjectMember.objects.filter( + member_id=default_assignee_id, + project_id=project_id, + role__gte=15, + is_active=True, + ).exists() + ): try: IssueAssignee.objects.create( assignee_id=default_assignee_id, @@ -234,21 +234,20 @@ class IssueCreateSerializer(BaseSerializer): created_by_id = instance.created_by_id updated_by_id = instance.updated_by_id - valid_assignee_ids = self.get_valid_assignees(assignees, project_id) - if valid_assignee_ids is not None: + if assignees is not None: IssueAssignee.objects.filter(issue=instance).delete() try: IssueAssignee.objects.bulk_create( [ IssueAssignee( - assignee_id=user_id, + assignee_id=assignee_id, issue=instance, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for user_id in valid_assignee_ids + for assignee_id in assignees ], batch_size=10, ignore_conflicts=True, diff --git a/apiserver/plane/app/views/intake/base.py b/apiserver/plane/app/views/intake/base.py index 72861d381c..949139c6d0 100644 --- a/apiserver/plane/app/views/intake/base.py +++ b/apiserver/plane/app/views/intake/base.py @@ -431,7 +431,7 @@ class IntakeIssueViewSet(BaseViewSet): } issue_serializer = IssueCreateSerializer( - issue, data=issue_data, partial=True + issue, data=issue_data, partial=True, context={"project_id": project_id} ) if issue_serializer.is_valid(): diff --git a/apiserver/plane/app/views/issue/base.py b/apiserver/plane/app/views/issue/base.py index 7ad5058b3e..3afee8b1ff 100644 --- a/apiserver/plane/app/views/issue/base.py +++ b/apiserver/plane/app/views/issue/base.py @@ -55,10 +55,7 @@ from plane.utils.grouper import ( ) from plane.utils.issue_filters import issue_filters from plane.utils.order_queryset import order_issue_queryset -from plane.utils.paginator import ( - GroupedOffsetPaginator, - SubGroupedOffsetPaginator, -) +from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator from .. import BaseAPIView, BaseViewSet from plane.utils.timezone_converter import user_timezone_converter from plane.bgtasks.recent_visited_task import recent_visited_task @@ -75,13 +72,10 @@ class IssueListEndpoint(BaseAPIView): if not issue_ids: return Response( - {"error": "Issues are required"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST ) - issue_ids = [ - issue_id for issue_id in issue_ids.split(",") if issue_id != "" - ] + issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""] queryset = ( Issue.issue_objects.filter( @@ -113,9 +107,7 @@ class IssueListEndpoint(BaseAPIView): .values("count") ) .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -137,9 +129,7 @@ class IssueListEndpoint(BaseAPIView): # issue queryset issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, - group_by=group_by, - sub_group_by=sub_group_by, + queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by ) recent_visited_task.delay( @@ -208,9 +198,7 @@ class IssueViewSet(BaseViewSet): def get_queryset(self): return ( - Issue.issue_objects.filter( - project_id=self.kwargs.get("project_id") - ) + Issue.issue_objects.filter(project_id=self.kwargs.get("project_id")) .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") @@ -237,9 +225,7 @@ class IssueViewSet(BaseViewSet): .values("count") ) .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -251,9 +237,7 @@ class IssueViewSet(BaseViewSet): def list(self, request, slug, project_id): extra_filters = {} if request.GET.get("updated_at__gt", None) is not None: - extra_filters = { - "updated_at__gt": request.GET.get("updated_at__gt") - } + extra_filters = {"updated_at__gt": request.GET.get("updated_at__gt")} project = Project.objects.get(pk=project_id, workspace__slug=slug) filters = issue_filters(request.query_params, "GET") @@ -273,9 +257,7 @@ class IssueViewSet(BaseViewSet): # issue queryset issue_queryset = issue_queryset_grouper( - queryset=issue_queryset, - group_by=group_by, - sub_group_by=sub_group_by, + queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by ) recent_visited_task.delay( @@ -312,9 +294,7 @@ class IssueViewSet(BaseViewSet): order_by=order_by_param, queryset=issue_queryset, on_results=lambda issues: issue_on_results( - group_by=group_by, - issues=issues, - sub_group_by=sub_group_by, + group_by=group_by, issues=issues, sub_group_by=sub_group_by ), paginator_cls=SubGroupedOffsetPaginator, group_by_fields=issue_group_values( @@ -347,9 +327,7 @@ class IssueViewSet(BaseViewSet): order_by=order_by_param, queryset=issue_queryset, on_results=lambda issues: issue_on_results( - group_by=group_by, - issues=issues, - sub_group_by=sub_group_by, + group_by=group_by, issues=issues, sub_group_by=sub_group_by ), paginator_cls=GroupedOffsetPaginator, group_by_fields=issue_group_values( @@ -395,7 +373,7 @@ class IssueViewSet(BaseViewSet): project_id=project_id, slug=slug ) if workflow_state_manager._validate_issue_creation( - state_id=request.data.get("state_id"), + state_id=request.data.get("state_id") ): return Response( {"error": "You cannot create a issue in this state"}, @@ -408,9 +386,7 @@ class IssueViewSet(BaseViewSet): # Track the issue issue_activity.delay( type="issue.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), actor_id=str(request.user.id), issue_id=str(serializer.data.get("id", None)), project_id=str(project_id), @@ -421,9 +397,7 @@ class IssueViewSet(BaseViewSet): ) issue = ( issue_queryset_grouper( - queryset=self.get_queryset().filter( - pk=serializer.data["id"] - ), + queryset=self.get_queryset().filter(pk=serializer.data["id"]), group_by=None, sub_group_by=None, ) @@ -483,9 +457,7 @@ class IssueViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], - creator=True, - model=Issue, + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue ) def retrieve(self, request, slug, project_id, pk=None): project = Project.objects.get(pk=project_id, workspace__slug=slug) @@ -519,9 +491,7 @@ class IssueViewSet(BaseViewSet): .values("count") ) .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -567,9 +537,7 @@ class IssueViewSet(BaseViewSet): .prefetch_related( Prefetch( "issue_reactions", - queryset=IssueReaction.objects.select_related( - "issue", "actor" - ), + queryset=IssueReaction.objects.select_related("issue", "actor"), ) ) .prefetch_related( @@ -705,7 +673,7 @@ class IssueViewSet(BaseViewSet): requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) serializer = IssueCreateSerializer( - issue, data=request.data, partial=True + issue, data=request.data, partial=True, context={"project_id": project_id} ) if serializer.is_valid(): serializer.save() @@ -800,9 +768,7 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView): user=request.user, project_id=project_id ) - issue_property.filters = request.data.get( - "filters", issue_property.filters - ) + issue_property.filters = request.data.get("filters", issue_property.filters) issue_property.display_filters = request.data.get( "display_filters", issue_property.display_filters ) @@ -829,8 +795,7 @@ class BulkDeleteIssuesEndpoint(BaseAPIView): if not len(issue_ids): return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, + {"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST ) issues = Issue.issue_objects.filter( @@ -854,9 +819,7 @@ class DeletedIssuesListViewSet(BaseAPIView): if request.GET.get("updated_at__gt", None) is not None: filters = {"updated_at__gt": request.GET.get("updated_at__gt")} deleted_issues = ( - Issue.all_objects.filter( - workspace__slug=slug, project_id=project_id - ) + Issue.all_objects.filter(workspace__slug=slug, project_id=project_id) .filter(Q(archived_at__isnull=False) | Q(deleted_at__isnull=False)) .filter(**filters) .values_list("id", flat=True) @@ -875,9 +838,7 @@ class IssuePaginatedViewSet(BaseViewSet): ) return ( - issue_queryset.select_related( - "workspace", "project", "state", "parent" - ) + issue_queryset.select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") .annotate( cycle_id=Subquery( @@ -902,9 +863,7 @@ class IssuePaginatedViewSet(BaseViewSet): .values("count") ) .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1043,9 +1002,7 @@ class IssueDetailEndpoint(BaseAPIView): def get(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") issue = ( - Issue.issue_objects.filter( - workspace__slug=slug, project_id=project_id - ) + Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") .annotate( @@ -1108,9 +1065,7 @@ class IssueDetailEndpoint(BaseAPIView): .values("count") ) .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) .order_by() .annotate(count=Func(F("id"), function="Count")) .values("count") @@ -1133,9 +1088,7 @@ class IssueDetailEndpoint(BaseAPIView): class IssueBulkUpdateDateEndpoint(BaseAPIView): - def validate_dates( - self, current_start, current_target, new_start, new_target - ): + def validate_dates(self, current_start, current_target, new_start, new_target): """ Validate that start date is before target date. """ @@ -1179,12 +1132,8 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView): if start_date: issue_activity.delay( type="issue.activity.updated", - requested_data=json.dumps( - {"start_date": update.get("start_date")} - ), - current_instance=json.dumps( - {"start_date": str(issue.start_date)} - ), + requested_data=json.dumps({"start_date": update.get("start_date")}), + current_instance=json.dumps({"start_date": str(issue.start_date)}), issue_id=str(issue_id), actor_id=str(request.user.id), project_id=str(project_id), @@ -1211,18 +1160,14 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView): issues_to_update.append(issue) # Bulk update issues - Issue.objects.bulk_update( - issues_to_update, ["start_date", "target_date"] - ) + Issue.objects.bulk_update(issues_to_update, ["start_date", "target_date"]) return Response( - {"message": "Issues updated successfully"}, - status=status.HTTP_200_OK, + {"message": "Issues updated successfully"}, status=status.HTTP_200_OK ) class IssueMetaEndpoint(BaseAPIView): - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT") def get(self, request, slug, project_id, issue_id): issue = Issue.issue_objects.only("sequence_id", "project__identifier").get( @@ -1238,14 +1183,12 @@ class IssueMetaEndpoint(BaseAPIView): class IssueDetailIdentifierEndpoint(BaseAPIView): - def strict_str_to_int(self, s): - if not s.isdigit() and not (s.startswith('-') and s[1:].isdigit()): + if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()): raise ValueError("Invalid integer string") return int(s) def get(self, request, slug, project_identifier, issue_identifier): - # Check if the issue identifier is a valid integer try: issue_identifier = self.strict_str_to_int(issue_identifier) @@ -1257,10 +1200,8 @@ class IssueDetailIdentifierEndpoint(BaseAPIView): # Fetch the project project = Project.objects.get( - identifier__iexact=project_identifier, - workspace__slug=slug, + identifier__iexact=project_identifier, workspace__slug=slug ) - # Fetch the issue issue = ( @@ -1349,8 +1290,8 @@ class IssueDetailIdentifierEndpoint(BaseAPIView): .annotate( is_subscribed=Exists( IssueSubscriber.objects.filter( - workspace__slug=slug, - project_id=project.id, + workspace__slug=slug, + project_id=project.id, issue__sequence_id=issue_identifier, subscriber=request.user, ) @@ -1364,7 +1305,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView): {"error": "The required object does not exist."}, status=status.HTTP_404_NOT_FOUND, ) - + # Check if the user is a member of the project if not ProjectMember.objects.filter( workspace__slug=slug, @@ -1374,8 +1315,8 @@ class IssueDetailIdentifierEndpoint(BaseAPIView): ).exists(): return Response( { - "error": "You are not allowed to view this issue", - "type": project.network + "error": "You are not allowed to view this issue", + "type": project.network, }, status=status.HTTP_403_FORBIDDEN, ) diff --git a/apiserver/plane/space/views/intake.py b/apiserver/plane/space/views/intake.py index 99502b0bfa..f0fea8509b 100644 --- a/apiserver/plane/space/views/intake.py +++ b/apiserver/plane/space/views/intake.py @@ -15,7 +15,6 @@ from .base import BaseViewSet from plane.db.models import ( IntakeIssue, Issue, - State, IssueLink, FileAsset, DeployBoard, @@ -216,7 +215,12 @@ class IntakeIssuePublicViewSet(BaseViewSet): "description": issue_data.get("description", issue.description), } - issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) + issue_serializer = IssueCreateSerializer( + issue, + data=issue_data, + partial=True, + context={"project_id": project_deploy_board.project_id}, + ) if issue_serializer.is_valid(): current_instance = issue