From f66b4c9640fc8425504c6ceb26204b50a21cb116 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 20 Jul 2016 14:31:48 +0200 Subject: [PATCH] Refactoring bulk update order API calls --- settings/common.py | 3 +- taiga/base/middleware/cors.py | 2 +- taiga/base/utils/db.py | 31 ++-- taiga/projects/issues/services.py | 15 -- taiga/projects/services/__init__.py | 1 + taiga/projects/services/bulk_update_order.py | 59 ++++++- taiga/projects/tasks/api.py | 89 +++++++++- taiga/projects/tasks/services.py | 30 ++-- taiga/projects/tasks/validators.py | 3 + taiga/projects/userstories/api.py | 83 ++++++++- taiga/projects/userstories/services.py | 38 ++-- taiga/projects/userstories/validators.py | 4 +- .../test_userstories_resources.py | 6 +- tests/integration/test_issues.py | 9 - tests/integration/test_tasks.py | 4 +- tests/integration/test_userstories.py | 21 ++- tests/unit/test_order_updates.py | 165 ++++++++++++++++++ tests/unit/test_utils.py | 17 +- 18 files changed, 465 insertions(+), 115 deletions(-) create mode 100644 tests/unit/test_order_updates.py 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"),