mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: transfer issues from one project to another
This commit is contained in:
parent
11258686ad
commit
2161bf176b
@ -93,6 +93,7 @@ from plane.api.views import (
|
|||||||
IssueRelationViewSet,
|
IssueRelationViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
IssueDraftViewSet,
|
IssueDraftViewSet,
|
||||||
|
TransferProjectIssueEndpoint,
|
||||||
## End Issues
|
## End Issues
|
||||||
# States
|
# States
|
||||||
StateViewSet,
|
StateViewSet,
|
||||||
@ -856,6 +857,11 @@ urlpatterns = [
|
|||||||
ExportIssuesEndpoint.as_view(),
|
ExportIssuesEndpoint.as_view(),
|
||||||
name="export-issues",
|
name="export-issues",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/transfer-issues/",
|
||||||
|
TransferProjectIssueEndpoint.as_view(),
|
||||||
|
name="transfer-issues",
|
||||||
|
),
|
||||||
## End Issues
|
## End Issues
|
||||||
## Issue Activity
|
## Issue Activity
|
||||||
path(
|
path(
|
||||||
|
@ -90,6 +90,7 @@ from .issue import (
|
|||||||
IssueRetrievePublicEndpoint,
|
IssueRetrievePublicEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
IssueDraftViewSet,
|
IssueDraftViewSet,
|
||||||
|
TransferProjectIssueEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
|
@ -279,7 +279,8 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -460,10 +461,11 @@ class UserWorkSpaceIssues(BaseAPIView):
|
|||||||
{"error": "Group by and sub group by cannot be same"},
|
{"error": "Group by and sub group by cannot be same"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
if group_by:
|
if group_by:
|
||||||
return Response(
|
return Response(
|
||||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
group_results(issues, group_by, sub_group_by),
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
@ -2114,7 +2116,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if relation == "blocking":
|
if relation == "blocking":
|
||||||
return Response(
|
return Response(
|
||||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||||
@ -2157,6 +2159,8 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
.select_related("issue")
|
.select_related("issue")
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
@ -2366,7 +2370,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
serializer_class = IssueFlatSerializer
|
serializer_class = IssueFlatSerializer
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -2385,7 +2388,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
current_instance = (
|
current_instance = (
|
||||||
@ -2406,7 +2408,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
Issue.objects.annotate(
|
Issue.objects.annotate(
|
||||||
@ -2432,7 +2433,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
@ -2541,7 +2541,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
@ -2575,7 +2574,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
try:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
@ -2586,4 +2584,68 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
{"error": "Issue Does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TransferProjectIssueEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
issue_ids = request.data.get("issue_ids", [])
|
||||||
|
transfer_project_id = request.data.get("transfer_project_id", False)
|
||||||
|
|
||||||
|
if not issue_ids or not transfer_project_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue ids and transafer project id is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The project that all issues need to be transfered
|
||||||
|
transfer_project = Project.objects.get(
|
||||||
|
workspace__slug=slug, pk=transfer_project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch all the issues
|
||||||
|
issues = Issue.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
bulk_issues = []
|
||||||
|
for issue in issues:
|
||||||
|
issue.project_id = transfer_project_id
|
||||||
|
bulk_issues.append(issue)
|
||||||
|
|
||||||
|
moved_issues_count = Issue.objects.bulk_update(
|
||||||
|
bulk_issues, ["project_id"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
if moved_issues_count:
|
||||||
|
[
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.transfer.activity",
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
requested_data=json.dumps({"old_project_id": str(project_id)}),
|
||||||
|
current_instance=None,
|
||||||
|
project_id=transfer_project_id,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
)
|
||||||
|
for issue in bulk_issues
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": f"{moved_issues_count} issue(s) moved to {transfer_project.name}"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Transfer project does not exist"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
@ -1191,6 +1191,29 @@ def delete_draft_issue_activity(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_issue_activity(requested_data, current_instance, issue_id, project, actor, issue_activities):
|
||||||
|
|
||||||
|
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||||
|
|
||||||
|
# Old project
|
||||||
|
old_project = Project.objects.get(pk=requested_data.get("old_project_id"))
|
||||||
|
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
verb="updated",
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"moved the issue",
|
||||||
|
old_identifier=requested_data.get("old_project_id"),
|
||||||
|
new_identifier=project.id,
|
||||||
|
old_value=old_project.name,
|
||||||
|
new_value=project.name,
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Receive message from room group
|
# Receive message from room group
|
||||||
@shared_task
|
@shared_task
|
||||||
def issue_activity(
|
def issue_activity(
|
||||||
@ -1265,6 +1288,7 @@ def issue_activity(
|
|||||||
"issue_draft.activity.created": create_draft_issue_activity,
|
"issue_draft.activity.created": create_draft_issue_activity,
|
||||||
"issue_draft.activity.updated": update_draft_issue_activity,
|
"issue_draft.activity.updated": update_draft_issue_activity,
|
||||||
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||||
|
"issue.transfer.activity": transfer_issue_activity,
|
||||||
}
|
}
|
||||||
|
|
||||||
func = ACTIVITY_MAPPER.get(type)
|
func = ACTIVITY_MAPPER.get(type)
|
||||||
|
Loading…
Reference in New Issue
Block a user