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