From 45bb5c4d9bfd045d6496e7d645ba6ef7f2d1efde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 26 Jan 2017 20:30:17 +0100 Subject: [PATCH] Fix Issue #5910: Create validator for assign_to attr --- taiga/projects/epics/validators.py | 6 +- taiga/projects/issues/validators.py | 3 +- taiga/projects/mixins/validators.py | 21 ++++ taiga/projects/tasks/validators.py | 4 +- taiga/projects/userstories/validators.py | 7 +- tests/factories.py | 1 + tests/integration/test_epics.py | 118 ++++++++++++++++++++++ tests/integration/test_issues.py | 120 ++++++++++++++++++++++- tests/integration/test_tasks.py | 118 ++++++++++++++++++++++ tests/integration/test_userstories.py | 117 ++++++++++++++++++++++ 10 files changed, 506 insertions(+), 9 deletions(-) create mode 100644 taiga/projects/mixins/validators.py diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 7ed00481..f5d71ab2 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.mixins.validators import AssignedToValidator 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 @@ -39,7 +39,8 @@ class EpicExistsValidator: return attrs -class EpicValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): +class EpicValidator(AssignedToValidator, WatchersValidator, EditableWatchedResourceSerializer, + validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) @@ -61,7 +62,6 @@ class CreateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsVa bulk_userstories = serializers.CharField() - class EpicRelatedUserStoryValidator(validators.ModelValidator): class Meta: model = models.RelatedUserStory diff --git a/taiga/projects/issues/validators.py b/taiga/projects/issues/validators.py index 4c900c15..5052fb54 100644 --- a/taiga/projects/issues/validators.py +++ b/taiga/projects/issues/validators.py @@ -19,6 +19,7 @@ from taiga.base.api import serializers from taiga.base.api import validators from taiga.base.fields import PgArrayField +from taiga.projects.mixins.validators import AssignedToValidator from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField @@ -27,7 +28,7 @@ from taiga.projects.validators import ProjectExistsValidator from . import models -class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer, +class IssueValidator(AssignedToValidator, WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) diff --git a/taiga/projects/mixins/validators.py b/taiga/projects/mixins/validators.py new file mode 100644 index 00000000..89372432 --- /dev/null +++ b/taiga/projects/mixins/validators.py @@ -0,0 +1,21 @@ +from django.utils.translation import ugettext as _ +from taiga.base.exceptions import ValidationError +from taiga.projects.models import Membership + + +class AssignedToValidator: + def validate_assigned_to(self, attrs, source): + assigned_to = attrs[source] + project = (attrs.get("project", None) or + getattr(self.object, "project", None)) + + if assigned_to and project: + filters = { + "project_id": project.id, + "user_id": assigned_to.id + } + + if not Membership.objects.filter(**filters).exists(): + raise ValidationError(_("The user must be a project member.")) + + return attrs diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index b9061bde..d7498cc6 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -23,6 +23,7 @@ from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField from taiga.projects.milestones.models import Milestone +from taiga.projects.mixins.validators import AssignedToValidator from taiga.projects.models import TaskStatus from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator @@ -33,7 +34,8 @@ from taiga.projects.validators import ProjectExistsValidator from . import models -class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): +class TaskValidator(AssignedToValidator, WatchersValidator, EditableWatchedResourceSerializer, + validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index e20a704e..cf901d10 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -23,7 +23,9 @@ from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField from taiga.base.fields import PickledObjectField +from taiga.base.utils import json from taiga.projects.milestones.models import Milestone +from taiga.projects.mixins.validators import AssignedToValidator from taiga.projects.models import UserStoryStatus from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator @@ -33,8 +35,6 @@ from taiga.projects.validators import ProjectExistsValidator from . import models -import json - class UserStoryExistsValidator: def validate_us_id(self, attrs, source): @@ -55,7 +55,8 @@ class RolePointsField(serializers.WritableField): return json.loads(obj) -class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): +class UserStoryValidator(AssignedToValidator, WatchersValidator, + EditableWatchedResourceSerializer, validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) points = RolePointsField(source="role_points", required=False) diff --git a/tests/factories.py b/tests/factories.py index 55dafd87..5564cc55 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -707,6 +707,7 @@ def create_project(**kwargs): project.default_issue_type = IssueTypeFactory.create(project=project) project.default_us_status = UserStoryStatusFactory.create(project=project) project.default_task_status = TaskStatusFactory.create(project=project) + project.default_epic_status = EpicStatusFactory.create(project=project) project.save() diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index a95f1c7c..64d3edb7 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -25,8 +25,10 @@ from unittest import mock from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.epics import services from taiga.projects.epics import models +from taiga.projects.occ import OCCResourceMixin from .. import factories as f @@ -162,3 +164,119 @@ def test_unset_related_userstory(client): response = client.delete(url) assert response.status_code == 204 assert not models.RelatedUserStory.objects.filter(id=related_us.id).exists() + + +def test_api_validator_assigned_to_when_update_epics(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + epic = f.create_epic(project=project, owner=project.owner, status=project.epic_statuses.all()[0]) + + url = reverse('epics-detail', kwargs={"pk": epic.pk}) + + # assign + data = { + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + + +def test_api_validator_assigned_to_when_create_epics(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + url = reverse('epics-list') + + # assign + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "subject": "test", + "project": project.id, + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400, response.data diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index b2055bc4..5fc58827 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -28,8 +28,10 @@ from unittest import mock from django.core.urlresolvers import reverse -from taiga.projects.issues import services, models from taiga.base.utils import json +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.issues import services, models +from taiga.projects.occ import OCCResourceMixin from .. import factories as f @@ -592,3 +594,119 @@ def test_custom_fields_csv_generation(): assert row[23] == attr.name row = next(reader) assert row[23] == "val1" + + +def test_api_validator_assigned_to_when_update_issues(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + issue = f.create_issue(project=project, owner=project.owner) + + url = reverse('issues-detail', kwargs={"pk": issue.pk}) + + # assign + data = { + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + + +def test_api_validator_assigned_to_when_create_issues(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + url = reverse('issues-list') + + # assign + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "subject": "test", + "project": project.id, + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400, response.data diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index cdda8375..2e9ae356 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -29,6 +29,8 @@ from unittest import mock from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin from taiga.projects.tasks import services from .. import factories as f @@ -861,3 +863,119 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + +def test_api_validator_assigned_to_when_update_tasks(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + task = f.create_task(project=project, milestone__project=project, user_story=None, owner=project.owner) + + url = reverse('tasks-detail', kwargs={"pk": task.pk}) + + # assign + data = { + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + + +def test_api_validator_assigned_to_when_create_tasks(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + url = reverse('tasks-list') + + # assign + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "subject": "test", + "project": project.id, + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400, response.data diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 69d6af4a..13e1df8f 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -28,6 +28,8 @@ from unittest import mock from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin from taiga.projects.userstories import services, models from .. import factories as f @@ -1021,3 +1023,118 @@ def test_get_user_stories_including_attachments(client): response = client.get(url) assert response.status_code == 200 assert len(response.data[0].get("attachments")) == 1 + + +def test_api_validator_assigned_to_when_update_userstories(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + userstory = f.create_userstory(project=project, owner=project.owner, status=project.us_statuses.all()[0]) + + url = reverse('userstories-detail', kwargs={"pk": userstory.pk}) + + # assign + data = { + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + + +def test_api_validator_assigned_to_when_create_userstories(client): + project = f.create_project(anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) + project_member_owner = f.MembershipFactory.create(project=project, + user=project.owner, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_member = f.MembershipFactory.create(project=project, + is_admin=True, + role__project=project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project_no_member = f.MembershipFactory.create(is_admin=True) + + url = reverse('userstories-list') + + # assign + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == project_member.user.id + + # unassign + data = { + "subject": "test", + "project": project.id, + "assigned_to": None, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + assert "assigned_to" in response.data + assert response.data["assigned_to"] == None + + # assign to invalid user + data = { + "subject": "test", + "project": project.id, + "assigned_to": project_no_member.user.id, + } + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400, response.data