diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 72aa5ca0..edda87ff 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -27,11 +27,13 @@ from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 from taiga.base.utils.db import get_object_or_none +from taiga.projects.models import Project from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin from . import serializers +from . import services from . import validators from . import models from . import permissions @@ -143,6 +145,28 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, return response.Ok(milestone_stats) + @detail_route(methods=["POST"]) + def bulk_update_items(self, request, pk=None, **kwargs): + milestone = get_object_or_404(models.Milestone, pk=pk) + + self.check_permissions(request, "bulk_update_items", milestone) + + validator = validators.UpdateMilestoneBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) + + data = validator.data + project = get_object_or_404(Project, pk=data["project_id"]) + milestone = get_object_or_404(models.Milestone, pk=data["sprint_id"]) + + print('data', validator.bulk_stories) + if data["bulk_stories"]: + self.check_permissions(request, "bulk_update_us_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() + class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): permission_classes = (permissions.MilestoneWatchersPermission,) resource_model = models.Milestone diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index 4029c846..e62d7df6 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -33,6 +33,8 @@ class MilestonePermission(TaigaResourcePermission): stats_perms = HasProjectPerm('view_milestones') watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones') unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones') + bulk_update_items_perms = HasProjectPerm('modify_milestone') + bulk_update_us_milestone_perms = HasProjectPerm('modify_us') class MilestoneWatchersPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py index af50b81e..84084cde 100644 --- a/taiga/projects/milestones/services.py +++ b/taiga/projects/milestones/services.py @@ -16,12 +16,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils import timezone +from taiga.base.utils import db +from taiga.events import events +from taiga.projects.history.services import take_snapshot +from taiga.projects.services import apply_order_updates +from taiga.projects.tasks.models import Task +from taiga.projects.userstories.models import UserStory from . import models - def calculate_milestone_is_closed(milestone): return (milestone.user_stories.all().count() > 0 and all([task.status is not None and task.status.is_closed for task in milestone.tasks.all()]) and @@ -38,3 +42,49 @@ def open_milestone(milestone): if milestone.closed: milestone.closed = False milestone.save(update_fields=["closed",]) + + +def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): + """ + Update the milestone and the milestone order of some user stories adding + the extra orders needed to keep consistency. + `bulk_data` should be a list of dicts with the following format: + [{'us_id': , 'order': }, ...] + """ + user_stories = milestone.user_stories.all() + us_orders = {us.id: getattr(us, "sprint_order") for us in user_stories} + new_us_orders = {} + for e in bulk_data: + new_us_orders[e["us_id"]] = e["order"] + # The base orders where we apply the new orders must containg all + # the values + us_orders[e["us_id"]] = e["order"] + + apply_order_updates(us_orders, new_us_orders) + + 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_attr_in_bulk_for_ids(us_milestones, "milestone_id", + model=UserStory) + db.update_attr_in_bulk_for_ids(us_orders, "sprint_order", UserStory) + + # Updating the milestone for the tasks + Task.objects.filter( + user_story_id__in=[e["us_id"] for e in bulk_data]).update( + milestone=milestone) + + return us_orders + + +def snapshot_userstories_in_bulk(bulk_data, user): + for us_data in bulk_data: + try: + us = UserStory.objects.get(pk=us_data['us_id']) + take_snapshot(us, user=user) + except UserStory.DoesNotExist: + pass diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index c37ae2b1..777d42fb 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -19,10 +19,12 @@ from django.utils.translation import ugettext as _ from taiga.base.exceptions import ValidationError +from taiga.base.api import serializers from taiga.base.api import validators -from taiga.projects.validators import DuplicatedNameInProjectValidator from taiga.projects.notifications.validators import WatchersValidator - +from taiga.projects.userstories.models import UserStory +from taiga.projects.validators import DuplicatedNameInProjectValidator +from taiga.projects.validators import ProjectExistsValidator from . import models @@ -39,3 +41,37 @@ class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, va class Meta: model = models.Milestone read_only_fields = ("id", "created_date", "modified_date") + + +# bulk validators +class _UserStoryMilestoneBulkValidator(validators.Validator): + us_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateMilestoneBulkValidator(MilestoneExistsValidator, + ProjectExistsValidator, + validators.Validator): + project_id = serializers.IntegerField() + sprint_id = serializers.IntegerField() + bulk_stories = _UserStoryMilestoneBulkValidator(many=True) + + # def validate_milestone_id(self, attrs, source): + # filters = { + # "project__id": attrs["project_id"], + # "id": attrs[source] + # } + # if not Milestone.objects.filter(**filters).exists(): + # raise ValidationError(_("The milestone isn't valid for the project")) + # return attrs + + def validate_bulk_stories(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id__in": [us["us_id"] for us in attrs[source]] + } + + if UserStory.objects.filter(**filters).count() != len(filters["id__in"]): + raise ValidationError(_("All the user stories must be from the same project")) + + return attrs diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index dbc46779..d4072877 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -202,8 +202,10 @@ def test_api_update_milestone_in_bulk_userstories(client): } client.login(project.owner) - assert project.milestones.get(id=milestone1.id).user_stories.count() == 1 + assert project.milestones.get(id=milestone1.id).user_stories.count() == 2 response = client.json.post(url, json.dumps(data)) assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone1.id).user_stories.count() == 1 assert project.milestones.get(id=milestone2.id).user_stories.count() == 1 +