diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index d3727b78..ed7cd208 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import get_object_or_404 from taiga.base import filters, response from taiga.base import exceptions as exc @@ -79,3 +80,27 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, return response.Ok(tasks_serialized.data) return response.BadRequest(serializer.errors) + + def _bulk_update_order(self, order_field, request, **kwargs): + serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + project = get_object_or_404(Project, pk=data["project_id"]) + + self.check_permissions(request, "bulk_update_order", project) + services.update_tasks_order_in_bulk(data["bulk_tasks"], + project=project, + field=order_field) + services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user) + + return response.NoContent() + + @list_route(methods=["POST"]) + def bulk_update_taskboard_order(self, request, **kwargs): + return self._bulk_update_order("taskboard_order", request, **kwargs) + + @list_route(methods=["POST"]) + def bulk_update_us_order(self, request, **kwargs): + return self._bulk_update_order("us_order", request, **kwargs) diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 46cde396..1983d7a6 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -27,3 +27,4 @@ class TaskPermission(TaigaResourcePermission): destroy_perms = HasProjectPerm('delete_task') list_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_task') + bulk_update_order_perms = HasProjectPerm('modify_task') diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index 2dcf6097..fe71186b 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -18,9 +18,9 @@ from rest_framework import serializers from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator +from taiga.projects.validators import ProjectExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.userstories.validators import UserStoryExistsValidator +from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.notifications.validators import WatchersValidator from . import models @@ -70,10 +70,21 @@ class NeighborTaskSerializer(serializers.ModelSerializer): depth = 0 -class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, TaskStatusExistsValidator, - UserStoryExistsValidator, Serializer): +class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, + TaskExistsValidator, Serializer): project_id = serializers.IntegerField() sprint_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() + +## Order bulk serializers + +class _TaskOrderBulkSerializer(TaskExistsValidator, Serializer): + task_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, Serializer): + project_id = serializers.IntegerField() + bulk_tasks = _TaskOrderBulkSerializer(many=True) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 6e9e571c..379d1321 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -15,6 +15,8 @@ # along with this program. If not, see . from taiga.base.utils import db, text +from taiga.projects.history.services import take_snapshot +from taiga.events import events from . import models @@ -43,3 +45,33 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi tasks = get_tasks_from_bulk(bulk_data, **additional_fields) db.save_in_bulk(tasks, callback, precall) return tasks + + +def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object): + """ + Update the order of some tasks. + `bulk_data` should be a list of tuples with the following format: + + [(, {: , ...}), ...] + """ + task_ids = [] + new_order_values = [] + for task_data in bulk_data: + task_ids.append(task_data["task_id"]) + new_order_values.append({field: task_data["order"]}) + + events.emit_event_for_ids(ids=task_ids, + content_type="tasks.task", + projectid=project.pk) + + db.update_in_bulk_with_ids(task_ids, new_order_values, model=models.Task) + + +def snapshot_tasks_in_bulk(bulk_data, user): + task_ids = [] + for task_data in bulk_data: + try: + task = models.Task.objects.get(pk=task_data['task_id']) + take_snapshot(task, user=user) + except models.UserStory.DoesNotExist: + pass diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py new file mode 100644 index 00000000..c7f1293b --- /dev/null +++ b/taiga/projects/tasks/validators.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers + +from . import models + + +class TaskExistsValidator: + def validate_task_id(self, attrs, source): + value = attrs[source] + if not models.Task.objects.filter(pk=value).exists(): + msg = _("There's no task with that id") + raise serializers.ValidationError(msg) + return attrs diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index f95b0007..166ddd26 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -110,8 +110,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi return response.Ok(user_stories_serialized.data) return response.BadRequest(serializer.errors) - @list_route(methods=["POST"]) - def bulk_update_backlog_order(self, request, **kwargs): + def _bulk_update_order(self, order_field, request, **kwargs): serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) if not serializer.is_valid(): return response.BadRequest(serializer.errors) @@ -122,42 +121,22 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi self.check_permissions(request, "bulk_update_order", project) services.update_userstories_order_in_bulk(data["bulk_stories"], project=project, - field="backlog_order") + field=order_field) services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) return response.NoContent() + @list_route(methods=["POST"]) + def bulk_update_backlog_order(self, request, **kwargs): + return self._bulk_update_order("backlog_order", request, **kwargs) + @list_route(methods=["POST"]) def bulk_update_sprint_order(self, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - project = get_object_or_404(Project, pk=data["project_id"]) - - self.check_permissions(request, "bulk_update_order", project) - services.update_userstories_order_in_bulk(data["bulk_stories"], - project=project, - field="sprint_order") - services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) - return response.NoContent() + return self._bulk_update_order("sprint_order", request, **kwargs) @list_route(methods=["POST"]) def bulk_update_kanban_order(self, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - project = get_object_or_404(Project, pk=data["project_id"]) - - self.check_permissions(request, "bulk_update_order", project) - services.update_userstories_order_in_bulk(data["bulk_stories"], - project=project, - field="kanban_order") - services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) - return response.NoContent() + return self._bulk_update_order("kanban_order", request, **kwargs) @transaction.atomic def create(self, *args, **kwargs): diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index d18bd1a8..924e8e99 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -87,3 +87,27 @@ def test_api_create_invalid_task(client): client.login(us.owner) response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + + +def test_api_update_order_in_bulk(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 204, response1.data + assert response2.status_code == 204, response2.data