Merge pull request #138 from taigaio/watchers-validation-bug

Add proper validation of watchers for issues, tasks and userstories serializers.
remotes/origin/enhancement/email-actions
Alejandro 2014-10-24 09:11:15 +02:00
commit 6f590ff0c7
7 changed files with 241 additions and 10 deletions

View File

@ -19,11 +19,12 @@ from rest_framework import serializers
from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from . import models from . import models
class IssueSerializer(serializers.ModelSerializer): class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(required=False) tags = PickleField(required=False)
is_closed = serializers.Field(source="is_closed") is_closed = serializers.Field(source="is_closed")
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")

View File

@ -0,0 +1,45 @@
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
class WatchersValidator:
def validate_watchers(self, attrs, source):
users = attrs[source]
# Try obtain a valid project
if self.object is None and "project" in attrs:
project = attrs["project"]
elif self.object:
project = self.object.project
else:
project = None
# If project is empty in all conditions, continue
# without errors, because other validator should
# validate the empty project field.
if not project:
return attrs
# Check if incoming watchers are contained
# in project members list
result = set(users).difference(set(project.members.all()))
if result:
raise serializers.ValidationError("Watchers contains invalid users")
return attrs

View File

@ -21,11 +21,12 @@ from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator
from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from . import models from . import models
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(required=False, default=[]) tags = PickleField(required=False, default=[])
comment = serializers.SerializerMethodField("get_comment") comment = serializers.SerializerMethodField("get_comment")
milestone_slug = serializers.SerializerMethodField("get_milestone_slug") milestone_slug = serializers.SerializerMethodField("get_milestone_slug")

View File

@ -22,6 +22,7 @@ from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerM
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
from . import models from . import models
@ -36,7 +37,7 @@ class RolePointsField(serializers.WritableField):
return json.loads(obj) return json.loads(obj)
class UserStorySerializer(serializers.ModelSerializer): class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
tags = PickleField(default=[], required=False) tags = PickleField(default=[], required=False)
points = RolePointsField(source="role_points", required=False) points = RolePointsField(source="role_points", required=False)
total_points = serializers.SerializerMethodField("get_total_points") total_points = serializers.SerializerMethodField("get_total_points")

View File

