Merge pull request #793 from taigaio/refactoring-bulk-update-order-api-calls
Refactoring bulk update order API callsremotes/origin/issue/4795/notification_even_they_are_disabled
commit
888162af63
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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):
|
||||
|
|
|
@ -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 id>, <new issue order value>), ...]
|
||||
"""
|
||||
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
|
||||
#####################################################
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>, {<field>: <value>, ...}), ...]
|
||||
[{'project_id': <value>, 'order': <value>}, ...]
|
||||
"""
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>, {<field>: <value>, ...}), ...]
|
||||
[{'task_id': <value>, 'order': <value>}, ...]
|
||||
"""
|
||||
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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
[(<user story id>, {<field>: <value>, ...}), ...]
|
||||
[{'us_id': <value>, 'order': <value>}, ...]
|
||||
"""
|
||||
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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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}],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in New Issue