Compare commits

...

2 Commits

Author SHA1 Message Date
pablohashescobar
a3d555971c dev: remove related assignee, labels, cycles and modules and transfer attachments and links 2023-09-20 12:24:20 +05:30
pablohashescobar
2161bf176b feat: transfer issues from one project to another 2023-09-19 23:06:19 +05:30
4 changed files with 149 additions and 11 deletions

View File

@ -93,6 +93,7 @@ from plane.api.views import (
IssueRelationViewSet,
CommentReactionViewSet,
IssueDraftViewSet,
TransferProjectIssueEndpoint,
## End Issues
# States
StateViewSet,
@ -856,6 +857,11 @@ urlpatterns = [
ExportIssuesEndpoint.as_view(),
name="export-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/transfer-issues/",
TransferProjectIssueEndpoint.as_view(),
name="transfer-issues",
),
## End Issues
## Issue Activity
path(

View File

@ -90,6 +90,7 @@ from .issue import (
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
TransferProjectIssueEndpoint,
)
from .auth_extended import (

View File

@ -71,6 +71,8 @@ from plane.db.models import (
IssueProperty,
Label,
IssueLink,
IssueLabel,
IssueAssignee,
IssueAttachment,
State,
IssueSubscriber,
@ -81,6 +83,8 @@ from plane.db.models import (
IssueVote,
IssueRelation,
ProjectPublicMember,
CycleIssue,
ModuleIssue,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@ -279,7 +283,8 @@ class IssueViewSet(BaseViewSet):
if group_by:
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)
@ -460,10 +465,11 @@ class UserWorkSpaceIssues(BaseAPIView):
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
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)
@ -2114,7 +2120,7 @@ class IssueRelationViewSet(BaseViewSet):
project_id=str(project_id),
current_instance=None,
)
if relation == "blocking":
return Response(
RelatedIssueSerializer(issue_relation, many=True).data,
@ -2157,6 +2163,8 @@ class IssueRelationViewSet(BaseViewSet):
.select_related("issue")
.distinct()
)
class IssueRetrievePublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
@ -2366,7 +2374,6 @@ class IssueDraftViewSet(BaseViewSet):
serializer_class = IssueFlatSerializer
model = Issue
def perform_update(self, serializer):
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = (
@ -2385,7 +2392,6 @@ class IssueDraftViewSet(BaseViewSet):
)
return super().perform_update(serializer)
def perform_destroy(self, instance):
current_instance = (
@ -2406,7 +2412,6 @@ class IssueDraftViewSet(BaseViewSet):
)
return super().perform_destroy(instance)
def get_queryset(self):
return (
Issue.objects.annotate(
@ -2432,7 +2437,6 @@ class IssueDraftViewSet(BaseViewSet):
)
)
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
try:
@ -2541,7 +2545,6 @@ class IssueDraftViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, slug, project_id):
try:
project = Project.objects.get(pk=project_id)
@ -2575,7 +2578,6 @@ class IssueDraftViewSet(BaseViewSet):
{"error": "Project was not found"}, status=status.HTTP_404_NOT_FOUND
)
def retrieve(self, request, slug, project_id, pk=None):
try:
issue = Issue.objects.get(
@ -2586,4 +2588,109 @@ class IssueDraftViewSet(BaseViewSet):
return Response(
{"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
)
# Get the default state of the new project
default_state = State.objects.filter(
workspace__slug=slug, project_id=transfer_project_id, default=True,
).first()
# Fetch all the issues
issues = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
)
# Append all the issues
bulk_issues = []
for issue in issues:
if str(issue.project_id) != str(transfer_project_id):
issue.project_id = transfer_project_id
if default_state is not None:
issue.state = default_state
bulk_issues.append(issue)
# Bulk update
moved_issues_count = Issue.objects.bulk_update(
bulk_issues, ["project_id", "state"], batch_size=100
)
# Activity logs
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
]
# Issue IDs
issue_ids = [issue.id for issue in bulk_issues]
# Transfer attachments
issue_attachments = IssueAttachment.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id)
bulk_attachment = []
for issue_attachment in issue_attachments:
issue_attachment.project_id = transfer_project_id
bulk_attachment.append(issue_attachment)
IssueAttachment.objects.bulk_update(bulk_attachment, ["project_id"], batch_size=100)
# Transfer Links
issue_links = IssueLink.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id)
bulk_links = []
for issue_link in issue_links:
issue_link.project_id = transfer_project_id
bulk_links.append(issue_link)
IssueLink.objects.bulk_update(issue_links, ["project_id"], batch_size=100)
# Delete all the other attached properties
# Delete all the issue labels in the old project
IssueLabel.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id).delete()
# Delete assignees
IssueAssignee.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id).delete()
# Delete attached cycles
CycleIssue.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id).delete()
# Delete attached modules
ModuleIssue.objects.filter(issue_id__in=issue_ids, workspace__slug=slug, project_id=project_id).delete()
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,
# )

View File

@ -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
@shared_task
def issue_activity(
@ -1265,6 +1288,7 @@ def issue_activity(
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
"issue.transfer.activity": transfer_issue_activity,
}
func = ACTIVITY_MAPPER.get(type)