@ -394,8 +394,14 @@ class AttachmentFactory(Factory):
def create_issue(**kwargs): def create_issue(**kwargs):
"Create an issue and along with its dependencies." "Create an issue and along with its dependencies."
owner = kwargs.pop("owner", UserFactory()) owner = kwargs.pop("owner", None)
if owner is None:
owner = UserFactory.create()
project = kwargs.pop("project", None)
if project is None:
project = ProjectFactory.create(owner=owner) project = ProjectFactory.create(owner=owner)
defaults = { defaults = {
"project": project, "project": project,
"owner": owner, "owner": owner,
@ -412,8 +418,14 @@ def create_issue(**kwargs):
def create_task(**kwargs): def create_task(**kwargs):
"Create a task and along with its dependencies." "Create a task and along with its dependencies."
owner = kwargs.pop("owner", UserFactory()) owner = kwargs.pop("owner", None)
if not owner:
owner = UserFactory.create()
project = kwargs.pop("project", None)
if project is None:
project = ProjectFactory.create(owner=owner) project = ProjectFactory.create(owner=owner)
defaults = { defaults = {
"project": project, "project": project,
"owner": owner, "owner": owner,
@ -460,12 +472,19 @@ def create_invitation(**kwargs):
def create_userstory(**kwargs): def create_userstory(**kwargs):
"Create an user story along with its dependencies" "Create an user story along with its dependencies"
project = kwargs.pop("project", create_project())
owner = kwargs.pop("owner", None)
if not owner:
owner = UserFactory.create()
project = kwargs.pop("project", None)
if project is None:
project = ProjectFactory.create(owner=owner)
defaults = { defaults = {
"project": project, "project": project,
"owner": project.owner, "owner": owner,
"milestone": MilestoneFactory.create(project=project, owner=project.owner) "milestone": MilestoneFactory.create(project=project, owner=owner)
} }
defaults.update(kwargs) defaults.update(kwargs)

View File

@ -26,6 +26,9 @@ from .. import factories as f
from taiga.projects.notifications import services from taiga.projects.notifications import services
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.history.choices import HistoryType from taiga.projects.history.choices import HistoryType
from taiga.projects.issues.serializers import IssueSerializer
from taiga.projects.userstories.serializers import UserStorySerializer
from taiga.projects.tasks.serializers import TaskSerializer
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -246,3 +249,162 @@ def test_resource_notification_test(client, mail):
response = client.delete(url) response = client.delete(url)
assert response.status_code == 204 assert response.status_code == 204
assert len(mail.outbox) == 2 assert len(mail.outbox) == 2
def test_watchers_assignation_for_issue(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
project1 = f.ProjectFactory.create(owner=user1)
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1)
role2 = f.RoleFactory.create(project=project2)
member1 = f.MembershipFactory.create(project=project1, user=user1, role=role1)
member2 = f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version,
"watchers": [user1.pk]}
url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200, response.content
issue = f.create_issue(project=project1, owner=user1)
data = {"version": issue.version,
"watchers": [user1.pk, user2.pk]}
url = reverse("issues-detail", args=[issue.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
issue = f.create_issue(project=project1, owner=user1)
data = dict(IssueSerializer(issue).data)
data["id"] = None
data["version"] = None
data["watchers"] = [user1.pk, user2.pk]
url = reverse("issues-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
# Test the impossible case when project is not
# exists in create request, and validator works as expected
issue = f.create_issue(project=project1, owner=user1)
data = dict(IssueSerializer(issue).data)
data["id"] = None
data["watchers"] = [user1.pk, user2.pk]
data["project"] = None
url = reverse("issues-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_watchers_assignation_for_task(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
project1 = f.ProjectFactory.create(owner=user1)
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1)
role2 = f.RoleFactory.create(project=project2)
member1 = f.MembershipFactory.create(project=project1, user=user1, role=role1)
member2 = f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
task = f.create_task(project=project1, owner=user1)
data = {"version": task.version,
"watchers": [user1.pk]}
url = reverse("tasks-detail", args=[task.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200, response.content
task = f.create_task(project=project1, owner=user1)
data = {"version": task.version,
"watchers": [user1.pk, user2.pk]}
url = reverse("tasks-detail", args=[task.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
task = f.create_task(project=project1, owner=user1)
data = dict(TaskSerializer(task).data)
data["id"] = None
data["version"] = None
data["watchers"] = [user1.pk, user2.pk]
url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
# Test the impossible case when project is not
# exists in create request, and validator works as expected
task = f.create_task(project=project1, owner=user1)
data = dict(TaskSerializer(task).data)
data["id"] = None
data["watchers"] = [user1.pk, user2.pk]
data["project"] = None
url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_watchers_assignation_for_us(client):
user1 = f.UserFactory.create()
user2 = f.UserFactory.create()
project1 = f.ProjectFactory.create(owner=user1)
project2 = f.ProjectFactory.create(owner=user2)
role1 = f.RoleFactory.create(project=project1)
role2 = f.RoleFactory.create(project=project2)
member1 = f.MembershipFactory.create(project=project1, user=user1, role=role1)
member2 = f.MembershipFactory.create(project=project2, user=user2, role=role2)
client.login(user1)
us = f.create_userstory(project=project1, owner=user1)
data = {"version": us.version,
"watchers": [user1.pk]}
url = reverse("userstories-detail", args=[us.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
us = f.create_userstory(project=project1, owner=user1)
data = {"version": us.version,
"watchers": [user1.pk, user2.pk]}
url = reverse("userstories-detail", args=[us.pk])
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
us = f.create_userstory(project=project1, owner=user1)
data = dict(UserStorySerializer(us).data)
data["id"] = None
data["version"] = None
data["watchers"] = [user1.pk, user2.pk]
url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
# Test the impossible case when project is not
# exists in create request, and validator works as expected
us = f.create_userstory(project=project1, owner=user1)
data = dict(UserStorySerializer(us).data)
data["id"] = None
data["watchers"] = [user1.pk, user2.pk]
data["project"] = None
url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400

View File

@ -47,6 +47,7 @@ def test_api_update_task_tags(client):
def test_api_create_in_bulk_with_status(client): def test_api_create_in_bulk_with_status(client):
us = f.create_userstory() us = f.create_userstory()
us.project.default_task_status = f.TaskStatusFactory.create(project=us.project)
url = reverse("tasks-bulk-create") url = reverse("tasks-bulk-create")
data = { data = {
"bulk_tasks": "Story #1\nStory #2", "bulk_tasks": "Story #1\nStory #2",
@ -68,6 +69,7 @@ def test_api_create_invalid_task(client):
# But the User Story is not associated with the milestone # But the User Story is not associated with the milestone
us_milestone = f.MilestoneFactory.create() us_milestone = f.MilestoneFactory.create()
us = f.create_userstory(milestone=us_milestone) us = f.create_userstory(milestone=us_milestone)
us.project.default_task_status = f.TaskStatusFactory.create(project=us.project)
task_milestone = f.MilestoneFactory.create(project=us.project, owner=us.owner) task_milestone = f.MilestoneFactory.create(project=us.project, owner=us.owner)
url = reverse("tasks-list") url = reverse("tasks-list")