diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 3dd634bd..033b72e3 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -22,23 +22,18 @@ 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.milestones.models import Milestone +from taiga.projects.models import TaskStatus 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.models import UserStory from taiga.projects.validators import ProjectExistsValidator + + from . import models -class TaskExistsValidator: - def validate_task_id(self, attrs, source): - value = attrs[source] - if not models.Task.objects.filter(pk=value).exists(): - msg = _("There's no task with that id") - raise ValidationError(msg) - return attrs - - class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) @@ -48,25 +43,72 @@ class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, valida read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') -class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, - TaskExistsValidator, validators.Validator): +class TasksBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() sprint_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() + def validate_sprint_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + filters["id"] = attrs["sprint_id"] + + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid sprint id.")) + + return attrs + + def validate_status_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + filters["id"] = attrs["status_id"] + + if not TaskStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid task status id.")) + + return attrs + + def validate_us_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + + if "sprint_id" in attrs: + filters["milestone__id"] = attrs["sprint_id"] + + filters["id"] = attrs["us_id"] + + if not UserStory.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid sprint id.")) + + return attrs + # Order bulk validators -class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator): +class _TaskOrderBulkValidator(validators.Validator): task_id = serializers.IntegerField() order = serializers.IntegerField() class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() - milestone_id = serializers.IntegerField(required=False) status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) + milestone_id = serializers.IntegerField(required=False) bulk_tasks = _TaskOrderBulkValidator(many=True) + + def validate(self, data): + filters = {"project__id": data["project_id"]} + if "status_id" in data: + filters["status__id"] = data["status_id"] + if "us_id" in data: + filters["user_story__id"] = data["us_id"] + if "milestone_id" in data: + filters["milestone__id"] = data["milestone_id"] + + filters["id__in"] = [t["task_id"] for t in data["bulk_tasks"]] + + if models.Task.objects.filter(**filters).count() != len(filters["id__in"]): + raise ValidationError(_("Invalid task ids. All tasks must belong to the same project and, " + "if it exists, to the same status, user story and/or milestone.")) + + return data diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 398a40a2..c13add00 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -85,10 +85,16 @@ def test_create_task_without_default_values(client): assert response.data['status'] == None -def test_api_create_in_bulk_with_status(client): - us = f.create_userstory() - f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) - us.project.default_task_status = f.TaskStatusFactory.create(project=us.project) +def test_api_create_in_bulk_with_status_milestone_userstory(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + url = reverse("tasks-bulk-create") data = { "bulk_tasks": "Story #1\nStory #2", @@ -98,13 +104,141 @@ def test_api_create_in_bulk_with_status(client): "status_id": us.project.default_task_status.id } - client.login(us.owner) + client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 assert response.data[0]["status"] == us.project.default_task_status.id +def test_api_create_in_bulk_with_status_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "project_id": us.project.id, + "sprint_id": us.milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert response.data[0]["status"] == us.project.default_task_status.id + + +def test_api_create_in_bulk_with_invalid_status(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + status = f.TaskStatusFactory.create() + + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory() + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_userstory_1(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory() + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_userstory_2(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": us.project.id, + "sprint_id": milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + def test_api_create_invalid_task(client): # Associated to a milestone and a user story. # But the User Story is not associated with the milestone @@ -152,6 +286,115 @@ def test_api_update_order_in_bulk(client): assert response2.status_code == 200, response2.data +def test_api_update_order_in_bulk_invalid_tasks(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task() + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project, status=task1.status) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "status_id": task1.status.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create(project=project) + task1 = f.create_task(project=project, milestone=mil1) + task2 = f.create_task(project=project, milestone=mil1) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_user_story(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + task1 = f.create_task(project=project, user_story=us1) + task2 = f.create_task(project=project, user_story=us1) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + def test_get_invalid_csv(client): url = reverse("tasks-csv")