diff --git a/settings/common.py b/settings/common.py
index 333d310a..7f3f8cbd 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -437,7 +437,8 @@ APP_EXTRA_EXPOSE_HEADERS = [
"taiga-info-total-opened-milestones",
"taiga-info-total-closed-milestones",
"taiga-info-project-memberships",
- "taiga-info-project-is-private"
+ "taiga-info-project-is-private",
+ "taiga-info-order-updated"
]
DEFAULT_PROJECT_TEMPLATE = "scrum"
diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py
index c7e2c615..3f5cbd38 100644
--- a/taiga/base/middleware/cors.py
+++ b/taiga/base/middleware/cors.py
@@ -25,7 +25,7 @@ COORS_ALLOWED_METHODS = ["POST", "GET", "OPTIONS", "PUT", "DELETE", "PATCH", "HE
COORS_ALLOWED_HEADERS = ["content-type", "x-requested-with",
"authorization", "accept-encoding",
"x-disable-pagination", "x-lazy-pagination",
- "x-host", "x-session-id"]
+ "x-host", "x-session-id", "set-orders"]
COORS_ALLOWED_CREDENTIALS = True
COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by",
"x-pagination-current", "x-pagination-next", "x-pagination-prev",
diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py
index 6569069d..9769abee 100644
--- a/taiga/base/utils/db.py
+++ b/taiga/base/utils/db.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from django.contrib.contenttypes.models import ContentType
+from django.db import connection
from django.db import transaction
from django.shortcuts import _get_queryset
@@ -26,6 +27,7 @@ from . import functions
import re
+
def get_object_or_none(klass, *args, **kwargs):
"""
Uses get() to return an object, or None if the object does not exist.
@@ -119,19 +121,28 @@ def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
callback(instance)
-def update_in_bulk_with_ids(ids, list_of_new_values, model):
+def update_attr_in_bulk_for_ids(values, attr, model):
"""Update a table using a list of ids.
- :params ids: List of ids.
- :params new_values: List of dicts or duples where each dict/duple is the new data corresponding
- to the instance in the same index position as the dict.
- :param model: Model of the ids.
+ :params values: Dict of new values where the key is the pk of the element to update.
+ :params attr: attr to update
+ :params model: Model of the ids.
"""
- tn = get_typename_for_model_class(model)
- for id, new_values in zip(ids, list_of_new_values):
- key = "{0}:{1}".format(tn, id)
- with advisory_lock(key) as acquired_key_lock:
- model.objects.filter(id=id).update(**new_values)
+ values = [str((id, order)) for id, order in values.items()]
+ sql = """
+ UPDATE {tbl}
+ SET {attr}=update_values.column2
+ FROM (
+ VALUES
+ {values}
+ ) AS update_values
+ WHERE {tbl}.id=update_values.column1;
+ """.format(tbl=model._meta.db_table,
+ values=', '.join(values),
+ attr=attr)
+
+ cursor = connection.cursor()
+ cursor.execute(sql)
def to_tsquery(term):
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 56790e82..782a184c 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -72,21 +72,6 @@ def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_f
return issues
-def update_issues_order_in_bulk(bulk_data):
- """Update the order of some issues.
-
- `bulk_data` should be a list of tuples with the following format:
-
- [(, ), ...]
- """
- issue_ids = []
- new_order_values = []
- for issue_id, new_order_value in bulk_data:
- issue_ids.append(issue_id)
- new_order_values.append({"order": new_order_value})
- db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue)
-
-
#####################################################
# CSV
#####################################################
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index a115275b..e8a93623 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -27,6 +27,7 @@ from .bulk_update_order import bulk_update_issue_status_order
from .bulk_update_order import bulk_update_task_status_order
from .bulk_update_order import bulk_update_points_order
from .bulk_update_order import bulk_update_userstory_status_order
+from .bulk_update_order import apply_order_updates
from .filters import get_all_tags
diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py
index 48e85218..4abf0e24 100644
--- a/taiga/projects/services/bulk_update_order.py
+++ b/taiga/projects/services/bulk_update_order.py
@@ -24,25 +24,66 @@ from taiga.projects import models
from contextlib import suppress
-def update_projects_order_in_bulk(bulk_data:list, field:str, user):
+def apply_order_updates(base_orders: dict, new_orders: dict):
+ """
+ `base_orders` must be a dict containing all the elements that can be affected by
+ order modifications.
+ `new_orders` must be a dict containing the basic order modifications to apply.
+
+ The result will a base_orders with the specified order changes in new_orders
+ and the extra calculated ones applied.
+ Extra order updates can be needed when moving elements to intermediate positions.
+ The elements where no order update is needed will be removed.
+ """
+ updated_order_ids = set()
+ # We will apply the multiple order changes by the new position order
+ sorted_new_orders = [(k, v) for k, v in new_orders.items()]
+ sorted_new_orders = sorted(sorted_new_orders, key=lambda e: e[1])
+
+ for new_order in sorted_new_orders:
+ old_order = base_orders[new_order[0]]
+ new_order = new_order[1]
+ for id, order in base_orders.items():
+ # When moving forward only the elements contained in the range new_order - old_order
+ # positions need to be updated
+ moving_backward = new_order <= old_order and order >= new_order and order < old_order
+ # When moving backward all the elements from the new_order position need to bee updated
+ moving_forward = new_order >= old_order and order >= new_order
+ if moving_backward or moving_forward:
+ base_orders[id] += 1
+ updated_order_ids.add(id)
+
+ # Overwritting the orders specified
+ for id, order in new_orders.items():
+ if base_orders[id] != order:
+ base_orders[id] = order
+ updated_order_ids.add(id)
+
+ # Remove not modified elements
+ removing_keys = [id for id in base_orders if id not in updated_order_ids]
+ [base_orders.pop(id, None) for id in removing_keys]
+
+
+def update_projects_order_in_bulk(bulk_data: list, field: str, user):
"""
Update the order of user projects in the user membership.
- `bulk_data` should be a list of tuples with the following format:
+ `bulk_data` should be a list of dicts with the following format:
- [(, {: , ...}), ...]
+ [{'project_id': , 'order': }, ...]
"""
- membership_ids = []
- new_order_values = []
+ memberships_orders = {m.id: getattr(m, field) for m in user.memberships.all()}
+ new_memberships_orders = {}
+
for membership_data in bulk_data:
project_id = membership_data["project_id"]
with suppress(ObjectDoesNotExist):
membership = user.memberships.get(project_id=project_id)
- membership_ids.append(membership.id)
- new_order_values.append({field: membership_data["order"]})
+ new_memberships_orders[membership.id] = membership_data["order"]
+
+ apply_order_updates(memberships_orders, new_memberships_orders)
from taiga.base.utils import db
-
- db.update_in_bulk_with_ids(membership_ids, new_order_values, model=models.Membership)
+ db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership)
@transaction.atomic
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 3dc2bd32..232d496e 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -25,12 +25,15 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-
+from taiga.base.utils import json
from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.milestones.models import Milestone
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
+from taiga.projects.userstories.models import UserStory
+
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
@@ -104,16 +107,74 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
+ """
+ Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard
+ These two methods generate a key for the task and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _us_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.us_order)
+
+ def _taskboard_order_key(self, obj):
+ return "{}-{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.status_id, obj.taskboard_order)
+
def pre_save(self, obj):
if obj.user_story:
obj.milestone = obj.user_story.milestone
if not obj.id:
obj.owner = self.request.user
+ else:
+ self._old_us_order_key = self._us_order_key(self.get_object())
+ self._old_taskboard_order_key = self._taskboard_order_key(self.get_object())
+
super().pre_save(obj)
+ def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr,
+ project, user_story=None, status=None, milestone=None):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key != order_key:
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"task_id": obj.id, "order": getattr(obj, order_attr)}]
+ for id, order in extra_orders.items():
+ data.append({"task_id": int(id), "order": order})
+
+ return services.update_tasks_order_in_bulk(data,
+ order_attr,
+ project,
+ user_story=user_story,
+ status=status,
+ milestone=milestone)
+ return {}
+
+ def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = {}
+ updated = self._reorder_if_needed(obj,
+ self._old_us_order_key,
+ self._us_order_key(obj),
+ "us_order",
+ obj.project,
+ user_story=obj.user_story)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_taskboard_order_key,
+ self._taskboard_order_key(obj),
+ "taskboard_order",
+ obj.project,
+ user_story=obj.user_story,
+ status=obj.status,
+ milestone=obj.milestone)
+ orders_updated.update(updated)
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
+ super().post_save(obj, created)
+
def update(self, request, *args, **kwargs):
self.object = self.get_object_or_none()
project_id = request.DATA.get('project', None)
+
if project_id and self.object and self.object.project.id != project_id:
try:
new_project = Project.objects.get(pk=project_id)
@@ -223,12 +284,28 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
if project.blocked_code is not None:
raise exc.Blocked(_("Blocked element"))
- services.update_tasks_order_in_bulk(data["bulk_tasks"],
- project=project,
- field=order_field)
- services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user)
+ user_story = None
+ user_story_id = data.get("user_story_id", None)
+ if user_story_id is not None:
+ user_story = get_object_or_404(UserStory, pk=user_story_id)
- return response.NoContent()
+ status = None
+ status_id = data.get("status_id", None)
+ if status_id is not None:
+ status = get_object_or_404(TaskStatus, pk=status_id)
+
+ milestone = None
+ milestone_id = data.get("milestone_id", None)
+ if milestone_id is not None:
+ milestone = get_object_or_404(Milestone, pk=milestone_id)
+
+ ret = services.update_tasks_order_in_bulk(data["bulk_tasks"],
+ order_field,
+ project,
+ user_story=user_story,
+ status=status,
+ milestone=milestone)
+ return response.Ok(ret)
@list_route(methods=["POST"])
def bulk_update_taskboard_order(self, request, **kwargs):
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index ac7a6478..055583bd 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -27,6 +27,7 @@ from django.utils.translation import ugettext as _
from taiga.base.utils import db, text
from taiga.projects.history.services import take_snapshot
+from taiga.projects.services import apply_order_updates
from taiga.projects.tasks.apps import connect_tasks_signals
from taiga.projects.tasks.apps import disconnect_tasks_signals
from taiga.events import events
@@ -73,24 +74,33 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi
return tasks
-def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object):
+def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object,
+ user_story: object=None, status: object=None, milestone: object=None):
"""
- Update the order of some tasks.
- `bulk_data` should be a list of tuples with the following format:
+ Updates the order of the tasks specified adding the extra updates needed
+ to keep consistency.
- [(, {: , ...}), ...]
+ [{'task_id': , 'order': }, ...]
"""
- 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"]})
+ tasks = project.tasks.all()
+ if user_story is not None:
+ tasks = tasks.filter(user_story=user_story)
+ if status is not None:
+ tasks = tasks.filter(status=status)
+ if milestone is not None:
+ tasks = tasks.filter(milestone=milestone)
+ task_orders = {task.id: getattr(task, field) for task in tasks}
+ new_task_orders = {e["task_id"]: e["order"] for e in bulk_data}
+ apply_order_updates(task_orders, new_task_orders)
+
+ task_ids = task_orders.keys()
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)
+ db.update_attr_in_bulk_for_ids(task_orders, field, models.Task)
+ return task_orders
def snapshot_tasks_in_bulk(bulk_data, user):
diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py
index ddb3f33b..3dd634bd 100644
--- a/taiga/projects/tasks/validators.py
+++ b/taiga/projects/tasks/validators.py
@@ -66,4 +66,7 @@ class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator):
class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
+ milestone_id = serializers.IntegerField(required=False)
+ status_id = serializers.IntegerField(required=False)
+ us_id = serializers.IntegerField(required=False)
bulk_tasks = _TaskOrderBulkValidator(many=True)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 0e718d10..143cb1ea 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -31,6 +31,7 @@ from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404
+from taiga.base.utils import json
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot
@@ -118,6 +119,21 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
raise exc.PermissionDenied(_("You don't have permissions to set this status "
"to this user story."))
+ """
+ Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard
+ These three methods generate a key for the user story and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _backlog_order_key(self, obj):
+ return "{}-{}".format(obj.project_id, obj.backlog_order)
+
+ def _kanban_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.status_id, obj.kanban_order)
+
+ def _sprint_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.milestone_id, obj.sprint_order)
+
def pre_save(self, obj):
# This is very ugly hack, but having
# restframework is the only way to do it.
@@ -129,10 +145,55 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
if not obj.id:
obj.owner = self.request.user
+ else:
+ self._old_backlog_order_key = self._backlog_order_key(self.get_object())
+ self._old_kanban_order_key = self._kanban_order_key(self.get_object())
+ self._old_sprint_order_key = self._sprint_order_key(self.get_object())
super().pre_save(obj)
+ def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr,
+ project, status=None, milestone=None):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key != order_key:
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"us_id": obj.id, "order": getattr(obj, order_attr)}]
+ for id, order in extra_orders.items():
+ data.append({"us_id": int(id), "order": order})
+
+ return services.update_userstories_order_in_bulk(data,
+ order_attr,
+ project,
+ status=status,
+ milestone=milestone)
+ return {}
+
def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = {}
+ updated = self._reorder_if_needed(obj,
+ self._old_backlog_order_key,
+ self._backlog_order_key(obj),
+ "backlog_order",
+ obj.project)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_kanban_order_key,
+ self._kanban_order_key(obj),
+ "kanban_order",
+ obj.project,
+ status=obj.status)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_sprint_order_key,
+ self._sprint_order_key(obj),
+ "sprint_order",
+ obj.project,
+ milestone=obj.milestone)
+ orders_updated.update(updated)
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
# Code related to the hack of pre_save method.
# Rather, this is the continuation of it.
if self._role_points:
@@ -180,6 +241,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
def update(self, request, *args, **kwargs):
self.object = self.get_object_or_none()
project_id = request.DATA.get('project', None)
+
if project_id and self.object and self.object.project.id != project_id:
try:
new_project = Project.objects.get(pk=project_id)
@@ -295,17 +357,26 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
+ status = None
+ status_id = data.get("status_id", None)
+ if status_id is not None:
+ status = get_object_or_404(UserStoryStatus, pk=status_id)
+
+ milestone = None
+ milestone_id = data.get("milestone_id", None)
+ if milestone_id is not None:
+ milestone = get_object_or_404(Milestone, pk=milestone_id)
self.check_permissions(request, "bulk_update_order", project)
if project.blocked_code is not None:
raise exc.Blocked(_("Blocked element"))
- services.update_userstories_order_in_bulk(data["bulk_stories"],
- project=project,
- field=order_field)
- services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
-
- return response.NoContent()
+ ret = services.update_userstories_order_in_bulk(data["bulk_stories"],
+ order_field,
+ project,
+ status=status,
+ milestone=milestone)
+ return response.Ok(ret)
@list_route(methods=["POST"])
def bulk_update_backlog_order(self, request, **kwargs):
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index 61fe52ec..f1c5d683 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -28,9 +28,9 @@ from django.utils.translation import ugettext as _
from taiga.base.utils import db, text
from taiga.projects.history.services import take_snapshot
+from taiga.projects.services import apply_order_updates
from taiga.projects.userstories.apps import connect_userstories_signals
from taiga.projects.userstories.apps import disconnect_userstories_signals
-
from taiga.events import events
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.notifications.utils import attach_watchers_to_queryset
@@ -75,24 +75,32 @@ def create_userstories_in_bulk(bulk_data, callback=None, precall=None, **additio
return userstories
-def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object):
+def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object,
+ status: object=None, milestone: object=None):
"""
- Update the order of some user stories.
- `bulk_data` should be a list of tuples with the following format:
+ Updates the order of the userstories specified adding the extra updates needed
+ to keep consistency.
+ `bulk_data` should be a list of dicts with the following format:
+ `field` is the order field used
- [(, {: , ...}), ...]
+ [{'us_id': , 'order': }, ...]
"""
- user_story_ids = []
- new_order_values = []
- for us_data in bulk_data:
- user_story_ids.append(us_data["us_id"])
- new_order_values.append({field: us_data["order"]})
+ user_stories = project.user_stories.all()
+ if status is not None:
+ user_stories = user_stories.filter(status=status)
+ if milestone is not None:
+ user_stories = user_stories.filter(milestone=milestone)
+ us_orders = {us.id: getattr(us, field) for us in user_stories}
+ new_us_orders = {e["us_id"]: e["order"] for e in bulk_data}
+ apply_order_updates(us_orders, new_us_orders)
+
+ user_story_ids = us_orders.keys()
events.emit_event_for_ids(ids=user_story_ids,
content_type="userstories.userstory",
projectid=project.pk)
-
- db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory)
+ db.update_attr_in_bulk_for_ids(us_orders, field, models.UserStory)
+ return us_orders
def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
@@ -100,14 +108,14 @@ def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
Update the milestone of some user stories.
`bulk_data` should be a list of user story ids:
"""
- user_story_ids = [us_data["us_id"] for us_data in bulk_data]
- new_milestone_values = [{"milestone": milestone.id}] * len(user_story_ids)
+ us_milestones = {e["us_id"]: milestone.id for e in bulk_data}
+ user_story_ids = us_milestones.keys()
events.emit_event_for_ids(ids=user_story_ids,
content_type="userstories.userstory",
projectid=milestone.project.pk)
- db.update_in_bulk_with_ids(user_story_ids, new_milestone_values, model=models.UserStory)
+ db.update_attr_in_bulk_for_ids(us_milestones, "milestone_id", model=models.UserStory)
def snapshot_userstories_in_bulk(bulk_data, user):
diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py
index 2d61934f..ba470456 100644
--- a/taiga/projects/userstories/validators.py
+++ b/taiga/projects/userstories/validators.py
@@ -64,7 +64,7 @@ class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, v
class Meta:
model = models.UserStory
depth = 0
- read_only_fields = ('created_date', 'modified_date', 'owner')
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
@@ -84,6 +84,8 @@ class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validato
class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
validators.Validator):
project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ milestone_id = serializers.IntegerField(required=False)
bulk_stories = _UserStoryOrderBulkValidator(many=True)
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index 4eb0c416..bf8c596d 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -659,21 +659,21 @@ def test_user_story_action_bulk_update_order(client, data):
"project_id": data.public_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
- assert results == [401, 403, 403, 204, 204]
+ assert results == [401, 403, 403, 200, 200]
post_data = json.dumps({
"bulk_stories": [{"us_id": data.private_user_story1.id, "order": 2}],
"project_id": data.private_project1.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
- assert results == [401, 403, 403, 204, 204]
+ assert results == [401, 403, 403, 200, 200]
post_data = json.dumps({
"bulk_stories": [{"us_id": data.private_user_story2.id, "order": 2}],
"project_id": data.private_project2.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
- assert results == [401, 403, 403, 204, 204]
+ assert results == [401, 403, 403, 200, 200]
post_data = json.dumps({
"bulk_stories": [{"us_id": data.blocked_user_story.id, "order": 2}],
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index 4ea78a35..5a0cc00c 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -56,15 +56,6 @@ Issue #2
db.save_in_bulk.assert_called_once_with(issues, None, None)
-def test_update_issues_order_in_bulk():
- data = [(1, 1), (2, 2)]
-
- with mock.patch("taiga.projects.issues.services.db") as db:
- services.update_issues_order_in_bulk(data)
- db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"order": 1}, {"order": 2}],
- model=models.Issue)
-
-
def test_create_issue_without_status(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index c12e1ecb..398a40a2 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -148,8 +148,8 @@ def test_api_update_order_in_bulk(client):
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
+ assert response1.status_code == 200, response1.data
+ assert response2.status_code == 200, response2.data
def test_get_invalid_csv(client):
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 10805f8e..1d3984bd 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -50,17 +50,16 @@ def test_create_userstories_in_bulk():
def test_update_userstories_order_in_bulk():
- data = [{"us_id": 1, "order": 1}, {"us_id": 2, "order": 2}]
-
- project = mock.Mock()
- project.pk = 1
+ project = f.ProjectFactory.create()
+ us1 = f.UserStoryFactory.create(project=project, backlog_order=1)
+ us2 = f.UserStoryFactory.create(project=project, backlog_order=2)
+ data = [{"us_id": us1.id, "order": 1}, {"us_id": us2.id, "order": 2}]
with mock.patch("taiga.projects.userstories.services.db") as db:
services.update_userstories_order_in_bulk(data, "backlog_order", project)
- db.update_in_bulk_with_ids.assert_called_once_with([1, 2],
- [{"backlog_order": 1},
- {"backlog_order": 2}],
- model=models.UserStory)
+ db.update_attr_in_bulk_for_ids.assert_called_once_with({us1.id: 1, us2.id: 2},
+ "backlog_order",
+ models.UserStory)
def test_create_userstory_with_watchers(client):
@@ -176,9 +175,9 @@ def test_api_update_orders_in_bulk(client):
response2 = client.json.post(url2, json.dumps(data))
response3 = client.json.post(url3, json.dumps(data))
- assert response1.status_code == 204, response1.data
- assert response2.status_code == 204, response2.data
- assert response3.status_code == 204, response3.data
+ assert response1.status_code == 200, response1.data
+ assert response2.status_code == 200, response2.data
+ assert response3.status_code == 200, response3.data
def test_api_update_milestone_in_bulk(client):
diff --git a/tests/unit/test_order_updates.py b/tests/unit/test_order_updates.py
new file mode 100644
index 00000000..f7660bf0
--- /dev/null
+++ b/tests/unit/test_order_updates.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.services import apply_order_updates
+
+
+def test_apply_order_updates_one_element_backward():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 4,
+ "e": 5,
+ "f": 6
+ }
+ new_orders = {
+ "d": 2
+ }
+ apply_order_updates(orders, new_orders)
+ assert orders == {
+ "d": 2,
+ "b": 3,
+ "c": 4
+ }
+
+
+def test_apply_order_updates_one_element_forward():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 4,
+ "e": 5,
+ "f": 6
+ }
+ new_orders = {
+ "a": 3
+ }
+ apply_order_updates(orders, new_orders)
+ assert orders == {
+ "a": 3,
+ "c": 4,
+ "d": 5,
+ "e": 6,
+ "f": 7
+ }
+
+
+def test_apply_order_updates_multiple_elements_backward():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 4,
+ "e": 5,
+ "f": 6
+ }
+ new_orders = {
+ "d": 2,
+ "e": 3
+ }
+ apply_order_updates(orders, new_orders)
+ assert orders == {
+ "d": 2,
+ "e": 3,
+ "b": 4,
+ "c": 5
+ }
+
+def test_apply_order_updates_multiple_elements_forward():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 4,
+ "e": 5,
+ "f": 6
+ }
+ new_orders = {
+ "a": 4,
+ "b": 5
+ }
+ apply_order_updates(orders, new_orders)
+ assert orders == {
+ "a": 4,
+ "b": 5,
+ "d": 6,
+ "e": 7,
+ "f": 8
+ }
+
+def test_apply_order_updates_two_elements():
+ orders = {
+ "a": 0,
+ "b": 1,
+ }
+ new_orders = {
+ "b": 0
+ }
+ apply_order_updates(orders, new_orders)
+ assert orders == {
+ "b": 0,
+ "a": 1
+ }
+
+def test_apply_order_updates_duplicated_orders():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 3,
+ "e": 3,
+ "f": 4
+ }
+ new_orders = {
+ "a": 3
+ }
+ apply_order_updates(orders, new_orders)
+ print(orders)
+ assert orders == {
+ "a": 3,
+ "c": 4,
+ "d": 4,
+ "e": 4,
+ "f": 5
+ }
+
+def test_apply_order_updates_multiple_elements_duplicated_orders():
+ orders = {
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "d": 3,
+ "e": 3,
+ "f": 4
+ }
+ new_orders = {
+ "c": 3,
+ "d": 3,
+ "a": 4
+ }
+ apply_order_updates(orders, new_orders)
+ print(orders)
+ assert orders == {
+ "c": 3,
+ "d": 3,
+ "a": 4,
+ "e": 5,
+ "f": 6
+ }
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index c564b094..2264e970 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -23,7 +23,7 @@ import django_sites as sites
import re
from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url
-from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids, to_tsquery
+from taiga.base.utils.db import save_in_bulk, update_in_bulk, to_tsquery
pytestmark = pytest.mark.django_db
@@ -82,21 +82,6 @@ def test_update_in_bulk_with_a_callback():
assert callback.call_count == 2
-def test_update_in_bulk_with_ids():
- ids = [1, 2]
- new_values = [{"field1": 1}, {"field2": 2}]
- model = mock.Mock()
-
- update_in_bulk_with_ids(ids, new_values, model)
-
- expected_calls = [
- mock.call(id=1), mock.call().update(field1=1),
- mock.call(id=2), mock.call().update(field2=2)
- ]
-
- model.objects.filter.assert_has_calls(expected_calls)
-
-
TS_QUERY_TRANSFORMATIONS = [
("1 OR 2", "1 | 2"),
("(1) 2", "( 1 ) & 2"),