Skip to content

Commit

Permalink
fix: generate history entries, timeline entries and webhook requests …
Browse files Browse the repository at this point in the history
…after kanban order is updated (tg-4311, tg-4340)
  • Loading branch information
bameda authored and yamila-moreno committed May 25, 2021
1 parent 15d694d commit 5a84222
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 34 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 6.2.0 (unreleased)

- issue #4503: it allows to order issues by 'ref' field
- Allows to order issues by 'ref' field (issue #tg-4503)
- Generate history entries, timeline entries and webhook requests after kanban order is updated (issues #tg-4311, #tg-4340)

## 6.1.1 (2021-05-18)

Expand Down
3 changes: 2 additions & 1 deletion taiga/projects/userstories/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ def bulk_update_kanban_order(self, request, **kwargs):
if before_userstory_id is not None:
before_userstory = get_object_or_404(models.UserStory, pk=before_userstory_id, project=project)

ret = services.update_userstories_kanban_order_in_bulk(project=project,
ret = services.update_userstories_kanban_order_in_bulk(user=request.user,
project=project,
status=status,
swimlane=swimlane,
after_userstory=after_userstory,
Expand Down
57 changes: 34 additions & 23 deletions taiga/projects/userstories/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from taiga.projects.userstories.apps import connect_userstories_signals
from taiga.projects.userstories.apps import disconnect_userstories_signals
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.users.models import User
from taiga.users.gravatar import get_gravatar_id
from taiga.users.services import get_big_photo_url, get_photo_url

Expand Down Expand Up @@ -134,7 +135,8 @@ def reset_userstories_kanban_order_in_bulk(project: Project,
projectid=project.id)


def update_userstories_kanban_order_in_bulk(project: Project,
def update_userstories_kanban_order_in_bulk(user: User,
project: Project,
status: UserStoryStatus,
bulk_userstories: List[int],
before_userstory: Optional[models.UserStory] = None,
Expand All @@ -150,6 +152,8 @@ def update_userstories_kanban_order_in_bulk(project: Project,
- `bulk_userstories` should be a list of user stories IDs
"""


# filter user stories from status and swimlane
user_stories = project.user_stories.filter(status=status)
if swimlane is not None:
Expand Down Expand Up @@ -188,33 +192,30 @@ def update_userstories_kanban_order_in_bulk(project: Project,

# prepare rest of data
total_user_stories = len(user_story_ids)

user_story_swimlane_ids = (swimlane.id if swimlane else None,) * total_user_stories
user_story_status_ids = (status.id,) * total_user_stories
user_story_kanban_orders = range(start_order, start_order + total_user_stories)

data = tuple(zip(user_story_ids,
user_story_swimlane_ids,
user_story_status_ids,
user_story_kanban_orders))

# execute query for update status, swimlane and kanban_order
# execute query for update kanban_order
sql = """
UPDATE userstories_userstory
SET swimlane_id = tmp.new_swimlane_id::BIGINT,
status_id = tmp.new_status_id::BIGINT,
kanban_order = tmp.new_kanban_order::BIGINT
FROM (VALUES %s) AS tmp (id, new_swimlane_id, new_status_id, new_kanban_order)
SET kanban_order = tmp.new_kanban_order::BIGINT
FROM (VALUES %s) AS tmp (id, new_kanban_order)
WHERE tmp.id = userstories_userstory.id
"""
with connection.cursor() as cursor:
execute_values(cursor, sql, data)

# Update is_closed attr for UserStories adn related milestones
# execute query for update status, swimlane and kanban_order
bulk_userstories_objects = project.user_stories.filter(id__in=bulk_userstories)
bulk_userstories_objects.update(status=status, swimlane=swimlane)

# Update is_closed attr for user stories and related milestones
if settings.CELERY_ENABLED:
update_open_or_close_conditions_if_status_has_been_changed.delay(bulk_userstories)
_recalculate_is_closed_condition_and_take_snapshot.delay(bulk_userstories, user.id)
else:
update_open_or_close_conditions_if_status_has_been_changed(bulk_userstories)
_recalculate_is_closed_condition_and_take_snapshot(bulk_userstories, user.id)

# Sent events of updated stories
events.emit_event_for_ids(ids=user_story_ids,
Expand All @@ -224,13 +225,26 @@ def update_userstories_kanban_order_in_bulk(project: Project,
# Generate response with modified info
res = ({
"id": id,
"swimlane": swimlane,
"status": status,
"swimlane": swimlane.id if swimlane else None,
"status": status.id,
"kanban_order": kanban_order
} for (id, swimlane, status, kanban_order) in data)
} for (id, kanban_order) in data)
return res


@app.task
def _recalculate_is_closed_condition_and_take_snapshot(userstories_ids, user_id):
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
user = None

for userstory in models.UserStory.objects.filter(id__in=userstories_ids):
recalculate_is_closed_for_userstory_and_its_milestone(userstory)
# Generate the history entity
take_snapshot(userstory, user=user)


def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some user stories adding
Expand Down Expand Up @@ -314,7 +328,7 @@ def open_userstory(us):



def _update_open_or_close_conditions_if_status_has_been_changed(userstory):
def recalculate_is_closed_for_userstory_and_its_milestone(userstory):
"""
Check and update the open or closed condition for the userstory and its milestone.
Expand All @@ -323,6 +337,7 @@ def _update_open_or_close_conditions_if_status_has_been_changed(userstory):
[2] Do not use it if the milestone has been previously updated.
"""
has_changed = False
# Update is_close attr for the user story
if calculate_userstory_is_closed(userstory):
has_changed = close_userstory(userstory)
else:
Expand All @@ -331,17 +346,13 @@ def _update_open_or_close_conditions_if_status_has_been_changed(userstory):
if has_changed and userstory.milestone_id:
from taiga.projects.milestones import services as milestone_service

# Update is_close attr for the milestone
if milestone_service.calculate_milestone_is_closed(userstory.milestone):
milestone_service.close_milestone(userstory.milestone)
else:
milestone_service.open_milestone(userstory.milestone)


@app.task
def update_open_or_close_conditions_if_status_has_been_changed(userstories_ids):
for userstory in models.UserStory.objects.filter(id__in=userstories_ids):
_update_open_or_close_conditions_if_status_has_been_changed(userstory)


#####################################################
# CSV
Expand Down
33 changes: 24 additions & 9 deletions tests/integration/test_userstories_update_kanban_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@

from taiga.base.utils import json
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.userstories import services, models


from .. import factories as f

import pytest
Expand All @@ -30,7 +33,6 @@
## Move to no swimlane
##############################


def test_api_update_orders_in_bulk_succeeds_moved_to_no_swimlane_and_to_the_begining(client):
#
# | ST1 | ST2 | | | ST1 | ST2
Expand Down Expand Up @@ -926,18 +928,23 @@ def test_api_delete_userstory_status(client):


##############################
## Close and Open USs after they are moved
## Close and Open USs after they are moved, history entries and webhooks should be created too
##############################


def test_userstories_are_closed_after_moving_in_bulk_to_a_closed_status(client):
project = f.create_project()
@mock.patch('taiga.webhooks.tasks._send_request')
def test_userstories_are_closed_after_moving_in_bulk_to_a_closed_status(send_request_mock, client, settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
status_opened = f.UserStoryStatusFactory.create(project=project, order=1, is_closed=False)
status_closed = f.UserStoryStatusFactory.create(project=project, order=2, is_closed=True)
us1 = f.create_userstory(project=project, status=status_closed, is_closed=True, kanban_order=3)
us2 = f.create_userstory(project=project, status=status_opened, is_closed=False, kanban_order=2)

assert HistoryEntry.objects.all().count() == 0
assert send_request_mock.call_count == 0

url = reverse("userstories-bulk-update-kanban-order")

data = {
Expand Down Expand Up @@ -969,16 +976,24 @@ def test_userstories_are_closed_after_moving_in_bulk_to_a_closed_status(client):
us2.refresh_from_db()
assert us1.is_closed and us1.status == status_closed
assert us2.is_closed and us2.status == status_closed
assert HistoryEntry.objects.all().count() == 2
assert send_request_mock.call_count == 2


def test_userstories_are_opened_after_moving_in_bulk_to_a_opened_status(client):
project = f.create_project()
@mock.patch('taiga.webhooks.tasks._send_request')
def test_userstories_are_opened_after_moving_in_bulk_to_a_opened_status(send_request_mock, client, settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
status_opened = f.UserStoryStatusFactory.create(project=project, order=1, is_closed=False)
status_closed = f.UserStoryStatusFactory.create(project=project, order=2, is_closed=True)
us1 = f.create_userstory(project=project, status=status_closed, is_closed=True, kanban_order=3)
us2 = f.create_userstory(project=project, status=status_opened, is_closed=False, kanban_order=2)

assert HistoryEntry.objects.all().count() == 0
assert send_request_mock.call_count == 0

url = reverse("userstories-bulk-update-kanban-order")

data = {
Expand Down Expand Up @@ -1010,5 +1025,5 @@ def test_userstories_are_opened_after_moving_in_bulk_to_a_opened_status(client):
us2.refresh_from_db()
assert not us1.is_closed and us1.status == status_opened
assert not us2.is_closed and us2.status == status_opened


assert HistoryEntry.objects.all().count() == 2
assert send_request_mock.call_count == 2

0 comments on commit 5a84222

Please sign in to comment.