From b2724723963fede753a8a1abfbf464fa15e0a2f8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 24 May 2016 09:10:22 +0200 Subject: [PATCH] Adding endpoint to bulk updating the milestone for user stories --- taiga/projects/userstories/api.py | 122 +++++++++++++--------- taiga/projects/userstories/permissions.py | 1 + taiga/projects/userstories/serializers.py | 33 +++++- taiga/projects/userstories/services.py | 15 +++ tests/integration/test_userstories.py | 68 ++++++++++++ 5 files changed, 185 insertions(+), 54 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 745268cd..1b0b8035 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -17,7 +17,6 @@ from contextlib import suppress - from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ @@ -37,6 +36,7 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersVi from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.models import Project, UserStoryStatus +from taiga.projects.milestones.models import Milestone from taiga.projects.history.services import take_snapshot from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin @@ -86,32 +86,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return serializers.UserStorySerializer - 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) - self.check_permissions(request, "destroy", self.object) - self.check_permissions(request, "create", new_project) - - sprint_id = request.DATA.get('milestone', None) - if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: - request.DATA['milestone'] = None - - status_id = request.DATA.get('status', None) - if status_id is not None: - try: - old_status = self.object.project.us_statuses.get(pk=status_id) - new_status = new_project.us_statuses.get(slug=old_status.slug) - request.DATA['status'] = new_status.id - except UserStoryStatus.DoesNotExist: - request.DATA['status'] = new_project.default_us_status.id - except Project.DoesNotExist: - return response.BadRequest(_("The project doesn't exist")) - - return super().update(request, *args, **kwargs) - def get_queryset(self): qs = super().get_queryset() qs = qs.prefetch_related("role_points", @@ -126,6 +100,17 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi qs = self.attach_votes_attrs_to_queryset(qs) return self.attach_watchers_attrs_to_queryset(qs) + def pre_conditions_on_save(self, obj): + super().pre_conditions_on_save(obj) + + if obj.milestone and obj.milestone.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this sprint " + "to this user story.")) + + if obj.status and obj.status.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this status " + "to this user story.")) + def pre_save(self, obj): # This is very ugly hack, but having # restframework is the only way to do it. @@ -155,16 +140,49 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi super().post_save(obj, created) - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) + @transaction.atomic + def create(self, *args, **kwargs): + response = super().create(*args, **kwargs) - if obj.milestone and obj.milestone.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this sprint " - "to this user story.")) + # Added comment to the origin (issue) + if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: + self.object.generated_from_issue.save() - if obj.status and obj.status.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this status " - "to this user story.")) + comment = _("Generating the user story #{ref} - {subject}") + comment = comment.format(ref=self.object.ref, subject=self.object.subject) + history = take_snapshot(self.object.generated_from_issue, + comment=comment, + user=self.request.user) + + self.send_notifications(self.object.generated_from_issue, history) + + return response + + 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) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + sprint_id = request.DATA.get('milestone', None) + if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + request.DATA['milestone'] = None + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.us_statuses.get(pk=status_id) + new_status = new_project.us_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except UserStoryStatus.DoesNotExist: + request.DATA['status'] = new_project.default_us_status.id + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) @list_route(methods=["GET"]) def filters_data(self, request, *args, **kwargs): @@ -224,6 +242,23 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return response.Ok(user_stories_serialized.data) return response.BadRequest(serializer.errors) + @list_route(methods=["POST"]) + def bulk_update_milestone(self, request, **kwargs): + serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + project = get_object_or_404(Project, pk=data["project_id"]) + milestone = get_object_or_404(Milestone, pk=data["milestone_id"]) + + self.check_permissions(request, "bulk_update_milestone", project) + + services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone) + services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) + + return response.NoContent() + def _bulk_update_order(self, order_field, request, **kwargs): serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) if not serializer.is_valid(): @@ -255,23 +290,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def bulk_update_kanban_order(self, request, **kwargs): return self._bulk_update_order("kanban_order", request, **kwargs) - @transaction.atomic - def create(self, *args, **kwargs): - response = super().create(*args, **kwargs) - - # Added comment to the origin (issue) - if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: - self.object.generated_from_issue.save() - - comment = _("Generating the user story #{ref} - {subject}") - comment = comment.format(ref=self.object.ref, subject=self.object.subject) - history = take_snapshot(self.object.generated_from_issue, - comment=comment, - user=self.request.user) - - self.send_notifications(self.object.generated_from_issue, history) - - return response class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.UserStoryVotersPermission,) diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index c91ef2a7..51c6f01a 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -34,6 +34,7 @@ class UserStoryPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_update_order_perms = HasProjectPerm('modify_us') + bulk_update_milestone_perms = HasProjectPerm('modify_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_us') downvote_perms = IsAuthenticated() & HasProjectPerm('view_us') watch_perms = IsAuthenticated() & HasProjectPerm('view_us') diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 0931cae8..0a856e8a 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -17,6 +17,7 @@ from django.apps import apps from taiga.base.api import serializers +from taiga.base.api.utils import get_object_or_404 from taiga.base.fields import TagsField from taiga.base.fields import PickledObjectField from taiga.base.fields import PgArrayField @@ -24,8 +25,9 @@ from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.utils import json from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator -from taiga.projects.validators import UserStoryStatusExistsValidator +from taiga.projects.models import Project +from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicUserStoryStatusSerializer @@ -142,3 +144,30 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer): project_id = serializers.IntegerField() bulk_stories = _UserStoryOrderBulkSerializer(many=True) + + +## Milestone bulk serializers + +class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer): + us_id = serializers.IntegerField() + + +class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer): + project_id = serializers.IntegerField() + milestone_id = serializers.IntegerField() + bulk_stories = _UserStoryMilestoneBulkSerializer(many=True) + + def validate(self, data): + """ + All the userstories and the milestone are from the same project + """ + user_story_ids = [us["us_id"] for us in data["bulk_stories"]] + project = get_object_or_404(Project, pk=data["project_id"]) + + if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): + raise serializers.ValidationError("all the user stories must be from the same project") + + if project.milestones.filter(id=data["milestone_id"]).count() != 1: + raise serializers.ValidationError("the milestone isn't valid for the project") + + return data diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index b0b881a6..c1884228 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -91,6 +91,21 @@ def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object): db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory) +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) + + 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) + + def snapshot_userstories_in_bulk(bulk_data, user): user_story_ids = [] for us_data in bulk_data: diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 8081eefd..bcb0a618 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -164,6 +164,74 @@ def test_api_update_orders_in_bulk(client): assert response3.status_code == 204, response3.data +def test_api_update_milestone_in_bulk(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + milestone = f.MilestoneFactory.create(project=project) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + assert project.milestones.get(id=milestone.id).user_stories.count() == 0 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone.id).user_stories.count() == 2 + + +def test_api_update_milestone_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + m1 = f.MilestoneFactory.create(project=project) + m2 = f.MilestoneFactory.create() + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": m2.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert response.data["non_field_errors"][0] == "the milestone isn't valid for the project" + + +def test_api_update_milestone_in_bulk_invalid_userstories(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory() + milestone = f.MilestoneFactory.create(project=project) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert response.data["non_field_errors"][0] == "all the user stories must be from the same project" + + def test_update_userstory_points(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create()