diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index ea78c86a..c14e7792 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -276,6 +276,31 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) + @detail_route(methods=["POST"]) + def unset_related_userstory(self, request, **kwargs): + validator = validators.UnsetRelatedUserStoryValidator(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) + + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + related_us = get_object_or_404( + models.RelatedUserStory, + epic=epic, + user_story=user_story + ) + related_us.delete() + 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/validators.py b/taiga/projects/epics/validators.py index 674d030f..975f1e4d 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -63,3 +63,7 @@ class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsVal class SetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): us_id = serializers.IntegerField() order = serializers.IntegerField(required=False, default=10000) + + +class UnsetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 7b7f87ee..89b56bb5 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -159,6 +159,11 @@ def data(): m.private_us2 = f.UserStoryFactory(project=m.private_project2) m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + m.public_related_us = f.RelatedUserStory(epic=m.public_epic, user_story=m.public_us) + m.private_related_us1 = f.RelatedUserStory(epic=m.private_epic1, user_story=m.private_us1) + m.private_related_us2 = f.RelatedUserStory(epic=m.private_epic2, user_story=m.private_us2) + m.blocked_related_us = f.RelatedUserStory(epic=m.blocked_epic, user_story=m.blocked_us) + m.public_project.default_epic_status = m.public_epic.status m.public_project.save() m.private_project1.default_epic_status = m.private_epic1.status @@ -740,6 +745,43 @@ def test_set_related_user_story(client, data): assert results == [404, 404, 404, 451, 451] +def test_unset_related_user_story(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms + ] + + url = reverse('epics-unset-related-userstory', kwargs={"pk": data.public_epic.pk}) + edit_data = json.dumps({ + "us_id": data.public_related_us.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) + edit_data = json.dumps({ + "us_id": data.private_related_us1.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) + edit_data = json.dumps({ + "us_id": data.private_related_us2.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) + edit_data = json.dumps({ + "us_id": data.blocked_related_us.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 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 57a80d10..6d0e3107 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -133,7 +133,6 @@ def test_set_related_userstory_existing(client): 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}) @@ -148,3 +147,24 @@ def test_set_related_userstory_existing(client): related_us = models.RelatedUserStory.objects.get(id=related_us.id) assert related_us.order == 77 + + +def test_unset_related_userstory(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) + + url = reverse('epics-unset-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': 0, 'closed': 0} + + assert not models.RelatedUserStory.objects.filter(id=related_us.id).exists()