diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 2071ce84..172ac08e 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -33,6 +33,8 @@ from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.tasks.validators import UpdateMilestoneBulkValidator as \ TasksUpdateMilestoneValidator +from taiga.projects.issues.validators import UpdateMilestoneBulkValidator as \ + IssuesUpdateMilestoneValidator from . import serializers from . import services @@ -188,6 +190,27 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, return response.NoContent() + @detail_route(methods=["POST"]) + def move_issues_to_sprint(self, request, pk=None, **kwargs): + milestone = get_object_or_404(models.Milestone, pk=pk) + + self.check_permissions(request, "move_related_items", milestone) + + validator = IssuesUpdateMilestoneValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) + + data = validator.data + project = get_object_or_404(Project, pk=data["project_id"]) + milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"]) + + if data["bulk_issues"]: + self.check_permissions(request, "move_issues_to_sprint", project) + services.update_issues_milestone_in_bulk(data["bulk_issues"], milestone_result) + services.snapshot_issues_in_bulk(data["bulk_issues"], request.user) + + return response.NoContent() + class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): permission_classes = (permissions.MilestoneWatchersPermission,) diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index a4454c19..d33d7b0e 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -36,6 +36,7 @@ class MilestonePermission(TaigaResourcePermission): move_related_items_perms = HasProjectPerm('modify_milestone') move_uss_to_sprint_perms = HasProjectPerm('modify_us') move_tasks_to_sprint_perms = HasProjectPerm('modify_task') + move_issues_to_sprint_perms = HasProjectPerm('modify_issue') class MilestoneWatchersPermission(TaigaResourcePermission): diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py index 017b16c5..041fa0a0 100644 --- a/taiga/projects/milestones/services.py +++ b/taiga/projects/milestones/services.py @@ -20,6 +20,7 @@ from taiga.base.utils import db from taiga.events import events from taiga.projects.history.services import take_snapshot from taiga.projects.services import apply_order_updates +from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -142,3 +143,37 @@ def snapshot_tasks_in_bulk(bulk_data, user): take_snapshot(task, user=user) except Task.DoesNotExist: pass + + +def update_issues_milestone_in_bulk(bulk_data: list, milestone: object): + """ + Update the milestone some issues adding + `bulk_data` should be a list of dicts with the following format: + [{'task_id': }, ...] + """ + issue_milestones = {e["issue_id"]: milestone.id for e in bulk_data} + issue_ids = issue_milestones.keys() + + events.emit_event_for_ids(ids=issue_ids, + content_type="issues.issues", + projectid=milestone.project.pk) + + issues_instance_list = [] + issues_values = [] + for issue_id in issue_ids: + issue = Issue.objects.get(pk=issue_id) + issues_instance_list.append(issue) + issues_values.append({'milestone_id': milestone.id}) + + db.update_in_bulk(issues_instance_list, issues_values) + + return issue_milestones + + +def snapshot_issues_in_bulk(bulk_data, user): + for issue_data in bulk_data: + try: + issue = Issue.objects.get(pk=issue_data['issue_id']) + take_snapshot(issue, user=user) + except Issue.DoesNotExist: + pass diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index ce94ceb5..1edf9d0a 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -333,3 +333,61 @@ def test_api_move_tasks_to_another_sprint_close_previous(client): assert project.milestones.get(id=milestone1.id).tasks.count() == 1 assert project.milestones.get(id=milestone2.id).tasks.count() == 1 assert project.milestones.get(id=milestone1.id).closed + + +def test_api_move_issues_to_another_sprint(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, + is_admin=True) + milestone1 = f.MilestoneFactory.create(project=project) + milestone2 = f.MilestoneFactory.create(project=project) + + issue1 = f.create_issue(project=project, milestone=milestone1) + issue2 = f.create_issue(project=project, milestone=milestone1) + + assert project.milestones.get(id=milestone1.id).issues.count() == 2 + + url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk}) + data = { + "project_id": project.id, + "milestone_id": milestone2.id, + "bulk_issues": [{"issue_id": issue2.id, "order": 2}] + } + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone1.id).issues.count() == 1 + assert project.milestones.get(id=milestone2.id).issues.count() == 1 + + +def test_api_move_issues_to_another_sprint_close_previous(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, + is_admin=True) + milestone1 = f.MilestoneFactory.create(project=project) + milestone2 = f.MilestoneFactory.create(project=project) + + closed_status = f.IssueStatusFactory.create(project=project, + is_closed=True) + issue1 = f.create_issue(project=project, milestone=milestone1, + status=closed_status) + issue2 = f.create_issue(project=project, milestone=milestone1) + + assert project.milestones.get(id=milestone1.id).issues.count() == 2 + + url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk}) + data = { + "project_id": project.id, + "milestone_id": milestone2.id, + "bulk_issues": [{"issue_id": issue2.id, "order": 2}] + } + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone1.id).issues.count() == 1 + assert project.milestones.get(id=milestone2.id).issues.count() == 1 + assert project.milestones.get(id=milestone1.id).closed