Skip to content

Commit

Permalink
Refactoring bulk update order API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
superalex authored and bameda committed Jul 28, 2016
1 parent 4c84aaa commit f66b4c9
Show file tree
Hide file tree
Showing 18 changed files with 467 additions and 117 deletions.
3 changes: 2 additions & 1 deletion settings/common.py
Expand Up @@ -437,7 +437,8 @@
"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"
Expand Down
2 changes: 1 addition & 1 deletion taiga/base/middleware/cors.py
Expand Up @@ -25,7 +25,7 @@
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",
Expand Down
31 changes: 21 additions & 10 deletions taiga/base/utils/db.py
Expand Up @@ -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

Expand All @@ -26,6 +27,7 @@

import re


def get_object_or_none(klass, *args, **kwargs):
"""
Uses get() to return an object, or None if the object does not exist.
Expand Down Expand Up @@ -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):
Expand Down
15 changes: 0 additions & 15 deletions taiga/projects/issues/services.py
Expand Up @@ -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
#####################################################
Expand Down
1 change: 1 addition & 0 deletions taiga/projects/services/__init__.py
Expand Up @@ -27,6 +27,7 @@
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

Expand Down
59 changes: 50 additions & 9 deletions taiga/projects/services/bulk_update_order.py
Expand Up @@ -24,25 +24,66 @@
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"]

from taiga.base.utils import db
apply_order_updates(memberships_orders, new_memberships_orders)

db.update_in_bulk_with_ids(membership_ids, new_order_values, model=models.Membership)
from taiga.base.utils import db
db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership)


@transaction.atomic
Expand Down
91 changes: 84 additions & 7 deletions taiga/projects/tasks/api.py
Expand Up @@ -25,12 +25,15 @@
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
Expand Down Expand Up @@ -104,16 +107,74 @@ def pre_conditions_on_save(self, obj):
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)
Expand Down Expand Up @@ -223,12 +284,28 @@ def _bulk_update_order(self, order_field, request, **kwargs):
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)

return response.NoContent()
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)

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):
Expand Down
32 changes: 21 additions & 11 deletions taiga/projects/tasks/services.py
Expand Up @@ -27,6 +27,7 @@

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
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions taiga/projects/tasks/validators.py
Expand Up @@ -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)

0 comments on commit f66b4c9

Please sign in to comment.