mirror of
https://github.com/makeplane/plane.git
synced 2026-07-14 06:25:58 +02:00
cherry-pick: bugfixes in 1.8.2 from preview
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user