Merge pull request #730 from taigaio/adding-endpoint-to-bulk-update-milestone-for-user-stories

Adding endpoint to bulk updating the milestone for user stories
remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-05-26 12:15:39 +02:00
commit d463a1e8af
5 changed files with 185 additions and 54 deletions

View File

@ -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,)

View File

@ -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')

View File

@ -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

View File

@ -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:

View File

@ -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()