diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index f76d8c81..ea78c86a 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -32,6 +32,7 @@ from taiga.projects.models import Project, EpicStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin +from taiga.projects.userstories.models import UserStory from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -208,7 +209,7 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if validator.is_valid(): data = validator.data project = Project.objects.get(id=data["project_id"]) - self.check_permissions(request, 'bulk_create', project) + self.check_permissions(request, "bulk_create", project) if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) @@ -249,6 +250,32 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) + @detail_route(methods=["POST"]) + def set_related_userstory(self, request, **kwargs): + validator = validators.SetRelatedUserStoryValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data + epic = self.get_object() + project = epic.project + user_story = UserStory.objects.get(id=data["us_id"]) + self.check_permissions(request, "update", epic) + self.check_permissions(request, "select_related_userstory", user_story.project) + + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + obj, created = models.RelatedUserStory.objects.update_or_create( + epic=epic, + user_story=user_story, + defaults={ + "order": data["order"] + }) + epic = self.get_queryset().get(id=epic.id) + epic_serialized = self.get_serializer_class()(epic) + return response.Ok(epic_serialized.data) + + return response.BadRequest(validator.errors) + class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.EpicVotersPermission,) diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py index b009cfc2..3cd31b07 100644 --- a/taiga/projects/epics/permissions.py +++ b/taiga/projects/epics/permissions.py @@ -35,6 +35,7 @@ class EpicPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_epic') bulk_create_userstories_perms = HasProjectPerm('modify_epic') & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) + select_related_userstory_perms = HasProjectPerm('view_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') watch_perms = IsAuthenticated() & HasProjectPerm('view_epics') diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 450fefd6..674d030f 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -22,10 +22,10 @@ from taiga.base.api import serializers from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField -from taiga.projects.milestones.validators import MilestoneExistsValidator from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.validators import ProjectExistsValidator from . import models @@ -58,3 +58,8 @@ class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, validators.Validator): userstories = serializers.CharField() + + +class SetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() + order = serializers.IntegerField(required=False, default=10000) diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 1d128485..7b7f87ee 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -153,6 +153,12 @@ def data(): status__project=m.blocked_project) m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + m.public_us = f.UserStoryFactory(project=m.public_project) + m.private_us1 = f.UserStoryFactory(project=m.private_project1) + m.private_us2 = f.UserStoryFactory(project=m.private_project2) + m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + m.public_project.default_epic_status = m.public_epic.status m.public_project.save() m.private_project1.default_epic_status = m.private_epic1.status @@ -692,6 +698,48 @@ def test_bulk_create_related_userstories(client, data): assert results == [404, 404, 404, 451, 451] +def test_set_related_user_story(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.public_epic.pk}) + edit_data = json.dumps({ + "us_id": data.public_us.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) + edit_data = json.dumps({ + "us_id": data.private_us1.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) + edit_data = json.dumps({ + "us_id": data.private_us2.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) + edit_data = json.dumps({ + "us_id": data.blocked_us.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 451, 451] + + def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index b766fbf1..6f0df014 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -26,6 +26,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.epics import services +from taiga.projects.epics import models from .. import factories as f @@ -106,3 +107,46 @@ def test_bulk_create_related_userstories(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 2, 'closed': 0} + + +def test_set_related_userstory(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + + data = { + "us_id": us.id + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} + + +def test_set_related_userstory_existing(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + + data = { + "us_id": us.id, + "order": 77 + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} + + related_us = models.RelatedUserStory.objects.get(id=related_us.id) + assert related_us.order == 77