Merge pull request #730 from taigaio/adding-endpoint-to-bulk-update-milestone-for-user-stories
Adding endpoint to bulk updating the milestone for user storiesremotes/origin/issue/4795/notification_even_they_are_disabled
commit
d463a1e8af
|
@ -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,)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue