From bc473375ba84a671d6a0f2087df716c44226e9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Feb 2015 19:49:35 +0100 Subject: [PATCH 01/28] US #55: Custom fields - Create model --- settings/common.py | 1 + taiga/projects/custom_attributes/__init__.py | 0 .../migrations/0001_initial.py | 84 +++++++++++++++++++ .../custom_attributes/migrations/__init__.py | 0 taiga/projects/custom_attributes/models.py | 74 ++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 taiga/projects/custom_attributes/__init__.py create mode 100644 taiga/projects/custom_attributes/migrations/0001_initial.py create mode 100644 taiga/projects/custom_attributes/migrations/__init__.py create mode 100644 taiga/projects/custom_attributes/models.py diff --git a/settings/common.py b/settings/common.py index a8ea96cd..dc8d2b9d 100644 --- a/settings/common.py +++ b/settings/common.py @@ -180,6 +180,7 @@ INSTALLED_APPS = [ "taiga.userstorage", "taiga.projects", "taiga.projects.references", + "taiga.projects.custom_attributes", "taiga.projects.history", "taiga.projects.notifications", "taiga.projects.attachments", diff --git a/taiga/projects/custom_attributes/__init__.py b/taiga/projects/custom_attributes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/custom_attributes/migrations/0001_initial.py b/taiga/projects/custom_attributes/migrations/0001_initial.py new file mode 100644 index 00000000..5814507d --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0015_auto_20141230_1212'), + ] + + operations = [ + migrations.CreateModel( + name='IssueCustomAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('name', models.CharField(verbose_name='name', max_length=64)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('order', models.IntegerField(verbose_name='order', default=10000)), + ('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)), + ('modified_date', models.DateTimeField(verbose_name='modified date')), + ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='issuecustomattributes')), + ], + options={ + 'ordering': ['project', 'order', 'name'], + 'verbose_name': 'issue custom attribute', + 'verbose_name_plural': 'issue custom attributes', + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='TaskCustomAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('name', models.CharField(verbose_name='name', max_length=64)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('order', models.IntegerField(verbose_name='order', default=10000)), + ('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)), + ('modified_date', models.DateTimeField(verbose_name='modified date')), + ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='taskcustomattributes')), + ], + options={ + 'ordering': ['project', 'order', 'name'], + 'verbose_name': 'task custom attribute', + 'verbose_name_plural': 'task custom attributes', + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='UserStoryCustomAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), + ('name', models.CharField(verbose_name='name', max_length=64)), + ('description', models.TextField(blank=True, verbose_name='description')), + ('order', models.IntegerField(verbose_name='order', default=10000)), + ('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)), + ('modified_date', models.DateTimeField(verbose_name='modified date')), + ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='userstorycustomattributes')), + ], + options={ + 'ordering': ['project', 'order', 'name'], + 'verbose_name': 'user story custom attribute', + 'verbose_name_plural': 'user story custom attributes', + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='userstorycustomattribute', + unique_together=set([('project', 'name')]), + ), + migrations.AlterUniqueTogether( + name='taskcustomattribute', + unique_together=set([('project', 'name')]), + ), + migrations.AlterUniqueTogether( + name='issuecustomattribute', + unique_together=set([('project', 'name')]), + ), + ] diff --git a/taiga/projects/custom_attributes/migrations/__init__.py b/taiga/projects/custom_attributes/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py new file mode 100644 index 00000000..5efbea47 --- /dev/null +++ b/taiga/projects/custom_attributes/models.py @@ -0,0 +1,74 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone + + +###################################################### +# Base Model Class +####################################################### + +class AbstractCustomAttribute(models.Model): + name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name")) + description = models.TextField(null=False, blank=True, verbose_name=_("description")) + order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("order")) + project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss", + verbose_name=_("project")) + + created_date = models.DateTimeField(null=False, blank=False, default=timezone.now, + verbose_name=_("created date")) + modified_date = models.DateTimeField(null=False, blank=False, + verbose_name=_("modified date")) + _importing = None + + class Meta: + abstract = True + ordering = ["project", "order", "name"] + unique_together = ("project", "name") + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self._importing or not self.modified_date: + self.modified_date = timezone.now() + + return super().save(*args, **kwargs) + + + +###################################################### +# Custom Field Models +####################################################### + +class UserStoryCustomAttribute(AbstractCustomAttribute): + class Meta(AbstractCustomAttribute.Meta): + verbose_name = "user story custom attribute" + verbose_name_plural = "user story custom attributes" + + +class TaskCustomAttribute(AbstractCustomAttribute): + class Meta(AbstractCustomAttribute.Meta): + verbose_name = "task custom attribute" + verbose_name_plural = "task custom attributes" + + +class IssueCustomAttribute(AbstractCustomAttribute): + class Meta(AbstractCustomAttribute.Meta): + verbose_name = "issue custom attribute" + verbose_name_plural = "issue custom attributes" From 38d4eacd761ed5d37b377534bb7330f3f37f4c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Feb 2015 19:50:08 +0100 Subject: [PATCH 02/28] US #55: Custom fields - Create admin --- taiga/projects/custom_attributes/admin.py | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 taiga/projects/custom_attributes/admin.py diff --git a/taiga/projects/custom_attributes/admin.py b/taiga/projects/custom_attributes/admin.py new file mode 100644 index 00000000..201a31f0 --- /dev/null +++ b/taiga/projects/custom_attributes/admin.py @@ -0,0 +1,71 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from django.contrib import admin + +from . import models + + + +@admin.register(models.UserStoryCustomAttribute) +class UserStoryCustomAttributeAdmin(admin.ModelAdmin): + list_display = ["id", "name", "project", "order"] + list_display_links = ["id", "name"] + fieldsets = ( + (None, { + "fields": ("name", "description", ("project", "order")) + }), + ("Advanced options", { + "classes": ("collapse",), + "fields": (("created_date", "modified_date"),) + }) + ) + readonly_fields = ("created_date", "modified_date") + search_fields = ["id", "name", "project__name", "project__slug"] + + +@admin.register(models.TaskCustomAttribute) +class TaskCustomAttributeAdmin(admin.ModelAdmin): + list_display = ["id", "name", "project", "order"] + list_display_links = ["id", "name"] + fieldsets = ( + (None, { + "fields": ("name", "description", ("project", "order")) + }), + ("Advanced options", { + "classes": ("collapse",), + "fields": (("created_date", "modified_date"),) + }) + ) + readonly_fields = ("created_date", "modified_date") + search_fields = ["id", "name", "project__name", "project__slug"] + + +@admin.register(models.IssueCustomAttribute) +class IssueCustomAttributeAdmin(admin.ModelAdmin): + list_display = ["id", "name", "project", "order"] + list_display_links = ["id", "name"] + fieldsets = ( + (None, { + "fields": ("name", "description", ("project", "order")) + }), + ("Advanced options", { + "classes": ("collapse",), + "fields": (("created_date", "modified_date"),) + }) + ) + readonly_fields = ("created_date", "modified_date") + search_fields = ["id", "name", "project__name", "project__slug"] From c17157f8eca6b8b621df72ad93e0e8608c0f064a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Feb 2015 19:51:39 +0100 Subject: [PATCH 03/28] US #55: Custom fields - Create API end point --- taiga/projects/custom_attributes/api.py | 57 +++++++++++ .../projects/custom_attributes/permissions.py | 47 ++++++++++ .../projects/custom_attributes/serializers.py | 68 ++++++++++++++ taiga/projects/custom_attributes/services.py | 69 ++++++++++++++ taiga/routers.py | 94 +++++++++++-------- 5 files changed, 295 insertions(+), 40 deletions(-) create mode 100644 taiga/projects/custom_attributes/api.py create mode 100644 taiga/projects/custom_attributes/permissions.py create mode 100644 taiga/projects/custom_attributes/serializers.py create mode 100644 taiga/projects/custom_attributes/services.py diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py new file mode 100644 index 00000000..43f8a2f5 --- /dev/null +++ b/taiga/projects/custom_attributes/api.py @@ -0,0 +1,57 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from taiga.base.api import ModelCrudViewSet +from taiga.base import filters +from taiga.projects.mixins.ordering import BulkUpdateOrderMixin + +from . import models +from . import serializers +from . import permissions +from . import services + + +class UserStoryCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.UserStoryCustomAttribute + serializer_class = serializers.UserStoryCustomAttributeSerializer + permission_classes = (permissions.UserStoryCustomAttributePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ("project",) + bulk_update_param = "bulk_userstory_custom_attributes" + bulk_update_perm = "change_userstory_custom_attributes" + bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order + + +class TaskCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.TaskCustomAttribute + serializer_class = serializers.TaskCustomAttributeSerializer + permission_classes = (permissions.TaskCustomAttributePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ("project",) + bulk_update_param = "bulk_task_custom_attributes" + bulk_update_perm = "change_task_custom_attributes" + bulk_update_order_action = services.bulk_update_task_custom_attribute_order + + +class IssueCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.IssueCustomAttribute + serializer_class = serializers.IssueCustomAttributeSerializer + permission_classes = (permissions.IssueCustomAttributePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ("project",) + bulk_update_param = "bulk_issue_custom_attributes" + bulk_update_perm = "change_issue_custom_attributes" + bulk_update_order_action = services.bulk_update_issue_custom_attribute_order diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py new file mode 100644 index 00000000..3f82f38e --- /dev/null +++ b/taiga/projects/custom_attributes/permissions.py @@ -0,0 +1,47 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from taiga.base.api.permissions import TaigaResourcePermission +from taiga.base.api.permissions import HasProjectPerm +from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import AllowAny + + +class UserStoryCustomAttributePermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() + + +class TaskCustomAttributePermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() + + +class IssueCustomAttributePermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py new file mode 100644 index 00000000..025607f8 --- /dev/null +++ b/taiga/projects/custom_attributes/serializers.py @@ -0,0 +1,68 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from django.utils.translation import ugettext_lazy as _ + +from rest_framework.serializers import ValidationError + +from taiga.base.serializers import ModelSerializer + +from . import models + + +###################################################### +# Base Serializer Class +####################################################### + +class BaseCustomAttributeSerializer(ModelSerializer): + def validate(self, data): + """ + Check the name is not duplicated in the project. Check when: + - create a new one + - update the name + - update the project (move to another project) + """ + data_name = data.get("name", None) + data_project = data.get("project", None) + if self.object: + data_name = data_name or self.object.name + data_project = data_project or self.object.project + + model = self.Meta.model + qs = model.objects.filter(project=data_project, name=data_name) + if qs.exists(): + raise ValidationError(_("There is a custom field with the same name in this project.")) + + return data + + +###################################################### +# Custom Field Serializers +####################################################### + +class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): + class Meta: + model = models.UserStoryCustomAttribute + + +class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer): + class Meta: + model = models.TaskCustomAttribute + + +class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): + class Meta: + model = models.IssueCustomAttribute diff --git a/taiga/projects/custom_attributes/services.py b/taiga/projects/custom_attributes/services.py new file mode 100644 index 00000000..7cbea6c4 --- /dev/null +++ b/taiga/projects/custom_attributes/services.py @@ -0,0 +1,69 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from django.db import transaction +from django.db import connection + + +@transaction.atomic +def bulk_update_userstory_custom_attribute_order(project, user, data): + cursor = connection.cursor() + + sql = """ + prepare bulk_update_order as update custom_attributes_userstorycustomattribute set "order" = $1 + where custom_attributes_userstorycustomattribute.id = $2 and + custom_attributes_userstorycustomattribute.project_id = $3; + """ + cursor.execute(sql) + for id, order in data: + cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", + (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") + cursor.close() + + +@transaction.atomic +def bulk_update_task_custom_attribute_order(project, user, data): + cursor = connection.cursor() + + sql = """ + prepare bulk_update_order as update custom_attributes_taskcustomattribute set "order" = $1 + where custom_attributes_taskcustomattribute.id = $2 and + custom_attributes_taskcustomattribute.project_id = $3; + """ + cursor.execute(sql) + for id, order in data: + cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", + (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") + cursor.close() + + +@transaction.atomic +def bulk_update_issue_custom_attribute_order(project, user, data): + cursor = connection.cursor() + + sql = """ + prepare bulk_update_order as update custom_attributes_issuecustomattribute set "order" = $1 + where custom_attributes_issuecustomattribute.id = $2 and + custom_attributes_issuecustomattribute.project_id = $3; + """ + cursor.execute(sql) + for id, order in data: + cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", + (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") + cursor.close() diff --git a/taiga/routers.py b/taiga/routers.py index ad39b94c..86b1ba5e 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -35,23 +35,10 @@ from taiga.userstorage.api import StorageEntriesViewSet router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage") -# Resolver -from taiga.projects.references.api import ResolverViewSet +# Notify policies +from taiga.projects.notifications.api import NotifyPolicyViewSet -router.register(r"resolver", ResolverViewSet, base_name="resolver") - - -# Search -from taiga.searches.api import SearchViewSet - -router.register(r"search", SearchViewSet, base_name="search") - - -# Importer -from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet - -router.register(r"importer", ProjectImporterViewSet, base_name="importer") -router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") +router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications") # Projects & Selectors @@ -80,6 +67,31 @@ router.register(r"priorities", PriorityViewSet, base_name="priorities") router.register(r"severities",SeverityViewSet , base_name="severities") +# Custom Attributes +from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet +from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet +from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet + +router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet, + base_name="userstory-custom-attributes") +router.register(r"task-custom-attributes", TaskCustomAttributeViewSet, + base_name="task-custom-attributes") +router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet, + base_name="issue-custom-attributes") + + +# Search +from taiga.searches.api import SearchViewSet + +router.register(r"search", SearchViewSet, base_name="search") + + +# Resolver +from taiga.projects.references.api import ResolverViewSet + +router.register(r"resolver", ResolverViewSet, base_name="resolver") + + # Attachments from taiga.projects.attachments.api import UserStoryAttachmentViewSet from taiga.projects.attachments.api import IssueAttachmentViewSet @@ -93,11 +105,21 @@ router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue- router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments") -# Webhooks -from taiga.webhooks.api import WebhookViewSet, WebhookLogViewSet +# Project components +from taiga.projects.milestones.api import MilestoneViewSet +from taiga.projects.userstories.api import UserStoryViewSet +from taiga.projects.tasks.api import TaskViewSet +from taiga.projects.issues.api import IssueViewSet +from taiga.projects.issues.api import VotersViewSet +from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet -router.register(r"webhooks", WebhookViewSet, base_name="webhooks") -router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs") +router.register(r"milestones", MilestoneViewSet, base_name="milestones") +router.register(r"userstories", UserStoryViewSet, base_name="userstories") +router.register(r"tasks", TaskViewSet, base_name="tasks") +router.register(r"issues", IssueViewSet, base_name="issues") +router.register(r"issues/(?P\d+)/voters", VotersViewSet, base_name="issue-voters") +router.register(r"wiki", WikiViewSet, base_name="wiki") +router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") # History & Components @@ -120,27 +142,12 @@ router.register(r"timeline/user", UserTimeline, base_name="user-timeline") router.register(r"timeline/project", ProjectTimeline, base_name="project-timeline") -# Project components -from taiga.projects.milestones.api import MilestoneViewSet -from taiga.projects.userstories.api import UserStoryViewSet -from taiga.projects.tasks.api import TaskViewSet -from taiga.projects.issues.api import IssueViewSet -from taiga.projects.issues.api import VotersViewSet -from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet +# Webhooks +from taiga.webhooks.api import WebhookViewSet +from taiga.webhooks.api import WebhookLogViewSet -router.register(r"milestones", MilestoneViewSet, base_name="milestones") -router.register(r"userstories", UserStoryViewSet, base_name="userstories") -router.register(r"tasks", TaskViewSet, base_name="tasks") -router.register(r"issues", IssueViewSet, base_name="issues") -router.register(r"issues/(?P\d+)/voters", VotersViewSet, base_name="issue-voters") -router.register(r"wiki", WikiViewSet, base_name="wiki") -router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") - - -# Notify policies -from taiga.projects.notifications.api import NotifyPolicyViewSet - -router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications") +router.register(r"webhooks", WebhookViewSet, base_name="webhooks") +router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs") # GitHub webhooks @@ -161,5 +168,12 @@ from taiga.hooks.bitbucket.api import BitBucketViewSet router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook") +# Importer +from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet + +router.register(r"importer", ProjectImporterViewSet, base_name="importer") +router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") + + # feedback # - see taiga.feedback.routers and taiga.feedback.apps From 84db9956d5f59c2b78dbc48e69ca43dbd3b2098b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Feb 2015 19:52:16 +0100 Subject: [PATCH 04/28] US #55: Custom fields - Tests --- tests/factories.py | 30 + .../test_custom_attributes_resource.py | 586 ++++++++++++++++++ tests/integration/test_custom_attributes.py | 189 ++++++ 3 files changed, 805 insertions(+) create mode 100644 tests/integration/resources_permissions/test_custom_attributes_resource.py create mode 100644 tests/integration/test_custom_attributes.py diff --git a/tests/factories.py b/tests/factories.py index 96f6ccf4..0b555deb 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -354,6 +354,36 @@ class IssueTypeFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") +class UserStoryCustomAttributeFactory(Factory): + class Meta: + model = "custom_attributes.UserStoryCustomAttribute" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "UserStory Custom Attribute {}".format(n)) + description = factory.Sequence(lambda n: "Description for UserStory Custom Attribute {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class TaskCustomAttributeFactory(Factory): + class Meta: + model = "custom_attributes.TaskCustomAttribute" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "Task Custom Attribute {}".format(n)) + description = factory.Sequence(lambda n: "Description for Task Custom Attribute {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class IssueCustomAttributeFactory(Factory): + class Meta: + model = "custom_attributes.IssueCustomAttribute" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "Issue Custom Attribute {}".format(n)) + description = factory.Sequence(lambda n: "Description for Issue Custom Attribute {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + # class FanFactory(Factory): # project = factory.SubFactory("tests.factories.ProjectFactory") # user = factory.SubFactory("tests.factories.UserFactory") diff --git a/tests/integration/resources_permissions/test_custom_attributes_resource.py b/tests/integration/resources_permissions/test_custom_attributes_resource.py new file mode 100644 index 00000000..8b84411f --- /dev/null +++ b/tests/integration/resources_permissions/test_custom_attributes_resource.py @@ -0,0 +1,586 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.custom_attributes import serializers +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_owner=True) + + m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project) + m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1) + m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2) + + m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project) + m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1) + m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2) + + m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project) + m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) + m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) + + return m + + +######################################################### +# User Story Custom Fields +######################################################### + +def test_userstory_status_retrieve(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_userstory_status_update(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.public_userstory_ca).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', public_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca1).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', private1_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca2).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_userstory_status_delete(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_userstory_status_list(client, data): + url = reverse('userstory-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_userstory_status_patch(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_userstory_status_action_bulk_update_order(client, data): + url = reverse('userstory-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +######################################################### +# Task Custom Fields +######################################################### + +def test_task_status_retrieve(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_status_update(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.public_task_ca).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', public_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca1).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', private1_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca2).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_status_delete(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_task_status_list(client, data): + url = reverse('task-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_task_status_patch(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_status_action_bulk_update_order(client, data): + url = reverse('task-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +######################################################### +# Issue Custom Fields +######################################################### + +def test_issue_status_retrieve(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_status_update(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.public_issue_ca).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', public_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca1).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', private1_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca2).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_status_delete(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_status_list(client, data): + url = reverse('issue-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_issue_status_patch(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_status_action_bulk_update_order(client, data): + url = reverse('issue-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] diff --git a/tests/integration/test_custom_attributes.py b/tests/integration/test_custom_attributes.py new file mode 100644 index 00000000..91e0a8f6 --- /dev/null +++ b/tests/integration/test_custom_attributes.py @@ -0,0 +1,189 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +######################################################### +# User Story Custom Fields +######################################################### + +def test_userstory_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Task Custom Fields +######################################################### + +def test_task_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("task-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Issue Custom Fields +######################################################### + +def test_issue_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 From ebc3388d03b4fb63902f9c493b502c390e416f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 5 Feb 2015 12:30:13 +0100 Subject: [PATCH 05/28] US #55: Custom fields - Import/Export custom attributes --- taiga/export_import/api.py | 15 +++++++ taiga/export_import/dump_service.py | 10 +++++ taiga/export_import/serializers.py | 53 ++++++++++++++++++++----- taiga/export_import/service.py | 25 ++++++++++-- tests/integration/test_importer_api.py | 55 ++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 12 deletions(-) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index f05993df..8ddb4c15 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -127,6 +127,21 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi "severities" in data): service.store_default_choices(project_serialized.object, data) + if "userstorycustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "userstorycustomattributes", + serializers.UserStoryCustomAttributeExportSerializer) + + if "taskcustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "taskcustomattributes", + serializers.TaskCustomAttributeExportSerializer) + + if "issuecustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "issuecustomattributes", + serializers.IssueCustomAttributeExportSerializer) + if "roles" in data: service.store_roles(project_serialized.object, data) diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 09a9f0d9..2ef615a1 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -103,6 +103,16 @@ def dict_to_project(data, owner=None): if service.get_errors(clear=False): raise TaigaImportError('error importing default choices') + service.store_custom_attributes(proj, data, "userstorycustomattributes", + serializers.UserStoryCustomAttributeExportSerializer) + service.store_custom_attributes(proj, data, "taskcustomattributes", + serializers.TaskCustomAttributeExportSerializer) + service.store_custom_attributes(proj, data, "issuecustomattributes", + serializers.IssueCustomAttributeExportSerializer) + + if service.get_errors(clear=False): + raise TaigaImportError('error importing custom attributes') + service.store_roles(proj, data) if service.get_errors(clear=False): diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 8f0e0534..2893537b 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -20,11 +20,13 @@ from collections import OrderedDict from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError from rest_framework import serializers from taiga.projects import models as projects_models +from taiga.projects.custom_attributes import models as custom_attributes_models from taiga.projects.userstories import models as userstories_models from taiga.projects.tasks import models as tasks_models from taiga.projects.issues import models as issues_models @@ -81,14 +83,15 @@ class RelatedNoneSafeField(serializers.RelatedField): return value = self.get_default_value() + key = self.source or field_name if value in self.null_values: if self.required: raise ValidationError(self.error_messages['required']) - into[(self.source or field_name)] = None + into[key] = None elif self.many: - into[(self.source or field_name)] = [self.from_native(item) for item in value if self.from_native(item) is not None] + into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] else: - into[(self.source or field_name)] = self.from_native(value) + into[key] = self.from_native(value) class UserRelatedField(RelatedNoneSafeField): @@ -251,7 +254,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer): def get_attachments(self, obj): content_type = ContentType.objects.get_for_model(obj.__class__) - attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, content_type=content_type) + attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, + content_type=content_type) return AttachmentExportSerializer(attachments_qs, many=True).data @@ -305,6 +309,30 @@ class RoleExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') +class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.UserStoryCustomAttribute + exclude = ('id', 'project') + + +class TaskCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.TaskCustomAttribute + exclude = ('id', 'project') + + +class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.IssueCustomAttribute + exclude = ('id', 'project') + + class MembershipExportSerializer(serializers.ModelSerializer): user = UserRelatedField(required=False) role = ProjectRelatedField(slug_field="name") @@ -354,7 +382,8 @@ class MilestoneExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) @@ -368,7 +397,8 @@ class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSeriali exclude = ('id', 'project') -class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) @@ -383,7 +413,8 @@ class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSe exclude = ('id', 'project', 'points', 'tasks') -class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -403,7 +434,8 @@ class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerial exclude = ('id', 'project') -class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False) watchers = UserRelatedField(many=True, required=False) @@ -437,6 +469,9 @@ class ProjectExportSerializer(serializers.ModelSerializer): priorities = PriorityExportSerializer(many=True, required=False) severities = SeverityExportSerializer(many=True, required=False) issue_types = IssueTypeExportSerializer(many=True, required=False) + userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) + taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) + issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) roles = RoleExportSerializer(many=True, required=False) milestones = MilestoneExportSerializer(many=True, required=False) wiki_pages = WikiPageExportSerializer(many=True, required=False) diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 797ae81f..0d8aff03 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -57,7 +57,8 @@ def store_project(data): "default_priority", "default_severity", "default_issue_status", "default_issue_type", "memberships", "points", "us_statuses", "task_statuses", "issue_statuses", "priorities", "severities", - "issue_types", "roles", "milestones", "wiki_pages", + "issue_types", "userstorycustomattributes", "taskcustomattributes", + "issuecustomattributes", "roles", "milestones", "wiki_pages", "wiki_links", "notify_policies", "user_stories", "issues", "tasks", ] if key not in excluded_fields: @@ -72,7 +73,7 @@ def store_project(data): return None -def store_choice(project, data, field, serializer): +def _store_choice(project, data, field, serializer): serialized = serializer(data=data) if serialized.is_valid(): serialized.object.project = project @@ -86,7 +87,25 @@ def store_choice(project, data, field, serializer): def store_choices(project, data, field, serializer): result = [] for choice_data in data.get(field, []): - result.append(store_choice(project, choice_data, field, serializer)) + result.append(_store_choice(project, choice_data, field, serializer)) + return result + + +def _store_custom_attribute(project, data, field, serializer): + serialized = serializer(data=data) + if serialized.is_valid(): + serialized.object.project = project + serialized.object._importing = True + serialized.save() + return serialized.object + add_errors(field, serialized.errors) + return None + + +def store_custom_attributes(project, data, field, serializer): + result = [] + for custom_attribute_data in data.get(field, []): + result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer)) return result diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 6236f80f..6231d4a9 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -167,6 +167,61 @@ def test_invalid_project_import_with_extra_data(client): assert Project.objects.filter(slug="imported-project").count() == 0 +def test_valid_project_import_with_custom_attributes(client): + user = f.UserFactory.create() + + url = reverse("importer-list") + data = { + "name": "Imported project", + "description": "Imported project", + "userstorycustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }], + "taskcustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }], + "issuecustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }] + } + + must_empty_children = ["issues", "user_stories", "wiki_pages", "milestones", "wiki_links"] + must_one_instance_children = ["userstorycustomattributes", "taskcustomattributes", "issuecustomattributes"] + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert all(map(lambda x: len(response.data[x]) == 0, must_empty_children)) + # Allwais is created at least the owner membership + assert all(map(lambda x: len(response.data[x]) == 1, must_one_instance_children)) + assert response.data["owner"] == user.email + + +def test_invalid_project_import_with_custom_attributes(client): + user = f.UserFactory.create() + + url = reverse("importer-list") + data = { + "name": "Imported project", + "description": "Imported project", + "userstorycustomattributes": [{ }], + "taskcustomattributes": [{ }], + "issuecustomattributes": [{ }] + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert len(response.data) == 3 + assert Project.objects.filter(slug="imported-project").count() == 0 + + def test_invalid_issue_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) From 2893213932bc9706bfff00b11ad65bdbe9564445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 5 Feb 2015 18:08:09 +0100 Subject: [PATCH 06/28] US #55: Custom fields - Create model to save the custom attributes values and update the history --- taiga/projects/custom_attributes/api.py | 54 ++ ...svalues_userstorycustomattributesvalues.py | 66 ++ taiga/projects/custom_attributes/models.py | 68 +- .../projects/custom_attributes/permissions.py | 42 ++ .../projects/custom_attributes/serializers.py | 80 ++- taiga/projects/history/freeze_impl.py | 43 ++ taiga/projects/history/models.py | 29 + .../emails/includes/fields_diff-html.jinja | 53 +- .../emails/includes/fields_diff-text.jinja | 35 +- taiga/projects/issues/serializers.py | 2 +- taiga/projects/tasks/serializers.py | 2 +- taiga/projects/userstories/models.py | 2 +- taiga/projects/userstories/serializers.py | 20 +- taiga/routers.py | 10 + tests/factories.py | 27 + .../test_custom_attributes_resource.py | 586 ------------------ .../test_issues_custom_attributes_resource.py | 428 +++++++++++++ .../test_tasks_custom_attributes_resource.py | 287 +++++++++ ..._userstories_custom_attributes_resource.py | 287 +++++++++ tests/integration/test_custom_attributes.py | 189 ------ .../test_custom_attributes_issues.py | 271 ++++++++ .../test_custom_attributes_tasks.py | 268 ++++++++ .../test_custom_attributes_user_stories.py | 268 ++++++++ 23 files changed, 2310 insertions(+), 807 deletions(-) create mode 100644 taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py delete mode 100644 tests/integration/resources_permissions/test_custom_attributes_resource.py create mode 100644 tests/integration/resources_permissions/test_issues_custom_attributes_resource.py create mode 100644 tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py create mode 100644 tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py delete mode 100644 tests/integration/test_custom_attributes.py create mode 100644 tests/integration/test_custom_attributes_issues.py create mode 100644 tests/integration/test_custom_attributes_tasks.py create mode 100644 tests/integration/test_custom_attributes_user_stories.py diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 43f8a2f5..7e4033ab 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -14,9 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils.translation import ugettext_lazy as _ + from taiga.base.api import ModelCrudViewSet +from taiga.base.api.viewsets import ModelViewSet +from taiga.base import exceptions as exc from taiga.base import filters +from taiga.base import response + from taiga.projects.mixins.ordering import BulkUpdateOrderMixin +from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.occ.mixins import OCCResourceMixin from . import models from . import serializers @@ -24,6 +32,10 @@ from . import permissions from . import services +###################################################### +# Custom Attribute ViewSets +####################################################### + class UserStoryCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.UserStoryCustomAttribute serializer_class = serializers.UserStoryCustomAttributeSerializer @@ -55,3 +67,45 @@ class IssueCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): bulk_update_param = "bulk_issue_custom_attributes" bulk_update_perm = "change_issue_custom_attributes" bulk_update_order_action = services.bulk_update_issue_custom_attribute_order + + +###################################################### +# Custom Attributes Values ViewSets +####################################################### + +class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, ModelViewSet): + def list(self, request, *args, **kwargs): + return response.NotFound() + + def post_delete(self, obj): + # NOTE: When destroy a custom attributes values object, the + # content_object change after and not before + self.persist_history_snapshot(obj, delete=True) + super().pre_delete(obj) + + def get_object_for_snapshot(self, obj): + return getattr(obj, self.content_object) + + +class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): + model = models.UserStoryCustomAttributesValues + serializer_class = serializers.UserStoryCustomAttributesValuesSerializer + permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,) + lookup_field = "user_story_id" + content_object = "user_story" + + +class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): + model = models.TaskCustomAttributesValues + serializer_class = serializers.TaskCustomAttributesValuesSerializer + permission_classes = (permissions.TaskCustomAttributesValuesPermission,) + lockup_fields = "task_id" + content_object = "task" + + +class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): + model = models.IssueCustomAttributesValues + serializer_class = serializers.IssueCustomAttributesValuesSerializer + permission_classes = (permissions.IssueCustomAttributesValuesPermission,) + lockup_fields = "issue_id" + content_object = "issue" diff --git a/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py new file mode 100644 index 00000000..4c7d2461 --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django_pgjson.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0005_auto_20150114_0954'), + ('userstories', '0009_remove_userstory_is_archived'), + ('issues', '0004_auto_20150114_0954'), + ('custom_attributes', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='IssueCustomAttributesValues', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('version', models.IntegerField(default=1, verbose_name='version')), + ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('issue', models.OneToOneField(related_name='custom_attributes_values', to='issues.Issue', verbose_name='issue')), + ], + options={ + 'ordering': ['id'], + 'verbose_name_plural': 'issue custom attributes values', + 'verbose_name': 'issue ustom attributes values', + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='TaskCustomAttributesValues', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('version', models.IntegerField(default=1, verbose_name='version')), + ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('task', models.OneToOneField(related_name='custom_attributes_values', to='tasks.Task', verbose_name='task')), + ], + options={ + 'ordering': ['id'], + 'verbose_name_plural': 'task custom attributes values', + 'verbose_name': 'task ustom attributes values', + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='UserStoryCustomAttributesValues', + fields=[ + ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('version', models.IntegerField(default=1, verbose_name='version')), + ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('user_story', models.OneToOneField(related_name='custom_attributes_values', to='userstories.UserStory', verbose_name='user story')), + ], + options={ + 'ordering': ['id'], + 'verbose_name_plural': 'user story custom attributes values', + 'verbose_name': 'user story ustom attributes values', + 'abstract': False, + }, + bases=(models.Model,), + ), + ] diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 5efbea47..287498d4 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -18,9 +18,13 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django_pgjson.fields import JsonField + +from taiga.projects.occ.mixins import OCCModelMixin + ###################################################### -# Base Model Class +# Custom Attribute Models ####################################################### class AbstractCustomAttribute(models.Model): @@ -51,11 +55,6 @@ class AbstractCustomAttribute(models.Model): return super().save(*args, **kwargs) - -###################################################### -# Custom Field Models -####################################################### - class UserStoryCustomAttribute(AbstractCustomAttribute): class Meta(AbstractCustomAttribute.Meta): verbose_name = "user story custom attribute" @@ -72,3 +71,60 @@ class IssueCustomAttribute(AbstractCustomAttribute): class Meta(AbstractCustomAttribute.Meta): verbose_name = "issue custom attribute" verbose_name_plural = "issue custom attributes" + + +###################################################### +# Custom Attributes Values Models +####################################################### + +class AbstractCustomAttributesValues(OCCModelMixin, models.Model): + values = JsonField(null=False, blank=False, default={}, verbose_name=_("values")) + + class Meta: + abstract = True + ordering = ["id"] + + +class UserStoryCustomAttributesValues(AbstractCustomAttributesValues): + user_story = models.OneToOneField("userstories.UserStory", + null=False, blank=False, related_name="custom_attributes_values", + verbose_name=_("user story")) + + class Meta(AbstractCustomAttributesValues.Meta): + verbose_name = "user story ustom attributes values" + verbose_name_plural = "user story custom attributes values" + + @property + def project(self): + # NOTE: This property simplifies checking permissions + return self.user_story.project + + +class TaskCustomAttributesValues(AbstractCustomAttributesValues): + task = models.OneToOneField("tasks.Task", + null=False, blank=False, related_name="custom_attributes_values", + verbose_name=_("task")) + + class Meta(AbstractCustomAttributesValues.Meta): + verbose_name = "task ustom attributes values" + verbose_name_plural = "task custom attributes values" + + @property + def project(self): + # NOTE: This property simplifies checking permissions + return self.task.project + + +class IssueCustomAttributesValues(AbstractCustomAttributesValues): + issue = models.OneToOneField("issues.Issue", + null=False, blank=False, related_name="custom_attributes_values", + verbose_name=_("issue")) + + class Meta(AbstractCustomAttributesValues.Meta): + verbose_name = "issue ustom attributes values" + verbose_name_plural = "issue custom attributes values" + + @property + def project(self): + # NOTE: This property simplifies checking permissions + return self.issue.project diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 3f82f38e..617e90b0 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -18,9 +18,16 @@ from taiga.base.api.permissions import TaigaResourcePermission from taiga.base.api.permissions import HasProjectPerm from taiga.base.api.permissions import IsProjectOwner from taiga.base.api.permissions import AllowAny +from taiga.base.api.permissions import IsSuperUser +###################################################### +# Custom Attribute Permissions +####################################################### + class UserStoryCustomAttributePermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None retrieve_perms = HasProjectPerm('view_project') create_perms = IsProjectOwner() update_perms = IsProjectOwner() @@ -30,6 +37,8 @@ class UserStoryCustomAttributePermission(TaigaResourcePermission): class TaskCustomAttributePermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None retrieve_perms = HasProjectPerm('view_project') create_perms = IsProjectOwner() update_perms = IsProjectOwner() @@ -39,9 +48,42 @@ class TaskCustomAttributePermission(TaigaResourcePermission): class IssueCustomAttributePermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None retrieve_perms = HasProjectPerm('view_project') create_perms = IsProjectOwner() update_perms = IsProjectOwner() destroy_perms = IsProjectOwner() list_perms = AllowAny() bulk_update_order_perms = IsProjectOwner() + + +###################################################### +# Custom Attributes Values Permissions +####################################################### + +class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_us') + create_perms = HasProjectPerm('add_us') + update_perms = HasProjectPerm('modify_us') + destroy_perms = HasProjectPerm('delete_us') + + +class TaskCustomAttributesValuesPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_tasks') + create_perms = HasProjectPerm('add_task') + update_perms = HasProjectPerm('modify_task') + destroy_perms = HasProjectPerm('delete_task') + + +class IssueCustomAttributesValuesPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_issues') + create_perms = HasProjectPerm('add_issue') + update_perms = HasProjectPerm('modify_issue') + destroy_perms = HasProjectPerm('delete_issue') diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 025607f8..40a25ccc 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -14,20 +14,26 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + +from django.apps import apps from django.utils.translation import ugettext_lazy as _ from rest_framework.serializers import ValidationError from taiga.base.serializers import ModelSerializer +from taiga.base.serializers import JsonField from . import models ###################################################### -# Base Serializer Class +# Custom Attribute Serializer ####################################################### class BaseCustomAttributeSerializer(ModelSerializer): + class Meta: + read_only_fields = ('id', 'created_date', 'modified_date') + def validate(self, data): """ Check the name is not duplicated in the project. Check when: @@ -49,20 +55,78 @@ class BaseCustomAttributeSerializer(ModelSerializer): return data -###################################################### -# Custom Field Serializers -####################################################### - class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta: + class Meta(BaseCustomAttributeSerializer.Meta): model = models.UserStoryCustomAttribute class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta: + class Meta(BaseCustomAttributeSerializer.Meta): model = models.TaskCustomAttribute class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta: + class Meta(BaseCustomAttributeSerializer.Meta): model = models.IssueCustomAttribute + + +###################################################### +# Custom Attribute Serializer +####################################################### + + +class BaseCustomAttributesValuesSerializer: + values = JsonField(source="values", label="values", required=True) + _custom_attribute_model = None + _container_field = None + + def validate_values(self, attrs, source): + # values must be a dict + data_values = attrs.get("values", None) + if self.object: + data_values = (data_values or self.object.values) + + if type(data_values) is not dict: + raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) + + # Values keys must be in the container object project + data_container = attrs.get(self._container_field, None) + if data_container: + project_id = data_container.project_id + elif self.object: + project_id = getattr(self.object, self._container_field).project_id + else: + project_id = None + + values_ids = list(data_values.keys()) + qs = self._custom_attribute_model.objects.filter(project=project_id, + id__in=values_ids) + if qs.count() != len(values_ids): + raise ValidationError(_("It contain invalid custom fields.")) + + return attrs + + +class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): + _custom_attribute_model = models.UserStoryCustomAttribute + _container_model = "userstories.UserStory" + _container_field = "user_story" + + class Meta: + model = models.UserStoryCustomAttributesValues + + +class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): + _custom_attribute_model = models.TaskCustomAttribute + _container_field = "task" + + class Meta: + model = models.TaskCustomAttributesValues + + +class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): + _custom_attribute_model = models.IssueCustomAttribute + _container_field = "issue" + + class Meta: + model = models.IssueCustomAttributesValues diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index c3bc3a4f..1e67df09 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -14,9 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from contextlib import suppress + from functools import partial from django.apps import apps from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist + from taiga.base.utils.iterators import as_tuple from taiga.base.utils.iterators import as_dict from taiga.mdrender.service import render as mdrender @@ -181,6 +185,42 @@ def extract_attachments(obj) -> list: "order": attach.order} +@as_tuple +def extract_user_story_custom_attributes(obj) -> list: + with suppress(ObjectDoesNotExist): + custom_attributes_values = obj.custom_attributes_values.values + for attr in obj.project.userstorycustomattributes.all(): + with suppress(KeyError): + value = custom_attributes_values[str(attr.id)] + yield {"id": attr.id, + "name": attr.name, + "values": value} + + +@as_tuple +def extract_task_custom_attributes(obj) -> list: + with suppress(ObjectDoesNotExist): + custom_attributes_values = obj.custom_attributes_values.values + for attr in obj.project.taskcustomattributes.all(): + with suppress(KeyError): + value = custom_attributes_values[str(attr.id)] + yield {"id": attr.id, + "name": attr.name, + "values": value} + + +@as_tuple +def extract_issue_custom_attributes(obj) -> list: + with suppress(ObjectDoesNotExist): + custom_attributes_values = obj.custom_attributes_values.values + for attr in obj.project.issuecustomattributes.all(): + with suppress(KeyError): + value = custom_attributes_values[str(attr.id)] + yield {"id": attr.id, + "name": attr.name, + "values": value} + + def project_freezer(project) -> dict: fields = ("name", "slug", @@ -243,6 +283,7 @@ def userstory_freezer(us) -> dict: "is_blocked": us.is_blocked, "blocked_note": us.blocked_note, "blocked_note_html": mdrender(us.project, us.blocked_note), + "custom_attributes": extract_user_story_custom_attributes(us), } return snapshot @@ -267,6 +308,7 @@ def issue_freezer(issue) -> dict: "is_blocked": issue.is_blocked, "blocked_note": issue.blocked_note, "blocked_note_html": mdrender(issue.project, issue.blocked_note), + "custom_attributes": extract_issue_custom_attributes(issue), } return snapshot @@ -292,6 +334,7 @@ def task_freezer(task) -> dict: "is_blocked": task.is_blocked, "blocked_note": task.blocked_note, "blocked_note_html": mdrender(task.project, task.blocked_note), + "custom_attributes": extract_task_custom_attributes(task), } return snapshot diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 462e3d45..769ec789 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -197,6 +197,35 @@ class HistoryEntry(models.Model): if attachments["new"] or attachments["changed"] or attachments["deleted"]: value = attachments + elif key == "custom_attributes": + custom_attributes = { + "new": [], + "changed": [], + "deleted": [], + } + + oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0]} + newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1]} + + for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): + if aid in oldcustattrs and aid in newcustattrs: + changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid], + excluded_keys=("name")) + + if changes: + change = { + "name": newcustattrs.get(aid, {}).get("name", ""), + "changes": changes + } + custom_attributes["changed"].append(change) + elif aid in oldcustattrs and aid not in newcustattrs: + custom_attributes["deleted"].append(oldcustattrs[aid]) + elif aid not in oldcustattrs and aid in newcustattrs: + custom_attributes["new"].append(newcustattrs[aid]) + + if custom_attributes["new"] or custom_attributes["changed"] or custom_attributes["deleted"]: + value = custom_attributes + elif key in self.values: value = [resolve_value(key, x) for x in self.diff[key]] else: diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja index 4b537805..84a2d462 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja @@ -6,7 +6,8 @@ "backlog_order", "kanban_order", "taskboard_order", - "us_order" + "us_order", + "custom_attributes" ] %} {% for field_name, values in changed_fields.items() %} @@ -80,9 +81,7 @@

{{ _("Deleted attachment") }}

- {% if att.changes.description %}

{{ att.filename|linebreaksbr }}

- {% endif %} {% endfor %} @@ -155,7 +154,6 @@ {# * #} {% else %} -

{{ verbose_name(obj_class, field_name) }}

@@ -172,5 +170,52 @@ {% endif %} + + {% elif field_name == "custom_attributes" %} + {# CUSTOM ATTRIBUTES #} + {% if values.new %} + {% for attr in values['new']%} + + +

{{ attr.name }}

+ + + + + {{ _("to") }}
+ {{ attr.value|linebreaksbr }} + + + {% endfor %} + {% endif %} + {% if values.changed %} + {% for attr in values['changed'] %} + + +

{{ attr.name }}

+ + + {{ _("from") }}
+ {{ attr.changes.value.0|linebreaksbr }} + + + + + {{ _("to") }}
+ {{ attr.changes.value.1|linebreaksbr }} + + + {% endfor %} + {% endif %} + {% if values.deleted %} + {% for attr in values['deleted']%} + + +

{{ attr.name }}

+

{{ _("-deleted-") }}

+ + + {% endfor %} + {% endif %} {% endif %} {% endfor %} diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja index 206e237c..5ecbf496 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja @@ -8,7 +8,8 @@ "taskboard_order", "us_order", "blocked_note_diff", - "blocked_note_html" + "blocked_note_html", + "custom_attributes" ] %} {% for field_name, values in changed_fields.items() %} {% if field_name not in excluded_fields %} @@ -18,6 +19,7 @@ {% for role, points in values.items() %} * {{ role }} {{ _("to:") }} {{ points.1 }} {{ _("from:") }} {{ points.0 }} {% endfor %} + {# ATTACHMENTS #} {% elif field_name == "attachments" %} {% if values.new %} @@ -40,6 +42,7 @@ - {{ att.filename }} {% endfor %} {% endif %} + {# TAGS AND WATCHERS #} {% elif field_name in ["tags", "watchers"] %} {% set values_from = values.0 or [] %} @@ -53,6 +56,36 @@ {% if values_removed %} * {{ _("removed:") }} {{ ', '.join(values_removed) }} {% endif %} + + {# * #} + {% else %} + * {{ _("From:") }} {{ values.0 }} + * {{ _("To:") }} {{ values.1 }} {% endif %} + + {% elif field_name == "custom_attributes" %} + {# CUSTOM ATTRIBUTES #} + {% elif field_name == "attachments" %} + {% if values.new %} + {% for attr in values['new']%} + - {{ attr.name }}: + * {{ attr.value }} + {% endfor %} + {% endif %} + + {% if values.changed %} + {% for attr in values['changed'] %} + - {{ attr.name }}: + * {{ _("From:") }} {{ attr.changes.value.0 }} + * {{ _("To:") }} {{ attr.changes.value.1 }} + {% endfor %} + {% endif %} + + {% if values.deleted %} + {% for attr in values['deleted']%} + - {{ attr.name }}: {{ _("-deleted-") }} + * {{ attr.value }} + {% endfor %} + {% endif %} {% endif %} {% endfor %} diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 711cefd9..a951c369 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -16,7 +16,7 @@ from rest_framework import serializers -from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin, +from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin, PgArrayField, ModelSerializer) from taiga.mdrender.service import render as mdrender diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index 9e2d2f4a..7ee80fa3 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -18,7 +18,7 @@ from rest_framework import serializers from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin, PgArrayField, ModelSerializer) - + from taiga.mdrender.service import render as mdrender from taiga.projects.validators import ProjectExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 52772ca6..c0abc9b4 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -22,10 +22,10 @@ from django.utils import timezone from djorm_pgarray.fields import TextArrayField +from taiga.base.tags import TaggedMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin -from taiga.base.tags import TaggedMixin class RolePoints(models.Model): diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index f90970f3..ae81cf20 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -14,15 +14,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import json from django.apps import apps from rest_framework import serializers -from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin, - PgArrayField, ModelSerializer) +from taiga.base.serializers import Serializer +from taiga.base.serializers import TagsField +from taiga.base.serializers import NeighborsSerializerMixin +from taiga.base.serializers import PgArrayField +from taiga.base.serializers import ModelSerializer +from taiga.base.utils import json from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.validators import UserStoryStatusExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.notifications.validators import WatchersValidator @@ -92,7 +96,6 @@ class UserStorySerializer(WatchersValidator, ModelSerializer): class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): - def serialize_neighbor(self, neighbor): return NeighborUserStorySerializer(neighbor).data @@ -104,8 +107,7 @@ class NeighborUserStorySerializer(ModelSerializer): depth = 0 -class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, - Serializer): +class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer): project_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) bulk_stories = serializers.CharField() @@ -118,8 +120,6 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, Serializer): order = serializers.IntegerField() -class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, - UserStoryStatusExistsValidator, - Serializer): +class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer): project_id = serializers.IntegerField() bulk_stories = _UserStoryOrderBulkSerializer(many=True) diff --git a/taiga/routers.py b/taiga/routers.py index 86b1ba5e..8a09b80e 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -71,6 +71,9 @@ router.register(r"severities",SeverityViewSet , base_name="severities") from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet +from taiga.projects.custom_attributes.api import UserStoryCustomAttributesValuesViewSet +from taiga.projects.custom_attributes.api import TaskCustomAttributesValuesViewSet +from taiga.projects.custom_attributes.api import IssueCustomAttributesValuesViewSet router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet, base_name="userstory-custom-attributes") @@ -79,6 +82,13 @@ router.register(r"task-custom-attributes", TaskCustomAttributeViewSet, router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet, base_name="issue-custom-attributes") +router.register(r"userstories/custom-attributes-values", UserStoryCustomAttributesValuesViewSet, + base_name="userstory-custom-attributes-values") +router.register(r"tasks/custom-attributes-values", TaskCustomAttributesValuesViewSet, + base_name="task-custom-attributes-values") +router.register(r"issues/custom-attributes-values", IssueCustomAttributesValuesViewSet, + base_name="issue-custom-attributes-values") + # Search from taiga.searches.api import SearchViewSet diff --git a/tests/factories.py b/tests/factories.py index 0b555deb..e8f70e7d 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -384,6 +384,33 @@ class IssueCustomAttributeFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") +class UserStoryCustomAttributesValuesFactory(Factory): + class Meta: + model = "custom_attributes.UserStoryCustomAttributesValues" + strategy = factory.CREATE_STRATEGY + + values = {} + user_story = factory.SubFactory("tests.factories.UserStoryFactory") + + +class TaskCustomAttributesValuesFactory(Factory): + class Meta: + model = "custom_attributes.TaskCustomAttributesValues" + strategy = factory.CREATE_STRATEGY + + values = {} + task = factory.SubFactory("tests.factories.TaskFactory") + + +class IssueCustomAttributesValuesFactory(Factory): + class Meta: + model = "custom_attributes.IssueCustomAttributesValues" + strategy = factory.CREATE_STRATEGY + + values = {} + issue = factory.SubFactory("tests.factories.IssueFactory") + + # class FanFactory(Factory): # project = factory.SubFactory("tests.factories.ProjectFactory") # user = factory.SubFactory("tests.factories.UserFactory") diff --git a/tests/integration/resources_permissions/test_custom_attributes_resource.py b/tests/integration/resources_permissions/test_custom_attributes_resource.py deleted file mode 100644 index 8b84411f..00000000 --- a/tests/integration/resources_permissions/test_custom_attributes_resource.py +++ /dev/null @@ -1,586 +0,0 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán -# 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 . - - -from django.core.urlresolvers import reverse - -from taiga.base.utils import json -from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import MEMBERS_PERMISSIONS - -from tests import factories as f -from tests.utils import helper_test_http_method - -import pytest -pytestmark = pytest.mark.django_db - - -@pytest.fixture -def data(): - m = type("Models", (object,), {}) - m.registered_user = f.UserFactory.create() - m.project_member_with_perms = f.UserFactory.create() - m.project_member_without_perms = f.UserFactory.create() - m.project_owner = f.UserFactory.create() - m.other_user = f.UserFactory.create() - m.superuser = f.UserFactory.create(is_superuser=True) - - m.public_project = f.ProjectFactory(is_private=False, - anon_permissions=['view_project'], - public_permissions=['view_project'], - owner=m.project_owner) - m.private_project1 = f.ProjectFactory(is_private=True, - anon_permissions=['view_project'], - public_permissions=['view_project'], - owner=m.project_owner) - m.private_project2 = f.ProjectFactory(is_private=True, - anon_permissions=[], - public_permissions=[], - owner=m.project_owner) - - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - email=m.project_member_with_perms.email, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - email=m.project_member_with_perms.email, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - - f.MembershipFactory(project=m.private_project1, - user=m.project_member_without_perms, - email=m.project_member_without_perms.email, - role__project=m.private_project1, - role__permissions=[]) - - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - email=m.project_member_with_perms.email, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project2, - user=m.project_member_without_perms, - email=m.project_member_without_perms.email, - role__project=m.private_project2, - role__permissions=[]) - - f.MembershipFactory(project=m.public_project, - user=m.project_owner, - is_owner=True) - - f.MembershipFactory(project=m.private_project1, - user=m.project_owner, - is_owner=True) - - f.MembershipFactory(project=m.private_project2, - user=m.project_owner, - is_owner=True) - - m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project) - m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1) - m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2) - - m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project) - m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1) - m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2) - - m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project) - m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) - m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) - - return m - - -######################################################### -# User Story Custom Fields -######################################################### - -def test_userstory_status_retrieve(client, data): - public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) - private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) - private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private2_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_userstory_status_update(client, data): - public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) - private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) - private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.public_userstory_ca).data - userstory_ca_data["name"] = "test" - userstory_ca_data = json.dumps(userstory_ca_data) - results = helper_test_http_method(client, 'put', public_url, userstory_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca1).data - userstory_ca_data["name"] = "test" - userstory_ca_data = json.dumps(userstory_ca_data) - results = helper_test_http_method(client, 'put', private1_url, userstory_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca2).data - userstory_ca_data["name"] = "test" - userstory_ca_data = json.dumps(userstory_ca_data) - results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - -def test_userstory_status_delete(client, data): - public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) - private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) - private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private1_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private2_url, None, users) - assert results == [401, 403, 403, 403, 204] - - -def test_userstory_status_list(client, data): - url = reverse('userstory-custom-attributes-list') - - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_without_perms) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - client.login(data.project_owner) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - -def test_userstory_status_patch(client, data): - public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) - private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) - private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - - -def test_userstory_status_action_bulk_update_order(client, data): - url = reverse('userstory-custom-attributes-bulk-update-order') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - post_data = json.dumps({ - "bulk_userstory_custom_attributes": [(1,2)], - "project": data.public_project.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_userstory_custom_attributes": [(1,2)], - "project": data.private_project1.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_userstory_custom_attributes": [(1,2)], - "project": data.private_project2.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - -######################################################### -# Task Custom Fields -######################################################### - -def test_task_status_retrieve(client, data): - public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) - private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) - private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private2_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_task_status_update(client, data): - public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) - private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) - private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - task_ca_data = serializers.TaskCustomAttributeSerializer(data.public_task_ca).data - task_ca_data["name"] = "test" - task_ca_data = json.dumps(task_ca_data) - results = helper_test_http_method(client, 'put', public_url, task_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca1).data - task_ca_data["name"] = "test" - task_ca_data = json.dumps(task_ca_data) - results = helper_test_http_method(client, 'put', private1_url, task_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca2).data - task_ca_data["name"] = "test" - task_ca_data = json.dumps(task_ca_data) - results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - -def test_task_status_delete(client, data): - public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) - private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) - private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private1_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private2_url, None, users) - assert results == [401, 403, 403, 403, 204] - - -def test_task_status_list(client, data): - url = reverse('task-custom-attributes-list') - - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_without_perms) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - client.login(data.project_owner) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - -def test_task_status_patch(client, data): - public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) - private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) - private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - - -def test_task_status_action_bulk_update_order(client, data): - url = reverse('task-custom-attributes-bulk-update-order') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - post_data = json.dumps({ - "bulk_task_custom_attributes": [(1,2)], - "project": data.public_project.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_task_custom_attributes": [(1,2)], - "project": data.private_project1.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_task_custom_attributes": [(1,2)], - "project": data.private_project2.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - -######################################################### -# Issue Custom Fields -######################################################### - -def test_issue_status_retrieve(client, data): - public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) - private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) - private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private2_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_issue_status_update(client, data): - public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) - private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) - private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - issue_ca_data = serializers.IssueCustomAttributeSerializer(data.public_issue_ca).data - issue_ca_data["name"] = "test" - issue_ca_data = json.dumps(issue_ca_data) - results = helper_test_http_method(client, 'put', public_url, issue_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca1).data - issue_ca_data["name"] = "test" - issue_ca_data = json.dumps(issue_ca_data) - results = helper_test_http_method(client, 'put', private1_url, issue_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca2).data - issue_ca_data["name"] = "test" - issue_ca_data = json.dumps(issue_ca_data) - results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users) - assert results == [401, 403, 403, 403, 200] - - -def test_issue_status_delete(client, data): - public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) - private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) - private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private1_url, None, users) - assert results == [401, 403, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private2_url, None, users) - assert results == [401, 403, 403, 403, 204] - - -def test_issue_status_list(client, data): - url = reverse('issue-custom-attributes-list') - - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_without_perms) - response = client.json.get(url) - assert len(response.data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - client.login(data.project_owner) - response = client.json.get(url) - assert len(response.data) == 3 - assert response.status_code == 200 - - -def test_issue_status_patch(client, data): - public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) - private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) - private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) - assert results == [401, 403, 403, 403, 200] - - -def test_issue_status_action_bulk_update_order(client, data): - url = reverse('issue-custom-attributes-bulk-update-order') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - post_data = json.dumps({ - "bulk_issue_custom_attributes": [(1,2)], - "project": data.public_project.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_issue_custom_attributes": [(1,2)], - "project": data.private_project1.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] - - post_data = json.dumps({ - "bulk_issue_custom_attributes": [(1,2)], - "project": data.private_project2.pk - }) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 403, 204] diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py new file mode 100644 index 00000000..39caad51 --- /dev/null +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -0,0 +1,428 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.custom_attributes import serializers +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_owner=True) + + m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project) + m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) + m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) + + #m.public_issue = f.IssueFactory(project=m.public_project, owner=m.project_owner) + #m.private_issue1 = f.IssueFactory(project=m.private_project1, owner=m.project_owner) + #m.private_issue2 = f.IssueFactory(project=m.private_project2, owner=m.project_owner) + + #m.public_issue_cav = f.IssueCustomAttributesValuesFactory(project=m.public_project, + # issue=f.IssueFactory(project=m.public_project, + # owner=m.project_owner), + # values={str(m.public_issue_ca.id):"test"}) + #m.private_issue_cav1 = f.IssueCustomAttributesValuesFactory(project=m.private_project1, + # issue=f.IssueFactory(project=m.private_project1, + # owner=m.project_owner), + # values={str(m.private_issue_ca1.id):"test"}) + #m.private_issue_cav2 = f.IssueCustomAttributesValuesFactory(project=m.private_project2, + # issue=f.IssueFactory(project=m.private_project2, + # owner=m.project_owner), + # values={str(m.private_issue_ca2.id):"test"}) + + return m + + +######################################################### +# Issue Custom Attribute +######################################################### + +def test_issue_custom_attribute_retrieve(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_custom_attribute_create(client, data): + public_url = reverse('issue-custom-attributes-list') + private1_url = reverse('issue-custom-attributes-list') + private2_url = reverse('issue-custom-attributes-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_ca_data = {"name": "test-new", "project": data.public_project.id} + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'post', public_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + issue_ca_data = {"name": "test-new", "project": data.private_project1.id} + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'post', private1_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + issue_ca_data = {"name": "test-new", "project": data.private_project2.id} + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + +def test_issue_custom_attribute_update(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.public_issue_ca).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', public_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca1).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', private1_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca2).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_custom_attribute_delete(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_custom_attribute_list(client, data): + url = reverse('issue-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_issue_custom_attribute_patch(client, data): + public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) + private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) + private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_custom_attribute_action_bulk_update_order(client, data): + url = reverse('issue-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +######################################################### +# Issue Custom Attributes Values +######################################################### + +#def test_issue_custom_attributes_values_retrieve(client, data): +# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) +# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) +# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) +# users = [ +# None, +# data.registered_user, +# data.project_member_without_perms, +# data.project_member_with_perms, +# data.project_owner +# ] +# +# results = helper_test_http_method(client, 'get', public_url, None, users) +# assert results == [200, 200, 200, 200, 200] +# results = helper_test_http_method(client, 'get', private1_url, None, users) +# assert results == [200, 200, 200, 200, 200] +# results = helper_test_http_method(client, 'get', private2_url, None, users) +# assert results == [401, 403, 403, 200, 200] +# +# +#def test_issue_custom_attributes_values_update(client, data): +# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) +# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) +# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) +# +# users = [ +# None, +# data.registered_user, +# data.project_member_without_perms, +# data.project_member_with_perms, +# data.project_owner +# ] +# +# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.public_issue_cav).data +# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.public_issue_ca.id) +# issue_cav_data = json.dumps(issue_cav_data) +# results = helper_test_http_method(client, 'put', public_url, issue_cav_data, users) +# assert results == [401, 403, 403, 403, 200] +# +# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav1).data +# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.private_issue_ca1.id) +# issue_cav_data = json.dumps(issue_cav_data) +# results = helper_test_http_method(client, 'put', private1_url, issue_cav_data, users) +# assert results == [401, 403, 403, 403, 200] +# +# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav2).data +# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.private_issue_ca2.id) +# issue_cav_data = json.dumps(issue_cav_data) +# results = helper_test_http_method(client, 'put', private2_url, issue_cav_data, users) +# assert results == [401, 403, 403, 403, 200] +# +# +#def test_issue_custom_attributes_values_delete(client, data): +# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) +# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) +# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) +# +# users = [ +# None, +# data.registered_user, +# data.project_member_without_perms, +# data.project_member_with_perms, +# data.project_owner +# ] +# +# results = helper_test_http_method(client, 'delete', public_url, None, users) +# assert results == [401, 403, 403, 403, 204] +# results = helper_test_http_method(client, 'delete', private1_url, None, users) +# assert results == [401, 403, 403, 403, 204] +# results = helper_test_http_method(client, 'delete', private2_url, None, users) +# assert results == [401, 403, 403, 403, 204] +# +# +#def test_issue_custom_attributes_values_list(client, data): +# url = reverse('issue-custom-attributes-values-list') +# +# response = client.json.get(url) +# assert response.status_code == 404 +# +# client.login(data.registered_user) +# response = client.json.get(url) +# assert response.status_code == 404 +# +# client.login(data.project_member_without_perms) +# response = client.json.get(url) +# assert response.status_code == 404 +# +# client.login(data.project_member_with_perms) +# response = client.json.get(url) +# assert response.status_code == 404 +# +# client.login(data.project_owner) +# response = client.json.get(url) +# assert response.status_code == 404 +# +# +#def test_issue_custom_attributes_values_patch(client, data): +# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) +# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) +# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) +# +# users = [ +# None, +# data.registered_user, +# data.project_member_without_perms, +# data.project_member_with_perms, +# data.project_owner +# ] +# +# results = helper_test_http_method(client, 'patch', public_url, +# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.public_issue_ca.id), users) +# assert results == [401, 403, 403, 403, 200] +# results = helper_test_http_method(client, 'patch', private1_url, +# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.private_issue_ca1.id), users) +# assert results == [401, 403, 403, 403, 200] +# results = helper_test_http_method(client, 'patch', private2_url, +# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.private_issue_ca2.id), users) +# assert results == [401, 403, 403, 403, 200] diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py new file mode 100644 index 00000000..317ff991 --- /dev/null +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -0,0 +1,287 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.custom_attributes import serializers +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_owner=True) + + m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project) + m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1) + m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2) + + return m + + +######################################################### +# Task Custom Attribute +######################################################### + +def test_task_custom_attribute_retrieve(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_custom_attribute_create(client, data): + public_url = reverse('task-custom-attributes-list') + private1_url = reverse('task-custom-attributes-list') + private2_url = reverse('task-custom-attributes-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_ca_data = {"name": "test-new", "project": data.public_project.id} + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'post', public_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + task_ca_data = {"name": "test-new", "project": data.private_project1.id} + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'post', private1_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + task_ca_data = {"name": "test-new", "project": data.private_project2.id} + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'post', private2_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + +def test_task_custom_attribute_update(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.public_task_ca).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', public_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca1).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', private1_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca2).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_custom_attribute_delete(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_task_custom_attribute_list(client, data): + url = reverse('task-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_task_custom_attribute_patch(client, data): + public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) + private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) + private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_custom_attribute_action_bulk_update_order(client, data): + url = reverse('task-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py new file mode 100644 index 00000000..faee7fbe --- /dev/null +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -0,0 +1,287 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.custom_attributes import serializers +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_owner=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_owner=True) + + m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project) + m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1) + m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2) + + return m + + +######################################################### +# User Story Custom Attribute +######################################################### + +def test_userstory_custom_attribute_retrieve(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_userstory_custom_attribute_create(client, data): + public_url = reverse('userstory-custom-attributes-list') + private1_url = reverse('userstory-custom-attributes-list') + private2_url = reverse('userstory-custom-attributes-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + userstory_ca_data = {"name": "test-new", "project": data.public_project.id} + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'post', public_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + userstory_ca_data = {"name": "test-new", "project": data.private_project1.id} + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'post', private1_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + userstory_ca_data = {"name": "test-new", "project": data.private_project2.id} + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'post', private2_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + +def test_userstory_custom_attribute_update(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.public_userstory_ca).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', public_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca1).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', private1_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca2).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_userstory_custom_attribute_delete(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_userstory_custom_attribute_list(client, data): + url = reverse('userstory-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 3 + assert response.status_code == 200 + + +def test_userstory_custom_attribute_patch(client, data): + public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) + private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) + private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_userstory_custom_attribute_action_bulk_update_order(client, data): + url = reverse('userstory-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] diff --git a/tests/integration/test_custom_attributes.py b/tests/integration/test_custom_attributes.py deleted file mode 100644 index 91e0a8f6..00000000 --- a/tests/integration/test_custom_attributes.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán -# 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 . - - -from django.core.urlresolvers import reverse -from taiga.base.utils import json - -from .. import factories as f - -import pytest -pytestmark = pytest.mark.django_db - - -######################################################### -# User Story Custom Fields -######################################################### - -def test_userstory_custom_attribute_duplicate_name_error_on_create(client): - custom_attr_1 = f.UserStoryCustomAttributeFactory() - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("userstory-custom-attributes-list") - data = {"name": custom_attr_1.name, - "project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_userstory_custom_attribute_duplicate_name_error_on_update(client): - custom_attr_1 = f.UserStoryCustomAttributeFactory() - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"name": custom_attr_1.name} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_userstory_custom_attribute_duplicate_name_error_on_move_between_projects(client): - custom_attr_1 = f.UserStoryCustomAttributeFactory() - custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_2.project, - is_owner=True) - - - url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 - - -######################################################### -# Task Custom Fields -######################################################### - -def test_task_custom_attribute_duplicate_name_error_on_create(client): - custom_attr_1 = f.TaskCustomAttributeFactory() - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("task-custom-attributes-list") - data = {"name": custom_attr_1.name, - "project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_task_custom_attribute_duplicate_name_error_on_update(client): - custom_attr_1 = f.TaskCustomAttributeFactory() - custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"name": custom_attr_1.name} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(client): - custom_attr_1 = f.TaskCustomAttributeFactory() - custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_2.project, - is_owner=True) - - - url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 - - -######################################################### -# Issue Custom Fields -######################################################### - -def test_issue_custom_attribute_duplicate_name_error_on_create(client): - custom_attr_1 = f.IssueCustomAttributeFactory() - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("issue-custom-attributes-list") - data = {"name": custom_attr_1.name, - "project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_issue_custom_attribute_duplicate_name_error_on_update(client): - custom_attr_1 = f.IssueCustomAttributeFactory() - custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - - - url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"name": custom_attr_1.name} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 - - -def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(client): - custom_attr_1 = f.IssueCustomAttributeFactory() - custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name) - member = f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_1.project, - is_owner=True) - f.MembershipFactory(user=custom_attr_1.project.owner, - project=custom_attr_2.project, - is_owner=True) - - - url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) - data = {"project": custom_attr_1.project.pk} - - client.login(member.user) - response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 400 diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py new file mode 100644 index 00000000..50e689c1 --- /dev/null +++ b/tests/integration/test_custom_attributes_issues.py @@ -0,0 +1,271 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +######################################################### +# Issue Custom Attributes +######################################################### + +def test_issue_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.IssueCustomAttributeFactory() + custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Issue Custom Attributes Values +######################################################### + +def test_issue_custom_attributes_values_list(client): + member = f.MembershipFactory(is_owner=True) + + url = reverse("issue-custom-attributes-values-list") + + client.login(member.user) + response = client.json.get(url) + assert response.status_code == 404 + + +def test_issue_custom_attributes_values_create(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("issue-custom-attributes-values-list") + data = { + "issue": issue.id, + "values": { + ct1_id: "test_1", + ct2_id: "test_2" + }, + } + + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert json.loads(response.data["values"]) == data["values"] + issue = issue.__class__.objects.get(id=issue.id) + assert issue.custom_attributes_values.values == data["values"] + + +def test_issue_custom_attributes_values_create_with_error_invalid_key(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + + url = reverse("issue-custom-attributes-values-list") + data = { + "issue": issue.id, + "values": { + ct1_id: "test_1", + "123456": "test_2" + }, + } + + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attributes_values_update(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.IssueCustomAttributesValuesFactory( + issue=issue, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) + data = { + "values": { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + }, + "version": custom_attrs_val.version + } + + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert json.loads(response.data["values"]) == data["values"] + issue = issue.__class__.objects.get(id=issue.id) + assert issue.custom_attributes_values.values == data["values"] + + +def test_issue_custom_attributes_values_update_with_error_invalid_key(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.IssueCustomAttributesValuesFactory( + issue=issue, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) + data = { + "values": { + ct1_id: "test_1_updated", + "123456": "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_issue_custom_attributes_values_delete(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) + custom_attrs_val = f.IssueCustomAttributesValuesFactory( + issue=issue, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert issue.__class__.objects.filter(id=issue.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +def test_issue_custom_attributes_values_delete_us(client): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("issues-detail", args=[issue.id]) + custom_attrs_val = f.IssueCustomAttributesValuesFactory( + issue=issue, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert not issue.__class__.objects.filter(id=issue.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py new file mode 100644 index 00000000..f6786737 --- /dev/null +++ b/tests/integration/test_custom_attributes_tasks.py @@ -0,0 +1,268 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +######################################################### +# Task Custom Attributes +######################################################### + +def test_task_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("task-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.TaskCustomAttributeFactory() + custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Task Custom Attributes Values +######################################################### + +def test_task_custom_attributes_values_list(client): + member = f.MembershipFactory(is_owner=True) + + url = reverse("task-custom-attributes-values-list") + + client.login(member.user) + response = client.json.get(url) + assert response.status_code == 404 + + +def test_task_custom_attributes_values_create(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("task-custom-attributes-values-list") + data = { + "task": task.id, + "values": { + ct1_id: "test_1", + ct2_id: "test_2" + }, + } + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert json.loads(response.data["values"]) == data["values"] + task = task.__class__.objects.get(id=task.id) + assert task.custom_attributes_values.values == data["values"] + + +def test_task_custom_attributes_values_create_with_error_invalid_key(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + + url = reverse("task-custom-attributes-values-list") + data = { + "task": task.id, + "values": { + ct1_id: "test_1", + "123456": "test_2" + }, + } + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attributes_values_update(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.TaskCustomAttributesValuesFactory( + task=task, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + url = reverse("task-custom-attributes-values-detail", args=[task.id]) + data = { + "values": { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert json.loads(response.data["values"]) == data["values"] + task = task.__class__.objects.get(id=task.id) + assert task.custom_attributes_values.values == data["values"] + + +def test_task_custom_attributes_values_update_with_error_invalid_key(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.TaskCustomAttributesValuesFactory( + task=task, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + url = reverse("task-custom-attributes-values-detail", args=[task.id]) + data = { + "values": { + ct1_id: "test_1_updated", + "123456": "test_2_updated" + }, + "version": custom_attrs_val.version + } + + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_task_custom_attributes_values_delete(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("task-custom-attributes-values-detail", args=[task.id]) + custom_attrs_val = f.TaskCustomAttributesValuesFactory( + task=task, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert task.__class__.objects.filter(id=task.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +def test_task_custom_attributes_values_delete_us(client): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, + is_owner=True) + + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("tasks-detail", args=[task.id]) + custom_attrs_val = f.TaskCustomAttributesValuesFactory( + task=task, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert not task.__class__.objects.filter(id=task.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py new file mode 100644 index 00000000..8db3f395 --- /dev/null +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -0,0 +1,268 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + + +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db(transaction=True) + + +######################################################### +# User Story Custom Attributes +######################################################### + +def test_userstory_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.UserStoryCustomAttributeFactory() + custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_owner=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_owner=True) + + + url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# User Story Custom Attributes Values +######################################################### + +def test_userstory_custom_attributes_values_list(client): + member = f.MembershipFactory(is_owner=True) + + url = reverse("userstory-custom-attributes-values-list") + + client.login(member.user) + response = client.json.get(url) + assert response.status_code == 404 + + +def test_userstory_custom_attributes_values_create(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("userstory-custom-attributes-values-list") + data = { + "user_story": user_story.id, + "values": { + ct1_id: "test_1", + ct2_id: "test_2" + }, + } + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert json.loads(response.data["values"]) == data["values"] + user_story = user_story.__class__.objects.get(id=user_story.id) + assert user_story.custom_attributes_values.values == data["values"] + + +def test_userstory_custom_attributes_values_create_with_error_invalid_key(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + + url = reverse("userstory-custom-attributes-values-list") + data = { + "user_story": user_story.id, + "values": { + ct1_id: "test_1", + "123456": "test_2" + }, + } + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attributes_values_update(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) + data = { + "values": { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert json.loads(response.data["values"]) == data["values"] + user_story = user_story.__class__.objects.get(id=user_story.id) + assert user_story.custom_attributes_values.values == data["values"] + + +def test_userstory_custom_attributes_values_update_with_error_invalid_key(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) + data = { + "values": { + ct1_id: "test_1_updated", + "123456": "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_userstory_custom_attributes_values_delete(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert user_story.__class__.objects.filter(id=user_story.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +def test_userstory_custom_attributes_values_delete_us(client): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + url = reverse("userstories-detail", args=[user_story.id]) + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert not user_story.__class__.objects.filter(id=user_story.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() From 32ff494ddd3560e41d821426e0b58cc62b3a2470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 13 Feb 2015 17:59:51 +0100 Subject: [PATCH 07/28] US #55: Custom fields - :three: :weary: :tiger: --- ...0003_triggers_on_delete_customattribute.py | 87 +++++++++++++++++++ .../test_custom_attributes_issues.py | 34 ++++++++ .../test_custom_attributes_tasks.py | 34 ++++++++ .../test_custom_attributes_user_stories.py | 34 ++++++++ 4 files changed, 189 insertions(+) create mode 100644 taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py diff --git a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py new file mode 100644 index 00000000..a733bceb --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_attributes', '0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues'), + ] + + operations = [ + # Function: Remove a key in a json field + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + RETURNS json + LANGUAGE sql + IMMUTABLE + STRICT + AS $function$ + SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}') + FROM json_each("json") + WHERE "key" <> ALL ("keys_to_delete")), + '{}')::json $function$; + """, + reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys";""" + ), + + # Function: Romeve a key in the json field of *_custom_attributes_values.values + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"() + RETURNS trigger + AS $clean_key_in_custom_attributes_values$ + DECLARE + key text; + tablename text; + BEGIN + key := OLD.id::text; + tablename := TG_ARGV[0]::text; + + EXECUTE 'UPDATE ' || quote_ident(tablename) || ' + SET values = json_object_delete_keys(values, ' || quote_literal(key) || ')'; + + RETURN NULL; + END; $clean_key_in_custom_attributes_values$ + LANGUAGE plpgsql; + + """, + reverse_sql="""DROP FUNCTION IF EXISTS "clean_key_in_custom_attributes_values";""" + ), + + # Trigger: Clean userstorycustomattributes values before remove a userstorycustomattribute + migrations.RunSQL( + """ + CREATE TRIGGER "update_userstorycustomvalues_afeter_remove_userstorycustomattribute" + BEFORE DELETE ON custom_attributes_userstorycustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_userstorycustomattributesvalues'); + """, + reverse_sql="""DROP TRIGGER "update_userstorycustomvalues_afeter_remove_userstorycustomattribute";""" + ), + + # Trigger: Clean taskcustomattributes values before remove a taskcustomattribute + migrations.RunSQL( + """ + CREATE TRIGGER "update_taskcustomvalues_afeter_remove_taskcustomattribute" + BEFORE DELETE ON custom_attributes_taskcustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_taskcustomattributesvalues'); + """, + reverse_sql="""DROP TRIGGER "update_taskcustomvalues_afeter_remove_taskcustomattribute";""" + ), + + # Trigger: Clean issuecustomattributes values before remove a issuecustomattribute + migrations.RunSQL( + """ + CREATE TRIGGER "update_issuecustomvalues_afeter_remove_issuecustomattribute" + BEFORE DELETE ON custom_attributes_issuecustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_issuecustomattributesvalues'); + """, + reverse_sql="""DROP TRIGGER "update_issuecustomvalues_afeter_remove_issuecustomattribute";""" + ) + ] diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 50e689c1..40f22725 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -269,3 +269,37 @@ def test_issue_custom_attributes_values_delete_us(client): assert response.status_code == 204 assert not issue.__class__.objects.filter(id=issue.id).exists() assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +######################################################### +# Test tristres triggers :-P +######################################################### + +def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(): + issue = f.IssueFactory() + member = f.MembershipFactory(user=issue.project.owner, + project=issue.project, + is_owner=True) + + custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.IssueCustomAttributesValuesFactory( + project=issue.project, + issue=issue, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id in custom_attrs_val.values.keys() + + custom_attr_2.delete() + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id not in custom_attrs_val.values.keys() diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index f6786737..6740427d 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -266,3 +266,37 @@ def test_task_custom_attributes_values_delete_us(client): assert response.status_code == 204 assert not task.__class__.objects.filter(id=task.id).exists() assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +######################################################### +# Test tristres triggers :-P +######################################################### + +def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + project=user_story.project, + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id in custom_attrs_val.values.keys() + + custom_attr_2.delete() + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id not in custom_attrs_val.values.keys() diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index 8db3f395..b0ad7e9b 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -266,3 +266,37 @@ def test_userstory_custom_attributes_values_delete_us(client): assert response.status_code == 204 assert not user_story.__class__.objects.filter(id=user_story.id).exists() assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +######################################################### +# Test tristres triggers :-P +######################################################### + +def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(): + user_story = f.UserStoryFactory() + member = f.MembershipFactory(user=user_story.project.owner, + project=user_story.project, + is_owner=True) + + custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( + project=user_story.project, + user_story=user_story, + values= { + ct1_id: "test_1", + ct2_id: "test_2" + }, + ) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id in custom_attrs_val.values.keys() + + custom_attr_2.delete() + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + + assert ct1_id in custom_attrs_val.values.keys() + assert ct2_id not in custom_attrs_val.values.keys() From 56b9fe42e3554817d81adf91d3b1dbf62246f3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 17 Feb 2015 11:19:31 +0100 Subject: [PATCH 08/28] US #55: Custom fields - Import/Export custom attributes values --- taiga/export_import/serializers.py | 114 +++++++++++++-- taiga/export_import/service.py | 190 ++++++++++++++++--------- tests/integration/test_importer_api.py | 79 ++++++++++ 3 files changed, 304 insertions(+), 79 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 2893537b..69f84b14 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -333,6 +333,93 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') +class CustomAttributesValuesExportSerializerMixin: + custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") + + def custom_attributes_queryset(self, project): + raise NotImplementedError() + + def get_custom_attributes_values(self, obj): + def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): + ret = {} + for attr in custom_attributes: + value = values.get(str(attr["id"]), None) + if value is not None: + ret[attr["name"]] = value + + return ret + + try: + values = obj.custom_attributes_values.values + custom_attributes = self.custom_attribute_queryset(obj.project).values('id', 'name') + + return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) + except ObjectDoesNotExist: + return None + + +class BaseCustomAttributesValuesExportSerializer: + values = JsonField(source="values", label="values", required=True) + _custom_attribute_model = None + _container_field = None + + def validate_values(self, attrs, source): + # values must be a dict + data_values = attrs.get("values", None) + if self.object: + data_values = (data_values or self.object.values) + + if type(data_values) is not dict: + raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) + + # Values keys must be in the container object project + data_container = attrs.get(self._container_field, None) + if data_container: + project_id = data_container.project_id + elif self.object: + project_id = getattr(self.object, self._container_field).project_id + else: + project_id = None + + values_ids = list(data_values.keys()) + qs = self._custom_attribute_model.objects.filter(project=project_id, + id__in=values_ids) + if qs.count() != len(values_ids): + raise ValidationError(_("It contain invalid custom fields.")) + + return attrs + +class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, + serializers.ModelSerializer): + _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute + _container_model = "userstories.UserStory" + _container_field = "user_story" + + class Meta: + model = custom_attributes_models.UserStoryCustomAttributesValues + exclude = ("id",) + + +class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, + serializers.ModelSerializer): + _custom_attribute_model = custom_attributes_models.TaskCustomAttribute + _container_field = "task" + + class Meta: + model = custom_attributes_models.TaskCustomAttributesValues + exclude = ("id",) + + +class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, + serializers.ModelSerializer): + _custom_attribute_model = custom_attributes_models.IssueCustomAttribute + _container_field = "issue" + + class Meta: + model = custom_attributes_models.IssueCustomAttributesValues + exclude = ("id",) + + class MembershipExportSerializer(serializers.ModelSerializer): user = UserRelatedField(required=False) role = ProjectRelatedField(slug_field="name") @@ -382,8 +469,8 @@ class MilestoneExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - serializers.ModelSerializer): +class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) @@ -396,9 +483,12 @@ class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSeriali model = tasks_models.Task exclude = ('id', 'project') + def custom_attributes_queryset(self, project): + return project.taskcustomattributes.all() -class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - serializers.ModelSerializer): + +class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, serializers.ModelSerializer): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) @@ -412,9 +502,12 @@ class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSe model = userstories_models.UserStory exclude = ('id', 'project', 'points', 'tasks') + def custom_attributes_queryset(self, project): + return project.userstorycustomattributes.all() -class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - serializers.ModelSerializer): + +class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -426,13 +519,16 @@ class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerial votes = serializers.SerializerMethodField("get_votes") modified_date = serializers.DateTimeField(required=False) - def get_votes(self, obj): - return [x.email for x in votes_service.get_voters(obj)] - class Meta: model = issues_models.Issue exclude = ('id', 'project') + def get_votes(self, obj): + return [x.email for x in votes_service.get_voters(obj)] + + def custom_attributes_queryset(self, project): + return project.issuecustomattributes.all() + class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 0d8aff03..55ac48c8 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -108,6 +108,30 @@ def store_custom_attributes(project, data, field, serializer): result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer)) return result +def store_custom_attributes_values(obj, data_values, obj_field, serializer_class): + data = { + obj_field: obj.id, + "values": data_values, + } + + serializer = serializer_class(data=data) + if serializer.is_valid(): + serializer.save() + return serializer + + add_errors("custom_attributes_values", serializer.errors) + return None + + +def _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, values): + ret = {} + for attr in custom_attributes: + value = values.get(attr["name"], None) + if value is not None: + ret[str(attr["id"])] = value + + return ret + def store_role(project, role): serialized = serializers.RoleExportSerializer(data=role) @@ -122,7 +146,7 @@ def store_role(project, role): def store_roles(project, data): results = [] - for role in data.get('roles', []): + for role in data.get("roles", []): results.append(store_role(project, role)) return results @@ -164,16 +188,16 @@ def store_membership(project, membership): def store_memberships(project, data): results = [] - for membership in data.get('memberships', []): + for membership in data.get("memberships", []): results.append(store_membership(project, membership)) return results -def store_task(project, task): - if 'status' not in task and project.default_task_status: - task['status'] = project.default_task_status.name +def store_task(project, data): + if "status" not in data and project.default_task_status: + data["status"] = project.default_task_status.name - serialized = serializers.TaskExportSerializer(data=task, context={"project": project}) + serialized = serializers.TaskExportSerializer(data=data, context={"project": project}) if serialized.is_valid(): serialized.object.project = project if serialized.object.owner is None: @@ -192,12 +216,20 @@ def store_task(project, task): serialized.object.ref, _ = refs.make_reference(serialized.object, project) serialized.object.save() - for task_attachment in task.get('attachments', []): + for task_attachment in data.get("attachments", []): store_attachment(project, serialized.object, task_attachment) - for history in task.get('history', []): + for history in data.get("history", []): store_history(project, serialized.object, history) + custom_attributes_values = data.get("custom_attributes_values", None) + if custom_attributes_values: + custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name') + custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + store_custom_attributes_values(serialized.object, custom_attributes_values, + "task", serializers.TaskCustomAttributesValuesExportSerializer) + return serialized add_errors("tasks", serialized.errors) @@ -211,8 +243,8 @@ def store_milestone(project, milestone): serialized.object._importing = True serialized.save() - for task_without_us in milestone.get('tasks_without_us', []): - task_without_us['user_story'] = None + for task_without_us in milestone.get("tasks_without_us", []): + task_without_us["user_story"] = None store_task(project, task_without_us) return serialized @@ -251,7 +283,7 @@ def store_history(project, obj, history): def store_wiki_page(project, wiki_page): - wiki_page['slug'] = slugify(unidecode(wiki_page.get('slug', ''))) + wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", ""))) serialized = serializers.WikiPageExportSerializer(data=wiki_page) if serialized.is_valid(): serialized.object.project = project @@ -261,10 +293,10 @@ def store_wiki_page(project, wiki_page): serialized.object._not_notify = True serialized.save() - for attachment in wiki_page.get('attachments', []): + for attachment in wiki_page.get("attachments", []): store_attachment(project, serialized.object, attachment) - for history in wiki_page.get('history', []): + for history in wiki_page.get("history", []): store_history(project, serialized.object, history) return serialized @@ -295,61 +327,12 @@ def store_role_point(project, us, role_point): return None -def store_user_story(project, userstory): - if 'status' not in userstory and project.default_us_status: - userstory['status'] = project.default_us_status.name +def store_user_story(project, data): + if "status" not in data and project.default_us_status: + data["status"] = project.default_us_status.name - userstory_data = {} - for key, value in userstory.items(): - if key != 'role_points': - userstory_data[key] = value - serialized_us = serializers.UserStoryExportSerializer(data=userstory_data, context={"project": project}) - if serialized_us.is_valid(): - serialized_us.object.project = project - if serialized_us.object.owner is None: - serialized_us.object.owner = serialized_us.object.project.owner - serialized_us.object._importing = True - serialized_us.object._not_notify = True - - serialized_us.save() - - if serialized_us.object.ref: - sequence_name = refs.make_sequence_name(project) - if not seq.exists(sequence_name): - seq.create(sequence_name) - seq.set_max(sequence_name, serialized_us.object.ref) - else: - serialized_us.object.ref, _ = refs.make_reference(serialized_us.object, project) - serialized_us.object.save() - - for us_attachment in userstory.get('attachments', []): - store_attachment(project, serialized_us.object, us_attachment) - - for role_point in userstory.get('role_points', []): - store_role_point(project, serialized_us.object, role_point) - - for history in userstory.get('history', []): - store_history(project, serialized_us.object, history) - - return serialized_us - add_errors("user_stories", serialized_us.errors) - return None - - -def store_issue(project, data): - serialized = serializers.IssueExportSerializer(data=data, context={"project": project}) - - if 'type' not in data and project.default_issue_type: - data['type'] = project.default_issue_type.name - - if 'status' not in data and project.default_issue_status: - data['status'] = project.default_issue_status.name - - if 'priority' not in data and project.default_priority: - data['priority'] = project.default_priority.name - - if 'severity' not in data and project.default_severity: - data['severity'] = project.default_severity.name + data_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} + serialized = serializers.UserStoryExportSerializer(data=data_data, context={"project": project}) if serialized.is_valid(): serialized.object.project = project @@ -369,10 +352,77 @@ def store_issue(project, data): serialized.object.ref, _ = refs.make_reference(serialized.object, project) serialized.object.save() - for attachment in data.get('attachments', []): - store_attachment(project, serialized.object, attachment) - for history in data.get('history', []): + for us_attachment in data.get("attachments", []): + store_attachment(project, serialized.object, us_attachment) + + for role_point in data.get("role_points", []): + store_role_point(project, serialized.object, role_point) + + for history in data.get("history", []): store_history(project, serialized.object, history) + + custom_attributes_values = data.get("custom_attributes_values", None) + if custom_attributes_values: + custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name') + custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + store_custom_attributes_values(serialized.object, custom_attributes_values, + "user_story", serializers.UserStoryCustomAttributesValuesExportSerializer) + return serialized + + add_errors("user_stories", serialized.errors) + return None + + +def store_issue(project, data): + serialized = serializers.IssueExportSerializer(data=data, context={"project": project}) + + if "type" not in data and project.default_issue_type: + data["type"] = project.default_issue_type.name + + if "status" not in data and project.default_issue_status: + data["status"] = project.default_issue_status.name + + if "priority" not in data and project.default_priority: + data["priority"] = project.default_priority.name + + if "severity" not in data and project.default_severity: + data["severity"] = project.default_severity.name + + if serialized.is_valid(): + serialized.object.project = project + if serialized.object.owner is None: + serialized.object.owner = serialized.object.project.owner + serialized.object._importing = True + serialized.object._not_notify = True + + serialized.save() + + if serialized.object.ref: + sequence_name = refs.make_sequence_name(project) + if not seq.exists(sequence_name): + seq.create(sequence_name) + seq.set_max(sequence_name, serialized.object.ref) + else: + serialized.object.ref, _ = refs.make_reference(serialized.object, project) + serialized.object.save() + + for attachment in data.get("attachments", []): + store_attachment(project, serialized.object, attachment) + + for history in data.get("history", []): + store_history(project, serialized.object, history) + + custom_attributes_values = data.get("custom_attributes_values", None) + if custom_attributes_values: + custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name') + custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + store_custom_attributes_values(serialized.object, custom_attributes_values, + "issue", serializers.IssueCustomAttributesValuesExportSerializer) + + return serialized + add_errors("issues", serialized.errors) return None diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 6231d4a9..5c654394 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -22,6 +22,8 @@ from django.core.files.base import ContentFile from .. import factories as f +from django.apps import apps + from taiga.base.utils import json from taiga.projects.models import Project from taiga.projects.issues.models import Issue @@ -256,6 +258,30 @@ def test_valid_user_story_import(client): assert response_data["finish_date"] == "2014-10-24T00:00:00+0000" +def test_valid_user_story_import_with_custom_attributes_values(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + membership = f.MembershipFactory(project=project, user=user, is_owner=True) + project.default_us_status = f.UserStoryStatusFactory.create(project=project) + project.save() + custom_attr = f.UserStoryCustomAttributeFactory(project=project) + + url = reverse("importer-us", args=[project.pk]) + data = { + "subject": "Test Custom Attrs Values User Story", + "custom_attributes_values": { + custom_attr.name: "test_value" + } + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + custom_attributes_values = apps.get_model("custom_attributes.UserStoryCustomAttributesValues").objects.get( + user_story__subject=response.data["subject"]) + assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + + def test_valid_issue_import_without_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -279,6 +305,33 @@ def test_valid_issue_import_without_extra_data(client): assert response_data["ref"] is not None +def test_valid_issue_import_with_custom_attributes_values(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + membership = f.MembershipFactory(project=project, user=user, is_owner=True) + project.default_issue_type = f.IssueTypeFactory.create(project=project) + project.default_issue_status = f.IssueStatusFactory.create(project=project) + project.default_severity = f.SeverityFactory.create(project=project) + project.default_priority = f.PriorityFactory.create(project=project) + project.save() + custom_attr = f.IssueCustomAttributeFactory(project=project) + + url = reverse("importer-issue", args=[project.pk]) + data = { + "subject": "Test Custom Attrs Values Issues", + "custom_attributes_values": { + custom_attr.name: "test_value" + } + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + custom_attributes_values = apps.get_model("custom_attributes.IssueCustomAttributesValues").objects.get( + issue__subject=response.data["subject"]) + assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + + def test_valid_issue_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -536,6 +589,30 @@ def test_valid_task_import_without_extra_data(client): assert response_data["ref"] is not None +def test_valid_task_import_with_custom_attributes_values(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + membership = f.MembershipFactory(project=project, user=user, is_owner=True) + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + custom_attr = f.TaskCustomAttributeFactory(project=project) + + url = reverse("importer-task", args=[project.pk]) + data = { + "subject": "Test Custom Attrs Values Tasks", + "custom_attributes_values": { + custom_attr.name: "test_value" + } + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + custom_attributes_values = apps.get_model("custom_attributes.TaskCustomAttributesValues").objects.get( + task__subject=response.data["subject"]) + assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + + def test_valid_task_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -735,6 +812,7 @@ def test_valid_wiki_link_import(client): json.loads(response.content.decode("utf-8")) + def test_invalid_milestone_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -766,6 +844,7 @@ def test_valid_milestone_import(client): json.loads(response.content.decode("utf-8")) + def test_milestone_import_duplicated_milestone(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) From eedd2ca7d51c11531f187efa0d5499203598b6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 18 Feb 2015 10:21:31 +0100 Subject: [PATCH 09/28] US #55: Custom fields - Use custom attributes and values in sample_data cvommand --- .../management/commands/sample_data.py | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index f88c2ff0..c8139797 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -34,6 +34,7 @@ from taiga.projects.tasks.models import * from taiga.projects.issues.models import * from taiga.projects.wiki.models import * from taiga.projects.attachments.models import * +from taiga.projects.custom_attributes.models import * from taiga.projects.history.services import take_snapshot from taiga.events.apps import disconnect_events_signals @@ -150,6 +151,27 @@ class Command(BaseCommand): if role.computable: computable_project_roles.add(role) + # added custom attributes + if self.sd.boolean: + for i in range(1, 4): + UserStoryCustomAttribute.objects.create(name=self.sd.words(1, 3), + description=self.sd.words(3, 12), + project=project, + order=i) + if self.sd.boolean: + for i in range(1, 4): + TaskCustomAttribute.objects.create(name=self.sd.words(1, 3), + description=self.sd.words(3, 12), + project=project, + order=i) + if self.sd.boolean: + for i in range(1, 4): + IssueCustomAttribute.objects.create(name=self.sd.words(1, 3), + description=self.sd.words(3, 12), + project=project, + order=i) + + if x < NUM_PROJECTS: start_date = now() - datetime.timedelta(55) @@ -248,6 +270,11 @@ class Command(BaseCommand): project=project)), tags=self.sd.words(1, 10).split(" ")) + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.issuecustomattributes.all() if self.sd.boolean()} + if custom_attributes_values: + IssueCustomAttributesValues.objects.create(issue=bug, + values=custom_attributes_values) + for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(bug, i+1) @@ -291,6 +318,11 @@ class Command(BaseCommand): task.save() + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.taskcustomattributes.all() if self.sd.boolean()} + if custom_attributes_values: + TaskCustomAttributesValues.objects.create(task=task, + values=custom_attributes_values) + for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(task, i+1) @@ -328,6 +360,12 @@ class Command(BaseCommand): role_points.save() + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.userstorycustomattributes.all() if self.sd.boolean()} + if custom_attributes_values: + UserStoryCustomAttributesValues.objects.create(user_story=us, + values=custom_attributes_values) + + for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(us, i+1) @@ -345,7 +383,7 @@ class Command(BaseCommand): take_snapshot(us, comment=self.sd.paragraph(), user=us.owner) - + return us def create_milestone(self, project, start_date, end_date): @@ -375,9 +413,9 @@ class Command(BaseCommand): def create_user(self, counter=None, username=None, full_name=None, email=None): counter = counter or self.sd.int() - username = username or 'user{0}'.format(counter) + username = username or "user{0}".format(counter) full_name = full_name or "{} {}".format(self.sd.name('es'), self.sd.surname('es', number=1)) - email = email or self.sd.email() + email = email or "user{0}@taigaio.demo".format(counter) user = User.objects.create(username=username, full_name=full_name, From d1d0825d2e4ad96bc001dfcea5d83e70b30f9331 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 18 Feb 2015 11:44:01 +0100 Subject: [PATCH 10/28] US #55: Custom fields - Adding info about custom fields to project serializer --- taiga/projects/serializers.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index ad0e99a7..ade6566f 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -35,7 +35,9 @@ from taiga.permissions.service import is_project_owner from . import models from . import services from . validators import ProjectExistsValidator - +from . custom_attributes.serializers import(UserStoryCustomAttributeSerializer, + TaskCustomAttributeSerializer, + IssueCustomAttributeSerializer) ###################################################### ## Custom values for selectors @@ -262,6 +264,9 @@ class ProjectSerializer(ModelSerializer): tags_colors = TagsColorsField(required=False) users = serializers.SerializerMethodField("get_users") total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") + userstory_custom_attributes = serializers.SerializerMethodField("get_userstory_custom_attributes") + task_custom_attributes = serializers.SerializerMethodField("get_task_custom_attributes") + issue_custom_attributes = serializers.SerializerMethodField("get_issue_custom_attributes") class Meta: model = models.Project @@ -298,6 +303,14 @@ class ProjectSerializer(ModelSerializer): raise serializers.ValidationError("Total milestones must be major or equal to zero") return attrs + def get_userstory_custom_attributes(self, obj): + return UserStoryCustomAttributeSerializer(obj.userstorycustomattributes.all(), many=True).data + + def get_task_custom_attributes(self, obj): + return TaskCustomAttributeSerializer(obj.taskcustomattributes.all(), many=True).data + + def get_issue_custom_attributes(self, obj): + return IssueCustomAttributeSerializer(obj.issuecustomattributes.all(), many=True).data class ProjectDetailSerializer(ProjectSerializer): roles = serializers.SerializerMethodField("get_roles") From 22e62b4f651f726423855ef5300708b703cd4ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 18 Feb 2015 12:08:50 +0100 Subject: [PATCH 11/28] US #55: Custom fields - Change 'values' field in CustomAttributesValues models to 'attributes_values' --- taiga/export_import/serializers.py | 10 +++--- taiga/export_import/service.py | 2 +- ...svalues_userstorycustomattributesvalues.py | 6 ++-- ...0003_triggers_on_delete_customattribute.py | 3 +- taiga/projects/custom_attributes/models.py | 2 +- .../projects/custom_attributes/serializers.py | 8 ++--- taiga/projects/history/freeze_impl.py | 12 +++---- .../management/commands/sample_data.py | 15 ++++---- tests/factories.py | 6 ++-- .../test_issues_custom_attributes_resource.py | 6 ++-- .../test_custom_attributes_issues.py | 35 +++++++++---------- .../test_custom_attributes_tasks.py | 35 +++++++++---------- .../test_custom_attributes_user_stories.py | 35 +++++++++---------- tests/integration/test_importer_api.py | 6 ++-- 14 files changed, 91 insertions(+), 90 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 69f84b14..4027556d 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -350,7 +350,7 @@ class CustomAttributesValuesExportSerializerMixin: return ret try: - values = obj.custom_attributes_values.values + values = obj.custom_attributes_values.attributes_values custom_attributes = self.custom_attribute_queryset(obj.project).values('id', 'name') return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) @@ -359,15 +359,15 @@ class CustomAttributesValuesExportSerializerMixin: class BaseCustomAttributesValuesExportSerializer: - values = JsonField(source="values", label="values", required=True) + attributes_values = JsonField(source="attributes_values",required=True) _custom_attribute_model = None _container_field = None - def validate_values(self, attrs, source): + def validate_attributes_values(self, attrs, source): # values must be a dict - data_values = attrs.get("values", None) + data_values = attrs.get("attributes_values", None) if self.object: - data_values = (data_values or self.object.values) + data_values = (data_values or self.object.attributes_values) if type(data_values) is not dict: raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 55ac48c8..9c6b52c4 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -111,7 +111,7 @@ def store_custom_attributes(project, data, field, serializer): def store_custom_attributes_values(obj, data_values, obj_field, serializer_class): data = { obj_field: obj.id, - "values": data_values, + "attributes_values": data_values, } serializer = serializer_class(data=data) diff --git a/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py index 4c7d2461..dd9ed428 100644 --- a/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py +++ b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), ('issue', models.OneToOneField(related_name='custom_attributes_values', to='issues.Issue', verbose_name='issue')), ], options={ @@ -36,7 +36,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), ('task', models.OneToOneField(related_name='custom_attributes_values', to='tasks.Task', verbose_name='task')), ], options={ @@ -52,7 +52,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), ('user_story', models.OneToOneField(related_name='custom_attributes_values', to='userstories.UserStory', verbose_name='user story')), ], options={ diff --git a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py index a733bceb..fc237167 100644 --- a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py +++ b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py @@ -42,7 +42,8 @@ class Migration(migrations.Migration): tablename := TG_ARGV[0]::text; EXECUTE 'UPDATE ' || quote_ident(tablename) || ' - SET values = json_object_delete_keys(values, ' || quote_literal(key) || ')'; + SET attributes_values = json_object_delete_keys(attributes_values, ' || + quote_literal(key) || ')'; RETURN NULL; END; $clean_key_in_custom_attributes_values$ diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 287498d4..2fc0cf5c 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -78,7 +78,7 @@ class IssueCustomAttribute(AbstractCustomAttribute): ####################################################### class AbstractCustomAttributesValues(OCCModelMixin, models.Model): - values = JsonField(null=False, blank=False, default={}, verbose_name=_("values")) + attributes_values = JsonField(null=False, blank=False, default={}, verbose_name=_("attributes_values")) class Meta: abstract = True diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 40a25ccc..b295af16 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -76,15 +76,15 @@ class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): class BaseCustomAttributesValuesSerializer: - values = JsonField(source="values", label="values", required=True) + attributes_values = JsonField(source="attributes_values", label="attributes values", required=True) _custom_attribute_model = None _container_field = None - def validate_values(self, attrs, source): + def validate_attributes_values(self, attrs, source): # values must be a dict - data_values = attrs.get("values", None) + data_values = attrs.get("attributes_values", None) if self.object: - data_values = (data_values or self.object.values) + data_values = (data_values or self.object.attributes_values) if type(data_values) is not dict: raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 1e67df09..54c9eefb 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -188,37 +188,37 @@ def extract_attachments(obj) -> list: @as_tuple def extract_user_story_custom_attributes(obj) -> list: with suppress(ObjectDoesNotExist): - custom_attributes_values = obj.custom_attributes_values.values + custom_attributes_values = obj.custom_attributes_values.attributes_values for attr in obj.project.userstorycustomattributes.all(): with suppress(KeyError): value = custom_attributes_values[str(attr.id)] yield {"id": attr.id, "name": attr.name, - "values": value} + "value": value} @as_tuple def extract_task_custom_attributes(obj) -> list: with suppress(ObjectDoesNotExist): - custom_attributes_values = obj.custom_attributes_values.values + custom_attributes_values = obj.custom_attributes_values.attributes_values for attr in obj.project.taskcustomattributes.all(): with suppress(KeyError): value = custom_attributes_values[str(attr.id)] yield {"id": attr.id, "name": attr.name, - "values": value} + "value": value} @as_tuple def extract_issue_custom_attributes(obj) -> list: with suppress(ObjectDoesNotExist): - custom_attributes_values = obj.custom_attributes_values.values + custom_attributes_values = obj.custom_attributes_values.attributes_values for attr in obj.project.issuecustomattributes.all(): with suppress(KeyError): value = custom_attributes_values[str(attr.id)] yield {"id": attr.id, "name": attr.name, - "values": value} + "value": value} def project_freezer(project) -> dict: diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index c8139797..1e5331a0 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -270,10 +270,11 @@ class Command(BaseCommand): project=project)), tags=self.sd.words(1, 10).split(" ")) - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.issuecustomattributes.all() if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.issuecustomattributes.all() + if self.sd.boolean()} if custom_attributes_values: IssueCustomAttributesValues.objects.create(issue=bug, - values=custom_attributes_values) + attributes_values=custom_attributes_values) for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(bug, i+1) @@ -318,10 +319,11 @@ class Command(BaseCommand): task.save() - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.taskcustomattributes.all() if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.taskcustomattributes.all() + if self.sd.boolean()} if custom_attributes_values: TaskCustomAttributesValues.objects.create(task=task, - values=custom_attributes_values) + attributes_values=custom_attributes_values) for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(task, i+1) @@ -360,10 +362,11 @@ class Command(BaseCommand): role_points.save() - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.userstorycustomattributes.all() if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.userstorycustomattributes.all() + if self.sd.boolean()} if custom_attributes_values: UserStoryCustomAttributesValues.objects.create(user_story=us, - values=custom_attributes_values) + attributes_values=custom_attributes_values) for i in range(self.sd.int(*NUM_ATTACHMENTS)): diff --git a/tests/factories.py b/tests/factories.py index e8f70e7d..4e9b9d0c 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -389,7 +389,7 @@ class UserStoryCustomAttributesValuesFactory(Factory): model = "custom_attributes.UserStoryCustomAttributesValues" strategy = factory.CREATE_STRATEGY - values = {} + attributes_values = {} user_story = factory.SubFactory("tests.factories.UserStoryFactory") @@ -398,7 +398,7 @@ class TaskCustomAttributesValuesFactory(Factory): model = "custom_attributes.TaskCustomAttributesValues" strategy = factory.CREATE_STRATEGY - values = {} + attributes_values = {} task = factory.SubFactory("tests.factories.TaskFactory") @@ -407,7 +407,7 @@ class IssueCustomAttributesValuesFactory(Factory): model = "custom_attributes.IssueCustomAttributesValues" strategy = factory.CREATE_STRATEGY - values = {} + attributes_values = {} issue = factory.SubFactory("tests.factories.IssueFactory") diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 39caad51..2af987ad 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -102,15 +102,15 @@ def data(): #m.public_issue_cav = f.IssueCustomAttributesValuesFactory(project=m.public_project, # issue=f.IssueFactory(project=m.public_project, # owner=m.project_owner), - # values={str(m.public_issue_ca.id):"test"}) + # attributes_values={str(m.public_issue_ca.id):"test"}) #m.private_issue_cav1 = f.IssueCustomAttributesValuesFactory(project=m.private_project1, # issue=f.IssueFactory(project=m.private_project1, # owner=m.project_owner), - # values={str(m.private_issue_ca1.id):"test"}) + # attributes_values={str(m.private_issue_ca1.id):"test"}) #m.private_issue_cav2 = f.IssueCustomAttributesValuesFactory(project=m.private_project2, # issue=f.IssueFactory(project=m.private_project2, # owner=m.project_owner), - # values={str(m.private_issue_ca2.id):"test"}) + # attributes_values={str(m.private_issue_ca2.id):"test"}) return m diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 40f22725..568f82c9 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -107,7 +107,7 @@ def test_issue_custom_attributes_values_create(client): url = reverse("issue-custom-attributes-values-list") data = { "issue": issue.id, - "values": { + "attributes_values": { ct1_id: "test_1", ct2_id: "test_2" }, @@ -117,9 +117,9 @@ def test_issue_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] issue = issue.__class__.objects.get(id=issue.id) - assert issue.custom_attributes_values.values == data["values"] + assert issue.custom_attributes_values.attributes_values == data["attributes_values"] def test_issue_custom_attributes_values_create_with_error_invalid_key(client): @@ -135,7 +135,7 @@ def test_issue_custom_attributes_values_create_with_error_invalid_key(client): url = reverse("issue-custom-attributes-values-list") data = { "issue": issue.id, - "values": { + "attributes_values": { ct1_id: "test_1", "123456": "test_2" }, @@ -160,7 +160,7 @@ def test_issue_custom_attributes_values_update(client): custom_attrs_val = f.IssueCustomAttributesValuesFactory( issue=issue, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -168,7 +168,7 @@ def test_issue_custom_attributes_values_update(client): url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", ct2_id: "test_2_updated" }, @@ -179,9 +179,9 @@ def test_issue_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] issue = issue.__class__.objects.get(id=issue.id) - assert issue.custom_attributes_values.values == data["values"] + assert issue.custom_attributes_values.attributes_values == data["attributes_values"] def test_issue_custom_attributes_values_update_with_error_invalid_key(client): @@ -197,7 +197,7 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client): custom_attrs_val = f.IssueCustomAttributesValuesFactory( issue=issue, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -205,7 +205,7 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client): url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", "123456": "test_2_updated" }, @@ -231,7 +231,7 @@ def test_issue_custom_attributes_values_delete(client): url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) custom_attrs_val = f.IssueCustomAttributesValuesFactory( issue=issue, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -258,7 +258,7 @@ def test_issue_custom_attributes_values_delete_us(client): url = reverse("issues-detail", args=[issue.id]) custom_attrs_val = f.IssueCustomAttributesValuesFactory( issue=issue, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -287,19 +287,18 @@ def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(): ct2_id = "{}".format(custom_attr_2.id) custom_attrs_val = f.IssueCustomAttributesValuesFactory( - project=issue.project, issue=issue, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, ) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id in custom_attrs_val.attributes_values.keys() custom_attr_2.delete() custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id not in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index 6740427d..33ebce5d 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -107,7 +107,7 @@ def test_task_custom_attributes_values_create(client): url = reverse("task-custom-attributes-values-list") data = { "task": task.id, - "values": { + "attributes_values": { ct1_id: "test_1", ct2_id: "test_2" }, @@ -116,9 +116,9 @@ def test_task_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] task = task.__class__.objects.get(id=task.id) - assert task.custom_attributes_values.values == data["values"] + assert task.custom_attributes_values.attributes_values == data["attributes_values"] def test_task_custom_attributes_values_create_with_error_invalid_key(client): @@ -134,7 +134,7 @@ def test_task_custom_attributes_values_create_with_error_invalid_key(client): url = reverse("task-custom-attributes-values-list") data = { "task": task.id, - "values": { + "attributes_values": { ct1_id: "test_1", "123456": "test_2" }, @@ -158,7 +158,7 @@ def test_task_custom_attributes_values_update(client): custom_attrs_val = f.TaskCustomAttributesValuesFactory( task=task, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -166,7 +166,7 @@ def test_task_custom_attributes_values_update(client): url = reverse("task-custom-attributes-values-detail", args=[task.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", ct2_id: "test_2_updated" }, @@ -176,9 +176,9 @@ def test_task_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] task = task.__class__.objects.get(id=task.id) - assert task.custom_attributes_values.values == data["values"] + assert task.custom_attributes_values.attributes_values == data["attributes_values"] def test_task_custom_attributes_values_update_with_error_invalid_key(client): @@ -194,14 +194,14 @@ def test_task_custom_attributes_values_update_with_error_invalid_key(client): custom_attrs_val = f.TaskCustomAttributesValuesFactory( task=task, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, ) url = reverse("task-custom-attributes-values-detail", args=[task.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", "123456": "test_2_updated" }, @@ -228,7 +228,7 @@ def test_task_custom_attributes_values_delete(client): url = reverse("task-custom-attributes-values-detail", args=[task.id]) custom_attrs_val = f.TaskCustomAttributesValuesFactory( task=task, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -255,7 +255,7 @@ def test_task_custom_attributes_values_delete_us(client): url = reverse("tasks-detail", args=[task.id]) custom_attrs_val = f.TaskCustomAttributesValuesFactory( task=task, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -284,19 +284,18 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri ct2_id = "{}".format(custom_attr_2.id) custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - project=user_story.project, user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, ) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id in custom_attrs_val.attributes_values.keys() custom_attr_2.delete() custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id not in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index b0ad7e9b..f6f355ff 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -107,7 +107,7 @@ def test_userstory_custom_attributes_values_create(client): url = reverse("userstory-custom-attributes-values-list") data = { "user_story": user_story.id, - "values": { + "attributes_values": { ct1_id: "test_1", ct2_id: "test_2" }, @@ -116,9 +116,9 @@ def test_userstory_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] user_story = user_story.__class__.objects.get(id=user_story.id) - assert user_story.custom_attributes_values.values == data["values"] + assert user_story.custom_attributes_values.attributes_values == data["attributes_values"] def test_userstory_custom_attributes_values_create_with_error_invalid_key(client): @@ -134,7 +134,7 @@ def test_userstory_custom_attributes_values_create_with_error_invalid_key(client url = reverse("userstory-custom-attributes-values-list") data = { "user_story": user_story.id, - "values": { + "attributes_values": { ct1_id: "test_1", "123456": "test_2" }, @@ -158,7 +158,7 @@ def test_userstory_custom_attributes_values_update(client): custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -166,7 +166,7 @@ def test_userstory_custom_attributes_values_update(client): url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", ct2_id: "test_2_updated" }, @@ -176,9 +176,9 @@ def test_userstory_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["values"]) == data["values"] + assert json.loads(response.data["attributes_values"]) == data["attributes_values"] user_story = user_story.__class__.objects.get(id=user_story.id) - assert user_story.custom_attributes_values.values == data["values"] + assert user_story.custom_attributes_values.attributes_values == data["attributes_values"] def test_userstory_custom_attributes_values_update_with_error_invalid_key(client): @@ -194,7 +194,7 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -202,7 +202,7 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) data = { - "values": { + "attributes_values": { ct1_id: "test_1_updated", "123456": "test_2_updated" }, @@ -228,7 +228,7 @@ def test_userstory_custom_attributes_values_delete(client): url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -255,7 +255,7 @@ def test_userstory_custom_attributes_values_delete_us(client): url = reverse("userstories-detail", args=[user_story.id]) custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, @@ -284,19 +284,18 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri ct2_id = "{}".format(custom_attr_2.id) custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - project=user_story.project, user_story=user_story, - values= { + attributes_values= { ct1_id: "test_1", ct2_id: "test_2" }, ) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id in custom_attrs_val.attributes_values.keys() custom_attr_2.delete() custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) - assert ct1_id in custom_attrs_val.values.keys() - assert ct2_id not in custom_attrs_val.values.keys() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 5c654394..9cbb64c0 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -279,7 +279,7 @@ def test_valid_user_story_import_with_custom_attributes_values(client): assert response.status_code == 201 custom_attributes_values = apps.get_model("custom_attributes.UserStoryCustomAttributesValues").objects.get( user_story__subject=response.data["subject"]) - assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"} def test_valid_issue_import_without_extra_data(client): @@ -329,7 +329,7 @@ def test_valid_issue_import_with_custom_attributes_values(client): assert response.status_code == 201 custom_attributes_values = apps.get_model("custom_attributes.IssueCustomAttributesValues").objects.get( issue__subject=response.data["subject"]) - assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"} def test_valid_issue_import_with_extra_data(client): @@ -610,7 +610,7 @@ def test_valid_task_import_with_custom_attributes_values(client): assert response.status_code == 201 custom_attributes_values = apps.get_model("custom_attributes.TaskCustomAttributesValues").objects.get( task__subject=response.data["subject"]) - assert custom_attributes_values.values == {str(custom_attr.id): "test_value"} + assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"} def test_valid_task_import_with_extra_data(client): From 5eddda980316eacf31494921bb258a262bbfa487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 18 Feb 2015 13:02:45 +0100 Subject: [PATCH 12/28] US #55: Custom fields - Fix a problem with serializer classes and mixins --- taiga/export_import/serializers.py | 15 ++++++--------- taiga/projects/custom_attributes/serializers.py | 6 +++--- .../integration/test_custom_attributes_issues.py | 4 ++-- tests/integration/test_custom_attributes_tasks.py | 4 ++-- .../test_custom_attributes_user_stories.py | 4 ++-- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 4027556d..e3996e59 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -333,7 +333,7 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class CustomAttributesValuesExportSerializerMixin: +class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") def custom_attributes_queryset(self, project): @@ -351,14 +351,14 @@ class CustomAttributesValuesExportSerializerMixin: try: values = obj.custom_attributes_values.attributes_values - custom_attributes = self.custom_attribute_queryset(obj.project).values('id', 'name') + custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) except ObjectDoesNotExist: return None -class BaseCustomAttributesValuesExportSerializer: +class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): attributes_values = JsonField(source="attributes_values",required=True) _custom_attribute_model = None _container_field = None @@ -389,8 +389,7 @@ class BaseCustomAttributesValuesExportSerializer: return attrs -class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, - serializers.ModelSerializer): +class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute _container_model = "userstories.UserStory" _container_field = "user_story" @@ -400,8 +399,7 @@ class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValues exclude = ("id",) -class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, - serializers.ModelSerializer): +class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.TaskCustomAttribute _container_field = "task" @@ -410,8 +408,7 @@ class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExpor exclude = ("id",) -class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer, - serializers.ModelSerializer): +class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.IssueCustomAttribute _container_field = "issue" diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index b295af16..06f46ec2 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -75,8 +75,8 @@ class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): ####################################################### -class BaseCustomAttributesValuesSerializer: - attributes_values = JsonField(source="attributes_values", label="attributes values", required=True) +class BaseCustomAttributesValuesSerializer(ModelSerializer): + attributes_values = JsonField(source="attributes_values", label="attributes values") _custom_attribute_model = None _container_field = None @@ -107,7 +107,7 @@ class BaseCustomAttributesValuesSerializer: return attrs -class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): +class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): _custom_attribute_model = models.UserStoryCustomAttribute _container_model = "userstories.UserStory" _container_field = "user_story" diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 568f82c9..505fae5c 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -117,7 +117,7 @@ def test_issue_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] issue = issue.__class__.objects.get(id=issue.id) assert issue.custom_attributes_values.attributes_values == data["attributes_values"] @@ -179,7 +179,7 @@ def test_issue_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] issue = issue.__class__.objects.get(id=issue.id) assert issue.custom_attributes_values.attributes_values == data["attributes_values"] diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index 33ebce5d..1a11bc17 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -116,7 +116,7 @@ def test_task_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] task = task.__class__.objects.get(id=task.id) assert task.custom_attributes_values.attributes_values == data["attributes_values"] @@ -176,7 +176,7 @@ def test_task_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] task = task.__class__.objects.get(id=task.id) assert task.custom_attributes_values.attributes_values == data["attributes_values"] diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index f6f355ff..ab1fb5a3 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -116,7 +116,7 @@ def test_userstory_custom_attributes_values_create(client): client.login(member.user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] user_story = user_story.__class__.objects.get(id=user_story.id) assert user_story.custom_attributes_values.attributes_values == data["attributes_values"] @@ -176,7 +176,7 @@ def test_userstory_custom_attributes_values_update(client): client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert json.loads(response.data["attributes_values"]) == data["attributes_values"] + assert response.data["attributes_values"] == data["attributes_values"] user_story = user_story.__class__.objects.get(id=user_story.id) assert user_story.custom_attributes_values.attributes_values == data["attributes_values"] From df8c532cbd1ceb42a6c458d96f2f4edf22c7d189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 18 Feb 2015 15:45:01 +0100 Subject: [PATCH 13/28] US #55: Custom fields - Exclude id in CustomAttributesValues serializers --- taiga/export_import/serializers.py | 12 ++++++------ taiga/projects/custom_attributes/serializers.py | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index e3996e59..0af1ae52 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -363,6 +363,9 @@ class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): _custom_attribute_model = None _container_field = None + class Meta: + exclude = ("id",) + def validate_attributes_values(self, attrs, source): # values must be a dict data_values = attrs.get("attributes_values", None) @@ -394,27 +397,24 @@ class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValues _container_model = "userstories.UserStory" _container_field = "user_story" - class Meta: + class Meta(BaseCustomAttributesValuesExportSerializer.Meta): model = custom_attributes_models.UserStoryCustomAttributesValues - exclude = ("id",) class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.TaskCustomAttribute _container_field = "task" - class Meta: + class Meta(BaseCustomAttributesValuesExportSerializer.Meta): model = custom_attributes_models.TaskCustomAttributesValues - exclude = ("id",) class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.IssueCustomAttribute _container_field = "issue" - class Meta: + class Meta(BaseCustomAttributesValuesExportSerializer.Meta): model = custom_attributes_models.IssueCustomAttributesValues - exclude = ("id",) class MembershipExportSerializer(serializers.ModelSerializer): diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 06f46ec2..0a7d4a65 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -80,6 +80,9 @@ class BaseCustomAttributesValuesSerializer(ModelSerializer): _custom_attribute_model = None _container_field = None + class Meta: + exclude = ("id",) + def validate_attributes_values(self, attrs, source): # values must be a dict data_values = attrs.get("attributes_values", None) @@ -112,7 +115,7 @@ class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerial _container_model = "userstories.UserStory" _container_field = "user_story" - class Meta: + class Meta(BaseCustomAttributesValuesSerializer.Meta): model = models.UserStoryCustomAttributesValues @@ -120,7 +123,7 @@ class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, _custom_attribute_model = models.TaskCustomAttribute _container_field = "task" - class Meta: + class Meta(BaseCustomAttributesValuesSerializer.Meta): model = models.TaskCustomAttributesValues @@ -128,5 +131,5 @@ class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer _custom_attribute_model = models.IssueCustomAttribute _container_field = "issue" - class Meta: + class Meta(BaseCustomAttributesValuesSerializer.Meta): model = models.IssueCustomAttributesValues From 3369fa41f0f914f7375ab56ac0427d2f405d20aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 18 Feb 2015 23:17:07 +0100 Subject: [PATCH 14/28] US #55: Custom fields - Fix custo attribute serializer validator of name --- taiga/projects/custom_attributes/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 0a7d4a65..b52a15be 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -41,14 +41,18 @@ class BaseCustomAttributeSerializer(ModelSerializer): - update the name - update the project (move to another project) """ + data_id = data.get("id", None) data_name = data.get("name", None) data_project = data.get("project", None) + if self.object: + data_id = data_id or self.object.id data_name = data_name or self.object.name data_project = data_project or self.object.project model = self.Meta.model - qs = model.objects.filter(project=data_project, name=data_name) + qs = (model.objects.filter(project=data_project, name=data_name) + .exclude(id=data_id)) if qs.exists(): raise ValidationError(_("There is a custom field with the same name in this project.")) From e8078aaaa5a3c044c27a9203adc7080bfee707ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 19 Feb 2015 12:56:07 +0100 Subject: [PATCH 15/28] US #55: Custom fields - Create empty CustomAttributesValues when added USs, Tasks and Issues --- taiga/base/api/__init__.py | 2 + taiga/base/api/viewsets.py | 5 + taiga/export_import/serializers.py | 1 + taiga/export_import/service.py | 9 +- taiga/projects/custom_attributes/api.py | 13 +- ...svalues_userstorycustomattributesvalues.py | 26 ++-- ...0003_triggers_on_delete_customattribute.py | 24 ++- ...stomattributesvalues_for_existen_object.py | 83 ++++++++++ .../projects/custom_attributes/permissions.py | 6 - taiga/projects/custom_attributes/signals.py | 35 +++++ taiga/projects/issues/apps.py | 6 + .../management/commands/sample_data.py | 22 +-- taiga/projects/tasks/apps.py | 6 + taiga/projects/userstories/apps.py | 6 + .../test_issues_custom_attributes_resource.py | 141 ----------------- .../test_custom_attributes_issues.py | 130 ++-------------- .../test_custom_attributes_tasks.py | 143 +++-------------- .../test_custom_attributes_user_stories.py | 147 ++---------------- 18 files changed, 238 insertions(+), 567 deletions(-) create mode 100644 taiga/projects/custom_attributes/migrations/0004_create_empty_customattributesvalues_for_existen_object.py create mode 100644 taiga/projects/custom_attributes/signals.py diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py index 973f17ef..845821b2 100644 --- a/taiga/base/api/__init__.py +++ b/taiga/base/api/__init__.py @@ -19,10 +19,12 @@ from .viewsets import ModelListViewSet from .viewsets import ModelCrudViewSet +from .viewsets import ModelUpdateRetrieveViewSet from .viewsets import GenericViewSet from .viewsets import ReadOnlyListViewSet __all__ = ["ModelCrudViewSet", "ModelListViewSet", + "ModelUpdateRetrieveViewSet", "GenericViewSet", "ReadOnlyListViewSet"] diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index b2fbdd50..cad36dcd 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -168,3 +168,8 @@ class ModelListViewSet(pagination.HeadersPaginationMixin, mixins.ListModelMixin, GenericViewSet): pass + +class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin, + mixins.RetrieveModelMixin, + GenericViewSet): + pass diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 0af1ae52..7b27c427 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -22,6 +22,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 9c6b52c4..a5c9e22b 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -20,6 +20,7 @@ from unidecode import unidecode from django.template.defaultfilters import slugify from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist from taiga.projects.history.services import make_key_from_model_object from taiga.projects.references import sequences as seq @@ -108,13 +109,19 @@ def store_custom_attributes(project, data, field, serializer): result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer)) return result + def store_custom_attributes_values(obj, data_values, obj_field, serializer_class): data = { obj_field: obj.id, "attributes_values": data_values, } - serializer = serializer_class(data=data) + try: + custom_attributes_values = obj.custom_attributes_values + serializer = serializer_class(custom_attributes_values, data=data) + except ObjectDoesNotExist: + serializer = serializer_class(data=data) + if serializer.is_valid(): serializer.save() return serializer diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 7e4033ab..c1edfe33 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -17,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _ from taiga.base.api import ModelCrudViewSet -from taiga.base.api.viewsets import ModelViewSet +from taiga.base.api import ModelUpdateRetrieveViewSet from taiga.base import exceptions as exc from taiga.base import filters from taiga.base import response @@ -73,16 +73,7 @@ class IssueCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): # Custom Attributes Values ViewSets ####################################################### -class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, ModelViewSet): - def list(self, request, *args, **kwargs): - return response.NotFound() - - def post_delete(self, obj): - # NOTE: When destroy a custom attributes values object, the - # content_object change after and not before - self.persist_history_snapshot(obj, delete=True) - super().pre_delete(obj) - +class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, ModelUpdateRetrieveViewSet): def get_object_for_snapshot(self, obj): return getattr(obj, self.content_object) diff --git a/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py index dd9ed428..8c1848db 100644 --- a/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py +++ b/taiga/projects/custom_attributes/migrations/0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues.py @@ -9,8 +9,8 @@ class Migration(migrations.Migration): dependencies = [ ('tasks', '0005_auto_20150114_0954'), - ('userstories', '0009_remove_userstory_is_archived'), ('issues', '0004_auto_20150114_0954'), + ('userstories', '0009_remove_userstory_is_archived'), ('custom_attributes', '0001_initial'), ] @@ -18,14 +18,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='IssueCustomAttributesValues', fields=[ - ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), - ('issue', models.OneToOneField(related_name='custom_attributes_values', to='issues.Issue', verbose_name='issue')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')), + ('issue', models.OneToOneField(verbose_name='issue', to='issues.Issue', related_name='custom_attributes_values')), ], options={ - 'ordering': ['id'], 'verbose_name_plural': 'issue custom attributes values', + 'ordering': ['id'], 'verbose_name': 'issue ustom attributes values', 'abstract': False, }, @@ -34,14 +34,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TaskCustomAttributesValues', fields=[ - ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), - ('task', models.OneToOneField(related_name='custom_attributes_values', to='tasks.Task', verbose_name='task')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')), + ('task', models.OneToOneField(verbose_name='task', to='tasks.Task', related_name='custom_attributes_values')), ], options={ - 'ordering': ['id'], 'verbose_name_plural': 'task custom attributes values', + 'ordering': ['id'], 'verbose_name': 'task ustom attributes values', 'abstract': False, }, @@ -50,14 +50,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserStoryCustomAttributesValues', fields=[ - ('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)), + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), ('version', models.IntegerField(default=1, verbose_name='version')), - ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes values')), - ('user_story', models.OneToOneField(related_name='custom_attributes_values', to='userstories.UserStory', verbose_name='user story')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')), + ('user_story', models.OneToOneField(verbose_name='user story', to='userstories.UserStory', related_name='custom_attributes_values')), ], options={ - 'ordering': ['id'], 'verbose_name_plural': 'user story custom attributes values', + 'ordering': ['id'], 'verbose_name': 'user story ustom attributes values', 'abstract': False, }, diff --git a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py index fc237167..8a984a63 100644 --- a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py +++ b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py @@ -25,7 +25,8 @@ class Migration(migrations.Migration): WHERE "key" <> ALL ("keys_to_delete")), '{}')::json $function$; """, - reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys";""" + reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + CASCADE;""" ), # Function: Romeve a key in the json field of *_custom_attributes_values.values @@ -50,39 +51,46 @@ class Migration(migrations.Migration): LANGUAGE plpgsql; """, - reverse_sql="""DROP FUNCTION IF EXISTS "clean_key_in_custom_attributes_values";""" + reverse_sql="""DROP FUNCTION IF EXISTS "clean_key_in_custom_attributes_values"() + CASCADE;""" ), # Trigger: Clean userstorycustomattributes values before remove a userstorycustomattribute migrations.RunSQL( """ - CREATE TRIGGER "update_userstorycustomvalues_afeter_remove_userstorycustomattribute" + CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute" BEFORE DELETE ON custom_attributes_userstorycustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_userstorycustomattributesvalues'); """, - reverse_sql="""DROP TRIGGER "update_userstorycustomvalues_afeter_remove_userstorycustomattribute";""" + reverse_sql="""DROP TRIGGER IF EXISTS "update_userstorycustomvalues_after_remove_userstorycustomattribute" + ON custom_attributes_userstorycustomattribute + CASCADE;""" ), # Trigger: Clean taskcustomattributes values before remove a taskcustomattribute migrations.RunSQL( """ - CREATE TRIGGER "update_taskcustomvalues_afeter_remove_taskcustomattribute" + CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute" BEFORE DELETE ON custom_attributes_taskcustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_taskcustomattributesvalues'); """, - reverse_sql="""DROP TRIGGER "update_taskcustomvalues_afeter_remove_taskcustomattribute";""" + reverse_sql="""DROP TRIGGER IF EXISTS "update_taskcustomvalues_after_remove_taskcustomattribute" + ON custom_attributes_taskcustomattribute + CASCADE;""" ), # Trigger: Clean issuecustomattributes values before remove a issuecustomattribute migrations.RunSQL( """ - CREATE TRIGGER "update_issuecustomvalues_afeter_remove_issuecustomattribute" + CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute" BEFORE DELETE ON custom_attributes_issuecustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_issuecustomattributesvalues'); """, - reverse_sql="""DROP TRIGGER "update_issuecustomvalues_afeter_remove_issuecustomattribute";""" + reverse_sql="""DROP TRIGGER IF EXISTS "update_issuecustomvalues_after_remove_issuecustomattribute" + ON custom_attributes_issuecustomattribute + CASCADE;""" ) ] diff --git a/taiga/projects/custom_attributes/migrations/0004_create_empty_customattributesvalues_for_existen_object.py b/taiga/projects/custom_attributes/migrations/0004_create_empty_customattributesvalues_for_existen_object.py new file mode 100644 index 00000000..9d807ad4 --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0004_create_empty_customattributesvalues_for_existen_object.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +def create_empty_user_story_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "UserStoryCustomAttributesValues") + obj_model = apps.get_model("userstories", "UserStory") + db_alias = schema_editor.connection.alias + + data = [] + for user_story in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"): + if not hasattr(user_story, "custom_attributes_values"): + data.append(cav_model(user_story=user_story,attributes_values={})) + + cav_model.objects.using(db_alias).bulk_create(data) + + +def delete_empty_user_story_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "UserStoryCustomAttributesValues") + db_alias = schema_editor.connection.alias + + cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete() + + +def create_empty_task_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "TaskCustomAttributesValues") + obj_model = apps.get_model("tasks", "Task") + db_alias = schema_editor.connection.alias + + data = [] + for task in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"): + if not hasattr(task, "custom_attributes_values"): + data.append(cav_model(task=task,attributes_values={})) + + cav_model.objects.using(db_alias).bulk_create(data) + + +def delete_empty_task_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "TaskCustomAttributesValues") + db_alias = schema_editor.connection.alias + + cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete() + + +def create_empty_issues_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "IssueCustomAttributesValues") + obj_model = apps.get_model("issues", "Issue") + db_alias = schema_editor.connection.alias + + data = [] + for issue in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"): + if not hasattr(issue, "custom_attributes_values"): + data.append(cav_model(issue=issue,attributes_values={})) + + cav_model.objects.using(db_alias).bulk_create(data) + + +def delete_empty_issue_custom_attrributes_values(apps, schema_editor): + cav_model = apps.get_model("custom_attributes", "IssueCustomAttributesValues") + db_alias = schema_editor.connection.alias + + cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_attributes', '0003_triggers_on_delete_customattribute'), + ] + + operations = [ + migrations.RunPython(create_empty_user_story_custom_attrributes_values, + reverse_code=delete_empty_user_story_custom_attrributes_values, + atomic=True), + migrations.RunPython(create_empty_task_custom_attrributes_values, + reverse_code=delete_empty_task_custom_attrributes_values, + atomic=True), + migrations.RunPython(create_empty_issues_custom_attrributes_values, + reverse_code=delete_empty_issue_custom_attrributes_values, + atomic=True), + ] diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 617e90b0..160d340e 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -66,24 +66,18 @@ class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission): enought_perms = IsProjectOwner() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') - create_perms = HasProjectPerm('add_us') update_perms = HasProjectPerm('modify_us') - destroy_perms = HasProjectPerm('delete_us') class TaskCustomAttributesValuesPermission(TaigaResourcePermission): enought_perms = IsProjectOwner() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') - create_perms = HasProjectPerm('add_task') update_perms = HasProjectPerm('modify_task') - destroy_perms = HasProjectPerm('delete_task') class IssueCustomAttributesValuesPermission(TaigaResourcePermission): enought_perms = IsProjectOwner() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') - create_perms = HasProjectPerm('add_issue') update_perms = HasProjectPerm('modify_issue') - destroy_perms = HasProjectPerm('delete_issue') diff --git a/taiga/projects/custom_attributes/signals.py b/taiga/projects/custom_attributes/signals.py new file mode 100644 index 00000000..fa90bb10 --- /dev/null +++ b/taiga/projects/custom_attributes/signals.py @@ -0,0 +1,35 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +from . import models + + +def create_custom_attribute_value_when_create_user_story(sender, instance, created, **kwargs): + if created: + models.UserStoryCustomAttributesValues.objects.get_or_create(user_story=instance, + defaults={"attributes_values":{}}) + + +def create_custom_attribute_value_when_create_task(sender, instance, created, **kwargs): + if created: + models.TaskCustomAttributesValues.objects.get_or_create(task=instance, + defaults={"attributes_values":{}}) + + +def create_custom_attribute_value_when_create_issue(sender, instance, created, **kwargs): + if created: + models.IssueCustomAttributesValues.objects.get_or_create(issue=instance, + defaults={"attributes_values":{}}) diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 4b8714ff..3972ef48 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -19,6 +19,7 @@ from django.apps import apps from django.db.models import signals from taiga.projects import signals as generic_handlers +from taiga.projects.custom_attributes import signals as custom_attributes_handlers from . import signals as handlers @@ -39,3 +40,8 @@ class IssuesAppConfig(AppConfig): sender=apps.get_model("issues", "Issue")) signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, sender=apps.get_model("issues", "Issue")) + + # Custom Attributes + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue, + sender=apps.get_model("issues", "Issue"), + dispatch_uid="create_custom_attribute_value_when_create_issue") diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 1e5331a0..b8d86b75 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -270,11 +270,13 @@ class Command(BaseCommand): project=project)), tags=self.sd.words(1, 10).split(" ")) - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.issuecustomattributes.all() + bug.save() + + custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.issuecustomattributes.all() if self.sd.boolean()} if custom_attributes_values: - IssueCustomAttributesValues.objects.create(issue=bug, - attributes_values=custom_attributes_values) + bug.custom_attributes_values.attributes_values = custom_attributes_values + bug.custom_attributes_values.save() for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(bug, i+1) @@ -319,11 +321,11 @@ class Command(BaseCommand): task.save() - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.taskcustomattributes.all() + custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.taskcustomattributes.all() if self.sd.boolean()} if custom_attributes_values: - TaskCustomAttributesValues.objects.create(task=task, - attributes_values=custom_attributes_values) + task.custom_attributes_values.attributes_values = custom_attributes_values + task.custom_attributes_values.save() for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(task, i+1) @@ -362,11 +364,13 @@ class Command(BaseCommand): role_points.save() - custom_attributes_values = {str(ca.id): self.sd.paragraph() for ca in project.userstorycustomattributes.all() + us.save() + + custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.userstorycustomattributes.all() if self.sd.boolean()} if custom_attributes_values: - UserStoryCustomAttributesValues.objects.create(user_story=us, - attributes_values=custom_attributes_values) + us.custom_attributes_values.attributes_values = custom_attributes_values + us.custom_attributes_values.save() for i in range(self.sd.int(*NUM_ATTACHMENTS)): diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 752560de..a6597339 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -19,6 +19,7 @@ from django.apps import apps from django.db.models import signals from taiga.projects import signals as generic_handlers +from taiga.projects.custom_attributes import signals as custom_attributes_handlers from . import signals as handlers @@ -44,3 +45,8 @@ class TasksAppConfig(AppConfig): sender=apps.get_model("tasks", "Task")) signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, sender=apps.get_model("tasks", "Task")) + + # Custom Attributes + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task, + sender=apps.get_model("tasks", "Task"), + dispatch_uid="create_custom_attribute_value_when_create_task") diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index 299f1cfc..f6b1bb77 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -19,6 +19,7 @@ from django.apps import apps from django.db.models import signals from taiga.projects import signals as generic_handlers +from taiga.projects.custom_attributes import signals as custom_attributes_handlers from . import signals as handlers @@ -52,3 +53,8 @@ class UserStoriesAppConfig(AppConfig): sender=apps.get_model("userstories", "UserStory")) signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, sender=apps.get_model("userstories", "UserStory")) + + # Custom Attributes + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story, + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="create_custom_attribute_value_when_create_user_story") diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 2af987ad..694bfc36 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -95,23 +95,6 @@ def data(): m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) - #m.public_issue = f.IssueFactory(project=m.public_project, owner=m.project_owner) - #m.private_issue1 = f.IssueFactory(project=m.private_project1, owner=m.project_owner) - #m.private_issue2 = f.IssueFactory(project=m.private_project2, owner=m.project_owner) - - #m.public_issue_cav = f.IssueCustomAttributesValuesFactory(project=m.public_project, - # issue=f.IssueFactory(project=m.public_project, - # owner=m.project_owner), - # attributes_values={str(m.public_issue_ca.id):"test"}) - #m.private_issue_cav1 = f.IssueCustomAttributesValuesFactory(project=m.private_project1, - # issue=f.IssueFactory(project=m.private_project1, - # owner=m.project_owner), - # attributes_values={str(m.private_issue_ca1.id):"test"}) - #m.private_issue_cav2 = f.IssueCustomAttributesValuesFactory(project=m.private_project2, - # issue=f.IssueFactory(project=m.private_project2, - # owner=m.project_owner), - # attributes_values={str(m.private_issue_ca2.id):"test"}) - return m @@ -302,127 +285,3 @@ def test_issue_custom_attribute_action_bulk_update_order(client, data): }) results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] - - -######################################################### -# Issue Custom Attributes Values -######################################################### - -#def test_issue_custom_attributes_values_retrieve(client, data): -# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) -# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) -# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# results = helper_test_http_method(client, 'get', public_url, None, users) -# assert results == [200, 200, 200, 200, 200] -# results = helper_test_http_method(client, 'get', private1_url, None, users) -# assert results == [200, 200, 200, 200, 200] -# results = helper_test_http_method(client, 'get', private2_url, None, users) -# assert results == [401, 403, 403, 200, 200] -# -# -#def test_issue_custom_attributes_values_update(client, data): -# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) -# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) -# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) -# -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.public_issue_cav).data -# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.public_issue_ca.id) -# issue_cav_data = json.dumps(issue_cav_data) -# results = helper_test_http_method(client, 'put', public_url, issue_cav_data, users) -# assert results == [401, 403, 403, 403, 200] -# -# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav1).data -# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.private_issue_ca1.id) -# issue_cav_data = json.dumps(issue_cav_data) -# results = helper_test_http_method(client, 'put', private1_url, issue_cav_data, users) -# assert results == [401, 403, 403, 403, 200] -# -# issue_cav_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav2).data -# issue_cav_data["values"] = '{{"{}":"test-update"}}'.format(data.private_issue_ca2.id) -# issue_cav_data = json.dumps(issue_cav_data) -# results = helper_test_http_method(client, 'put', private2_url, issue_cav_data, users) -# assert results == [401, 403, 403, 403, 200] -# -# -#def test_issue_custom_attributes_values_delete(client, data): -# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) -# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) -# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) -# -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# results = helper_test_http_method(client, 'delete', public_url, None, users) -# assert results == [401, 403, 403, 403, 204] -# results = helper_test_http_method(client, 'delete', private1_url, None, users) -# assert results == [401, 403, 403, 403, 204] -# results = helper_test_http_method(client, 'delete', private2_url, None, users) -# assert results == [401, 403, 403, 403, 204] -# -# -#def test_issue_custom_attributes_values_list(client, data): -# url = reverse('issue-custom-attributes-values-list') -# -# response = client.json.get(url) -# assert response.status_code == 404 -# -# client.login(data.registered_user) -# response = client.json.get(url) -# assert response.status_code == 404 -# -# client.login(data.project_member_without_perms) -# response = client.json.get(url) -# assert response.status_code == 404 -# -# client.login(data.project_member_with_perms) -# response = client.json.get(url) -# assert response.status_code == 404 -# -# client.login(data.project_owner) -# response = client.json.get(url) -# assert response.status_code == 404 -# -# -#def test_issue_custom_attributes_values_patch(client, data): -# public_url = reverse('issue-custom-attributes-values-detail', args=[data.public_issue_cav.issue.id]) -# private1_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav1.issue.id]) -# private2_url = reverse('issue-custom-attributes-values-detail', args=[data.private_issue_cav2.issue.id]) -# -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# results = helper_test_http_method(client, 'patch', public_url, -# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.public_issue_ca.id), users) -# assert results == [401, 403, 403, 403, 200] -# results = helper_test_http_method(client, 'patch', private1_url, -# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.private_issue_ca1.id), users) -# assert results == [401, 403, 403, 403, 200] -# results = helper_test_http_method(client, 'patch', private2_url, -# '{{"values": {{"{}": "test-update"}}, "version": 1}}'.format(data.private_issue_ca2.id), users) -# assert results == [401, 403, 403, 403, 200] diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 505fae5c..b33b7e71 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -21,7 +21,7 @@ from taiga.base.utils import json from .. import factories as f import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db ######################################################### @@ -83,68 +83,9 @@ def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(cl # Issue Custom Attributes Values ######################################################### -def test_issue_custom_attributes_values_list(client): - member = f.MembershipFactory(is_owner=True) - - url = reverse("issue-custom-attributes-values-list") - - client.login(member.user) - response = client.json.get(url) - assert response.status_code == 404 - - -def test_issue_custom_attributes_values_create(client): +def test_issue_custom_attributes_values_when_create_us(client): issue = f.IssueFactory() - member = f.MembershipFactory(user=issue.project.owner, - project=issue.project, - is_owner=True) - - custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) - ct2_id = "{}".format(custom_attr_2.id) - - url = reverse("issue-custom-attributes-values-list") - data = { - "issue": issue.id, - "attributes_values": { - ct1_id: "test_1", - ct2_id: "test_2" - }, - } - - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 201 - assert response.data["attributes_values"] == data["attributes_values"] - issue = issue.__class__.objects.get(id=issue.id) - assert issue.custom_attributes_values.attributes_values == data["attributes_values"] - - -def test_issue_custom_attributes_values_create_with_error_invalid_key(client): - issue = f.IssueFactory() - member = f.MembershipFactory(user=issue.project.owner, - project=issue.project, - is_owner=True) - - custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) - - url = reverse("issue-custom-attributes-values-list") - data = { - "issue": issue.id, - "attributes_values": { - ct1_id: "test_1", - "123456": "test_2" - }, - } - - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 + assert issue.custom_attributes_values.attributes_values == {} def test_issue_custom_attributes_values_update(client): @@ -158,13 +99,7 @@ def test_issue_custom_attributes_values_update(client): custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.IssueCustomAttributesValuesFactory( - issue=issue, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = issue.custom_attributes_values url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) data = { @@ -176,6 +111,7 @@ def test_issue_custom_attributes_values_update(client): } + assert issue.custom_attributes_values.attributes_values == {} client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -193,15 +129,8 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client): custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) - ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.IssueCustomAttributesValuesFactory( - issue=issue, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = issue.custom_attributes_values url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) data = { @@ -216,8 +145,7 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client): response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - -def test_issue_custom_attributes_values_delete(client): +def test_issue_custom_attributes_values_delete_issue(client): issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, @@ -228,41 +156,9 @@ def test_issue_custom_attributes_values_delete(client): custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) ct2_id = "{}".format(custom_attr_2.id) - url = reverse("issue-custom-attributes-values-detail", args=[issue.id]) - custom_attrs_val = f.IssueCustomAttributesValuesFactory( - issue=issue, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) - - client.login(member.user) - response = client.json.delete(url) - assert response.status_code == 204 - assert issue.__class__.objects.filter(id=issue.id).exists() - assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() - - -def test_issue_custom_attributes_values_delete_us(client): - issue = f.IssueFactory() - member = f.MembershipFactory(user=issue.project.owner, - project=issue.project, - is_owner=True) - - custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) - ct2_id = "{}".format(custom_attr_2.id) + custom_attrs_val = issue.custom_attributes_values url = reverse("issues-detail", args=[issue.id]) - custom_attrs_val = f.IssueCustomAttributesValuesFactory( - issue=issue, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) client.login(member.user) response = client.json.delete(url) @@ -286,13 +182,9 @@ def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(): custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.IssueCustomAttributesValuesFactory( - issue=issue, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = issue.custom_attributes_values + custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"} + custom_attrs_val.save() assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index 1a11bc17..ecb909de 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -21,7 +21,7 @@ from taiga.base.utils import json from .. import factories as f import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db ######################################################### @@ -83,66 +83,9 @@ def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(cli # Task Custom Attributes Values ######################################################### -def test_task_custom_attributes_values_list(client): - member = f.MembershipFactory(is_owner=True) - - url = reverse("task-custom-attributes-values-list") - - client.login(member.user) - response = client.json.get(url) - assert response.status_code == 404 - - -def test_task_custom_attributes_values_create(client): +def test_task_custom_attributes_values_when_create_us(client): task = f.TaskFactory() - member = f.MembershipFactory(user=task.project.owner, - project=task.project, - is_owner=True) - - custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) - ct2_id = "{}".format(custom_attr_2.id) - - url = reverse("task-custom-attributes-values-list") - data = { - "task": task.id, - "attributes_values": { - ct1_id: "test_1", - ct2_id: "test_2" - }, - } - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 201 - assert response.data["attributes_values"] == data["attributes_values"] - task = task.__class__.objects.get(id=task.id) - assert task.custom_attributes_values.attributes_values == data["attributes_values"] - - -def test_task_custom_attributes_values_create_with_error_invalid_key(client): - task = f.TaskFactory() - member = f.MembershipFactory(user=task.project.owner, - project=task.project, - is_owner=True) - - custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) - - url = reverse("task-custom-attributes-values-list") - data = { - "task": task.id, - "attributes_values": { - ct1_id: "test_1", - "123456": "test_2" - }, - } - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 + assert task.custom_attributes_values.attributes_values == {} def test_task_custom_attributes_values_update(client): @@ -156,13 +99,7 @@ def test_task_custom_attributes_values_update(client): custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.TaskCustomAttributesValuesFactory( - task=task, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = task.custom_attributes_values url = reverse("task-custom-attributes-values-detail", args=[task.id]) data = { @@ -173,6 +110,7 @@ def test_task_custom_attributes_values_update(client): "version": custom_attrs_val.version } + assert task.custom_attributes_values.attributes_values == {} client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -190,15 +128,9 @@ def test_task_custom_attributes_values_update_with_error_invalid_key(client): custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) - ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.TaskCustomAttributesValuesFactory( - task=task, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = task.custom_attributes_values + url = reverse("task-custom-attributes-values-detail", args=[task.id]) data = { "attributes_values": { @@ -208,13 +140,13 @@ def test_task_custom_attributes_values_update_with_error_invalid_key(client): "version": custom_attrs_val.version } - + assert task.custom_attributes_values.attributes_values == {} client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 -def test_task_custom_attributes_values_delete(client): +def test_task_custom_attributes_values_delete_task(client): task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, @@ -225,41 +157,9 @@ def test_task_custom_attributes_values_delete(client): custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) ct2_id = "{}".format(custom_attr_2.id) - url = reverse("task-custom-attributes-values-detail", args=[task.id]) - custom_attrs_val = f.TaskCustomAttributesValuesFactory( - task=task, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) - - client.login(member.user) - response = client.json.delete(url) - assert response.status_code == 204 - assert task.__class__.objects.filter(id=task.id).exists() - assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() - - -def test_task_custom_attributes_values_delete_us(client): - task = f.TaskFactory() - member = f.MembershipFactory(user=task.project.owner, - project=task.project, - is_owner=True) - - custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) - ct2_id = "{}".format(custom_attr_2.id) + custom_attrs_val = task.custom_attributes_values url = reverse("tasks-detail", args=[task.id]) - custom_attrs_val = f.TaskCustomAttributesValuesFactory( - task=task, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) client.login(member.user) response = client.json.delete(url) @@ -272,24 +172,21 @@ def test_task_custom_attributes_values_delete_us(client): # Test tristres triggers :-P ######################################################### -def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(): - user_story = f.UserStoryFactory() - member = f.MembershipFactory(user=user_story.project.owner, - project=user_story.project, +def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(): + task = f.TaskFactory() + member = f.MembershipFactory(user=task.project.owner, + project=task.project, is_owner=True) - custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) + custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) + custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = task.custom_attributes_values + + custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"} + custom_attrs_val.save() assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index ab1fb5a3..362f5497 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -21,7 +21,7 @@ from taiga.base.utils import json from .. import factories as f import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db ######################################################### @@ -83,66 +83,9 @@ def test_userstory_custom_attribute_duplicate_name_error_on_move_between_project # User Story Custom Attributes Values ######################################################### -def test_userstory_custom_attributes_values_list(client): - member = f.MembershipFactory(is_owner=True) - - url = reverse("userstory-custom-attributes-values-list") - - client.login(member.user) - response = client.json.get(url) - assert response.status_code == 404 - - -def test_userstory_custom_attributes_values_create(client): +def test_userstory_custom_attributes_values_when_create_us(client): user_story = f.UserStoryFactory() - member = f.MembershipFactory(user=user_story.project.owner, - project=user_story.project, - is_owner=True) - - custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct2_id = "{}".format(custom_attr_2.id) - - url = reverse("userstory-custom-attributes-values-list") - data = { - "user_story": user_story.id, - "attributes_values": { - ct1_id: "test_1", - ct2_id: "test_2" - }, - } - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 201 - assert response.data["attributes_values"] == data["attributes_values"] - user_story = user_story.__class__.objects.get(id=user_story.id) - assert user_story.custom_attributes_values.attributes_values == data["attributes_values"] - - -def test_userstory_custom_attributes_values_create_with_error_invalid_key(client): - user_story = f.UserStoryFactory() - member = f.MembershipFactory(user=user_story.project.owner, - project=user_story.project, - is_owner=True) - - custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) - - url = reverse("userstory-custom-attributes-values-list") - data = { - "user_story": user_story.id, - "attributes_values": { - ct1_id: "test_1", - "123456": "test_2" - }, - } - - client.login(member.user) - response = client.json.post(url, json.dumps(data)) - assert response.status_code == 400 + assert user_story.custom_attributes_values.attributes_values == {} def test_userstory_custom_attributes_values_update(client): @@ -156,13 +99,7 @@ def test_userstory_custom_attributes_values_update(client): custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = user_story.custom_attributes_values url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) data = { @@ -173,6 +110,7 @@ def test_userstory_custom_attributes_values_update(client): "version": custom_attrs_val.version } + assert user_story.custom_attributes_values.attributes_values == {} client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -190,15 +128,8 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct1_id = "{}".format(custom_attr_1.id) custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = user_story.custom_attributes_values url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) data = { @@ -209,65 +140,12 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client "version": custom_attrs_val.version } + assert user_story.custom_attributes_values.attributes_values == {} client.login(member.user) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 -def test_userstory_custom_attributes_values_delete(client): - user_story = f.UserStoryFactory() - member = f.MembershipFactory(user=user_story.project.owner, - project=user_story.project, - is_owner=True) - - custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct2_id = "{}".format(custom_attr_2.id) - - url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id]) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) - - client.login(member.user) - response = client.json.delete(url) - assert response.status_code == 204 - assert user_story.__class__.objects.filter(id=user_story.id).exists() - assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() - - -def test_userstory_custom_attributes_values_delete_us(client): - user_story = f.UserStoryFactory() - member = f.MembershipFactory(user=user_story.project.owner, - project=user_story.project, - is_owner=True) - - custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct1_id = "{}".format(custom_attr_1.id) - custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) - ct2_id = "{}".format(custom_attr_2.id) - - url = reverse("userstories-detail", args=[user_story.id]) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) - - client.login(member.user) - response = client.json.delete(url) - assert response.status_code == 204 - assert not user_story.__class__.objects.filter(id=user_story.id).exists() - assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() - - ######################################################### # Test tristres triggers :-P ######################################################### @@ -283,13 +161,10 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct2_id = "{}".format(custom_attr_2.id) - custom_attrs_val = f.UserStoryCustomAttributesValuesFactory( - user_story=user_story, - attributes_values= { - ct1_id: "test_1", - ct2_id: "test_2" - }, - ) + custom_attrs_val = user_story.custom_attributes_values + + custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"} + custom_attrs_val.save() assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() From 61c973057abd879465a9d801bb077c23810c2907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 19 Feb 2015 22:37:05 +0100 Subject: [PATCH 16/28] US #55: Custom fields - Fix mixins order in CustomAttribute ViewSets --- taiga/projects/custom_attributes/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index c1edfe33..d0868690 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -36,7 +36,7 @@ from . import services # Custom Attribute ViewSets ####################################################### -class UserStoryCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): +class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): model = models.UserStoryCustomAttribute serializer_class = serializers.UserStoryCustomAttributeSerializer permission_classes = (permissions.UserStoryCustomAttributePermission,) @@ -47,7 +47,7 @@ class UserStoryCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order -class TaskCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): +class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): model = models.TaskCustomAttribute serializer_class = serializers.TaskCustomAttributeSerializer permission_classes = (permissions.TaskCustomAttributePermission,) @@ -58,7 +58,7 @@ class TaskCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): bulk_update_order_action = services.bulk_update_task_custom_attribute_order -class IssueCustomAttributeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): +class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): model = models.IssueCustomAttribute serializer_class = serializers.IssueCustomAttributeSerializer permission_classes = (permissions.IssueCustomAttributePermission,) From 47a4c5e0e7cb4943f721033c564e1487954b5b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 20 Feb 2015 13:55:20 +0100 Subject: [PATCH 17/28] US #55: Custom fields - Fix a bug with the :tiger:s --- .../0003_triggers_on_delete_customattribute.py | 6 +++--- tests/integration/test_custom_attributes_issues.py | 12 ++++++++---- tests/integration/test_custom_attributes_tasks.py | 10 +++++++--- .../test_custom_attributes_user_stories.py | 9 ++++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py index 8a984a63..43285e38 100644 --- a/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py +++ b/taiga/projects/custom_attributes/migrations/0003_triggers_on_delete_customattribute.py @@ -59,7 +59,7 @@ class Migration(migrations.Migration): migrations.RunSQL( """ CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute" - BEFORE DELETE ON custom_attributes_userstorycustomattribute + AFTER DELETE ON custom_attributes_userstorycustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_userstorycustomattributesvalues'); """, @@ -72,7 +72,7 @@ class Migration(migrations.Migration): migrations.RunSQL( """ CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute" - BEFORE DELETE ON custom_attributes_taskcustomattribute + AFTER DELETE ON custom_attributes_taskcustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_taskcustomattributesvalues'); """, @@ -85,7 +85,7 @@ class Migration(migrations.Migration): migrations.RunSQL( """ CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute" - BEFORE DELETE ON custom_attributes_issuecustomattribute + AFTER DELETE ON custom_attributes_issuecustomattribute FOR EACH ROW EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_issuecustomattributesvalues'); """, diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index b33b7e71..65152aa5 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -15,6 +15,7 @@ # along with this program. If not, see . +from django.db.transaction import atomic from django.core.urlresolvers import reverse from taiga.base.utils import json @@ -171,12 +172,11 @@ def test_issue_custom_attributes_values_delete_issue(client): # Test tristres triggers :-P ######################################################### -def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(): +def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(client): issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, is_owner=True) - custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) @@ -189,8 +189,12 @@ def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(): assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() - custom_attr_2.delete() - custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists() assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index ecb909de..fee38830 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -172,7 +172,7 @@ def test_task_custom_attributes_values_delete_task(client): # Test tristres triggers :-P ######################################################### -def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(): +def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(client): task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, @@ -191,8 +191,12 @@ def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(): assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() - custom_attr_2.delete() - custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists() assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index 362f5497..6e602269 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -150,7 +150,7 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client # Test tristres triggers :-P ######################################################### -def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(): +def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(client): user_story = f.UserStoryFactory() member = f.MembershipFactory(user=user_story.project.owner, project=user_story.project, @@ -169,8 +169,11 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id in custom_attrs_val.attributes_values.keys() - custom_attr_2.delete() - custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) assert ct1_id in custom_attrs_val.attributes_values.keys() assert ct2_id not in custom_attrs_val.attributes_values.keys() From 892f6f93dff7e00714152d8a94bb1ce02bc00dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 Feb 2015 09:29:31 +0100 Subject: [PATCH 18/28] US #55: Custom fields - Fix validations --- .../projects/custom_attributes/serializers.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index b52a15be..641c1996 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -34,16 +34,16 @@ class BaseCustomAttributeSerializer(ModelSerializer): class Meta: read_only_fields = ('id', 'created_date', 'modified_date') - def validate(self, data): + def _validate_integrity_between_project_and_name(self, attrs, source): """ Check the name is not duplicated in the project. Check when: - create a new one - update the name - update the project (move to another project) """ - data_id = data.get("id", None) - data_name = data.get("name", None) - data_project = data.get("project", None) + data_id = attrs.get("id", None) + data_name = attrs.get("name", None) + data_project = attrs.get("project", None) if self.object: data_id = data_id or self.object.id @@ -54,9 +54,15 @@ class BaseCustomAttributeSerializer(ModelSerializer): qs = (model.objects.filter(project=data_project, name=data_name) .exclude(id=data_id)) if qs.exists(): - raise ValidationError(_("There is a custom field with the same name in this project.")) + raise ValidationError(_("Already exists one with the same name.")) - return data + return attrs + + def validate_name(self, attrs, source): + return self._validate_integrity_between_project_and_name(attrs, source) + + def validate_project(self, attrs, source): + return self._validate_integrity_between_project_and_name(attrs, source) class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): From 9193c78de5125a26786b2609b77a1dcc49ff2d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 Feb 2015 19:46:01 +0100 Subject: [PATCH 19/28] US #55: Custom fields - Permissions tests over custom attributes values --- .../test_issues_custom_attributes_resource.py | 112 +++++++++++++++++- .../test_tasks_custom_attributes_resource.py | 106 ++++++++++++++++- ..._userstories_custom_attributes_resource.py | 111 ++++++++++++++++- 3 files changed, 326 insertions(+), 3 deletions(-) diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 694bfc36..8735fe5f 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -25,7 +25,7 @@ from tests import factories as f from tests.utils import helper_test_http_method import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db @pytest.fixture @@ -95,6 +95,29 @@ def data(): m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) + m.public_issue = f.IssueFactory(project=m.public_project, + status__project=m.public_project, + severity__project=m.public_project, + priority__project=m.public_project, + type__project=m.public_project, + milestone__project=m.public_project) + m.private_issue1 = f.IssueFactory(project=m.private_project1, + status__project=m.private_project1, + severity__project=m.private_project1, + priority__project=m.private_project1, + type__project=m.private_project1, + milestone__project=m.private_project1) + m.private_issue2 = f.IssueFactory(project=m.private_project2, + status__project=m.private_project2, + severity__project=m.private_project2, + priority__project=m.private_project2, + type__project=m.private_project2, + milestone__project=m.private_project2) + + m.public_issue_cav = m.public_issue.custom_attributes_values + m.private_issue_cav1 = m.private_issue1.custom_attributes_values + m.private_issue_cav2 = m.private_issue2.custom_attributes_values + return m @@ -285,3 +308,90 @@ def test_issue_custom_attribute_action_bulk_update_order(client, data): }) results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + + +######################################################### +# Issue Custom Attribute +######################################################### + + +def test_issue_custom_attributes_values_retrieve(client, data): + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_custom_attributes_values_update(client, data): + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_data = serializers.IssueCustomAttributesValuesSerializer(data.public_issue_cav).data + issue_data["attributes_values"] = {str(data.public_issue_ca.pk): "test"} + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav1).data + issue_data["attributes_values"] = {str(data.private_issue_ca1.pk): "test"} + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav2).data + issue_data["attributes_values"] = {str(data.private_issue_ca2.pk): "test"} + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_custom_attributes_values_patch(client, data): + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"attributes_values": {str(data.public_issue_ca.pk): "test"}, + "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_issue_ca1.pk): "test"}, + "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_issue_ca2.pk): "test"}, + "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 317ff991..c7562e86 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -25,7 +25,7 @@ from tests import factories as f from tests.utils import helper_test_http_method import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db @pytest.fixture @@ -95,6 +95,23 @@ def data(): m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1) m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2) + m.public_task = f.TaskFactory(project=m.public_project, + status__project=m.public_project, + milestone__project=m.public_project, + user_story__project=m.public_project) + m.private_task1 = f.TaskFactory(project=m.private_project1, + status__project=m.private_project1, + milestone__project=m.private_project1, + user_story__project=m.private_project1) + m.private_task2 = f.TaskFactory(project=m.private_project2, + status__project=m.private_project2, + milestone__project=m.private_project2, + user_story__project=m.private_project2) + + m.public_task_cav = m.public_task.custom_attributes_values + m.private_task_cav1 = m.private_task1.custom_attributes_values + m.private_task_cav2 = m.private_task2.custom_attributes_values + return m @@ -285,3 +302,90 @@ def test_task_custom_attribute_action_bulk_update_order(client, data): }) results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + + +######################################################### +# Task Custom Attribute +######################################################### + + +def test_task_custom_attributes_values_retrieve(client, data): + public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_custom_attributes_values_update(client, data): + public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_data = serializers.TaskCustomAttributesValuesSerializer(data.public_task_cav).data + task_data["attributes_values"] = {str(data.public_task_ca.pk): "test"} + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = serializers.TaskCustomAttributesValuesSerializer(data.private_task_cav1).data + task_data["attributes_values"] = {str(data.private_task_ca1.pk): "test"} + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = serializers.TaskCustomAttributesValuesSerializer(data.private_task_cav2).data + task_data["attributes_values"] = {str(data.private_task_ca2.pk): "test"} + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_custom_attributes_values_patch(client, data): + public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"attributes_values": {str(data.public_task_ca.pk): "test"}, + "version": data.public_task.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_task_ca1.pk): "test"}, + "version": data.private_task1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_task_ca2.pk): "test"}, + "version": data.private_task2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index faee7fbe..d23bef49 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -25,7 +25,7 @@ from tests import factories as f from tests.utils import helper_test_http_method import pytest -pytestmark = pytest.mark.django_db(transaction=True) +pytestmark = pytest.mark.django_db @pytest.fixture @@ -95,6 +95,18 @@ def data(): m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1) m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2) + + m.public_user_story = f.UserStoryFactory(project=m.public_project, + status__project=m.public_project) + m.private_user_story1 = f.UserStoryFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_user_story2 = f.UserStoryFactory(project=m.private_project2, + status__project=m.private_project2) + + m.public_user_story_cav = m.public_user_story.custom_attributes_values + m.private_user_story_cav1 = m.private_user_story1.custom_attributes_values + m.private_user_story_cav2 = m.private_user_story2.custom_attributes_values + return m @@ -285,3 +297,100 @@ def test_userstory_custom_attribute_action_bulk_update_order(client, data): }) results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + + + +######################################################### +# UserStory Custom Attribute +######################################################### + + +def test_userstory_custom_attributes_values_retrieve(client, data): + public_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.public_user_story.pk}) + private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story1.pk}) + private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_userstory_custom_attributes_values_update(client, data): + public_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.public_user_story.pk}) + private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story1.pk}) + private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.public_user_story_cav).data + user_story_data["attributes_values"] = {str(data.public_userstory_ca.pk): "test"} + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.private_user_story_cav1).data + user_story_data["attributes_values"] = {str(data.private_userstory_ca1.pk): "test"} + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.private_user_story_cav2).data + user_story_data["attributes_values"] = {str(data.private_userstory_ca2.pk): "test"} + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_userstory_custom_attributes_values_patch(client, data): + public_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.public_user_story.pk}) + private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story1.pk}) + private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"attributes_values": {str(data.public_userstory_ca.pk): "test"}, + "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_userstory_ca1.pk): "test"}, + "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_userstory_ca2.pk): "test"}, + "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] From 4368323165138ec45656c0a80ce4489aa130eeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 Feb 2015 20:43:59 +0100 Subject: [PATCH 20/28] US #55: Custom fields - Improve API queries --- taiga/projects/custom_attributes/api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index d0868690..f87edf54 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -85,6 +85,11 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): lookup_field = "user_story_id" content_object = "user_story" + def get_queryset(self): + qs = self.model.objects.all() + qs = qs.select_related("user_story", "user_story__project") + return qs + class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.TaskCustomAttributesValues @@ -93,6 +98,11 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): lockup_fields = "task_id" content_object = "task" + def get_queryset(self): + qs = self.model.objects.all() + qs = qs.select_related("task", "task__project") + return qs + class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.IssueCustomAttributesValues @@ -100,3 +110,8 @@ class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): permission_classes = (permissions.IssueCustomAttributesValuesPermission,) lockup_fields = "issue_id" content_object = "issue" + + def get_queryset(self): + qs = self.model.objects.all() + qs = qs.select_related("issue", "issue__project") + return qs From be6e03226b649d5d25bdaca7d542f851f229893f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 Feb 2015 21:37:39 +0100 Subject: [PATCH 21/28] US #55: Custom fields - Webhooks --- taiga/webhooks/serializers.py | 41 ++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index 613e832a..17712cc3 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.core.exceptions import ObjectDoesNotExist + from rest_framework import serializers from taiga.base.serializers import TagsField, PgArrayField, JsonField @@ -63,6 +65,30 @@ class UserSerializer(serializers.Serializer): return obj.full_name +class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): + custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") + + def custom_attributes_queryset(self, project): + raise NotImplementedError() + + def get_custom_attributes_values(self, obj): + def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): + ret = {} + for attr in custom_attributes: + value = values.get(str(attr["id"]), None) + if value is not None: + ret[attr["name"]] = value + + return ret + + try: + values = obj.custom_attributes_values.attributes_values + custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') + + return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) + except ObjectDoesNotExist: + return None + class PointSerializer(serializers.Serializer): id = serializers.SerializerMethodField("get_pk") name = serializers.SerializerMethodField("get_name") @@ -78,7 +104,7 @@ class PointSerializer(serializers.Serializer): return obj.value -class UserStorySerializer(serializers.ModelSerializer): +class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): tags = TagsField(default=[], required=False) external_reference = PgArrayField(required=False) owner = UserSerializer() @@ -90,8 +116,11 @@ class UserStorySerializer(serializers.ModelSerializer): model = us_models.UserStory exclude = ("backlog_order", "sprint_order", "kanban_order", "version") + def custom_attributes_queryset(self, project): + return project.userstorycustomattributes.all() -class TaskSerializer(serializers.ModelSerializer): + +class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): tags = TagsField(default=[], required=False) owner = UserSerializer() assigned_to = UserSerializer() @@ -100,8 +129,11 @@ class TaskSerializer(serializers.ModelSerializer): class Meta: model = task_models.Task + def custom_attributes_queryset(self, project): + return project.taskcustomattributes.all() -class IssueSerializer(serializers.ModelSerializer): + +class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): tags = TagsField(default=[], required=False) owner = UserSerializer() assigned_to = UserSerializer() @@ -110,6 +142,9 @@ class IssueSerializer(serializers.ModelSerializer): class Meta: model = issue_models.Issue + def custom_attributes_queryset(self, project): + return project.issuecustomattributes.all() + class WikiPageSerializer(serializers.ModelSerializer): owner = UserSerializer() From ffe04f2babbcd084efa640174c1d6303c1df5e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 25 Feb 2015 16:46:39 +0100 Subject: [PATCH 22/28] US #55: Custom fields - Fix asome error when generate diff in history model --- taiga/projects/history/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 769ec789..468eb439 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -204,8 +204,8 @@ class HistoryEntry(models.Model): "deleted": [], } - oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0]} - newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1]} + oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []} + newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []} for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): if aid in oldcustattrs and aid in newcustattrs: From 462638db5ebb6707d02362c95307a7c6865e7271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 26 Feb 2015 12:02:32 +0100 Subject: [PATCH 23/28] US #55: Custom fields - Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3811ae03..e6b9347d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## 1.6.0 ??? (Unreleased) ### Features -- ... +- Added custom fields per project for user stories, tasks and issues. ### Misc - New contrib plugin for hipchat (by Δndrea Stagi) From 618a7310193ae7f931793c0f21116adacf73f7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 26 Feb 2015 17:48:32 +0100 Subject: [PATCH 24/28] Fixing some tests --- .../test_issues_custom_attributes_resource.py | 11 ++++++----- .../test_tasks_custom_attributes_resource.py | 11 ++++++----- .../test_userstories_custom_attributes_resource.py | 12 +++++++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 8735fe5f..173f9b5f 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -19,7 +19,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS, USER_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -39,12 +40,12 @@ def data(): m.superuser = f.UserFactory.create(is_superuser=True) m.public_project = f.ProjectFactory(is_private=False, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index c7562e86..d45a3782 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -19,7 +19,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS, USER_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -39,12 +40,12 @@ def data(): m.superuser = f.UserFactory.create(is_superuser=True) m.public_project = f.ProjectFactory(is_private=False, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index d23bef49..17a30bc8 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -19,7 +19,9 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS, USER_PERMISSIONS) + from tests import factories as f from tests.utils import helper_test_http_method @@ -39,12 +41,12 @@ def data(): m.superuser = f.UserFactory.create(is_superuser=True) m.public_project = f.ProjectFactory(is_private=False, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, - anon_permissions=['view_project'], - public_permissions=['view_project'], + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], From f9cbc3630320ddcafa904ec991a21c1bfd3b4a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 26 Feb 2015 18:56:06 +0100 Subject: [PATCH 25/28] Fixing issues and task tests --- taiga/projects/custom_attributes/api.py | 4 ++-- .../test_issues_custom_attributes_resource.py | 18 +++++++++--------- .../test_tasks_custom_attributes_resource.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index f87edf54..2a53d854 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -95,7 +95,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.TaskCustomAttributesValues serializer_class = serializers.TaskCustomAttributesValuesSerializer permission_classes = (permissions.TaskCustomAttributesValuesPermission,) - lockup_fields = "task_id" + lookup_field = "task_id" content_object = "task" def get_queryset(self): @@ -108,7 +108,7 @@ class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.IssueCustomAttributesValues serializer_class = serializers.IssueCustomAttributesValuesSerializer permission_classes = (permissions.IssueCustomAttributesValuesPermission,) - lockup_fields = "issue_id" + lookup_field = "issue_id" content_object = "issue" def get_queryset(self): diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 173f9b5f..e8e87048 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -317,9 +317,9 @@ def test_issue_custom_attribute_action_bulk_update_order(client, data): def test_issue_custom_attributes_values_retrieve(client, data): - public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) - private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) - private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) users = [ None, @@ -338,9 +338,9 @@ def test_issue_custom_attributes_values_retrieve(client, data): def test_issue_custom_attributes_values_update(client, data): - public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) - private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) - private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) users = [ None, @@ -370,9 +370,9 @@ def test_issue_custom_attributes_values_update(client, data): def test_issue_custom_attributes_values_patch(client, data): - public_url = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.public_issue.pk}) - private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue1.pk}) - private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"pk": data.private_issue2.pk}) + public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) + private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) + private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index d45a3782..773c44cb 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -311,9 +311,9 @@ def test_task_custom_attribute_action_bulk_update_order(client, data): def test_task_custom_attributes_values_retrieve(client, data): - public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) - private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) - private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) users = [ None, @@ -332,9 +332,9 @@ def test_task_custom_attributes_values_retrieve(client, data): def test_task_custom_attributes_values_update(client, data): - public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) - private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) - private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) users = [ None, @@ -364,9 +364,9 @@ def test_task_custom_attributes_values_update(client, data): def test_task_custom_attributes_values_patch(client, data): - public_url = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.public_task.pk}) - private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task1.pk}) - private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"pk": data.private_task2.pk}) + public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) + private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) + private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) users = [ None, From 7ef5a3a9d90d60cb7ccfc3df7c2acd60ff21b403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 2 Mar 2015 12:39:37 +0100 Subject: [PATCH 26/28] US #55: Custom fields - Minor refactor --- .../projects/custom_attributes/serializers.py | 3 ++- taiga/projects/serializers.py | 27 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 641c1996..dbc367f4 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -32,7 +32,8 @@ from . import models class BaseCustomAttributeSerializer(ModelSerializer): class Meta: - read_only_fields = ('id', 'created_date', 'modified_date') + read_only_fields = ('id',) + exclude = ('created_date', 'modified_date') def _validate_integrity_between_project_and_name(self, attrs, source): """ diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index ade6566f..b0332be9 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -34,10 +34,11 @@ from taiga.permissions.service import is_project_owner from . import models from . import services -from . validators import ProjectExistsValidator -from . custom_attributes.serializers import(UserStoryCustomAttributeSerializer, - TaskCustomAttributeSerializer, - IssueCustomAttributeSerializer) +from .validators import ProjectExistsValidator +from .custom_attributes.serializers import UserStoryCustomAttributeSerializer +from .custom_attributes.serializers import TaskCustomAttributeSerializer +from .custom_attributes.serializers import IssueCustomAttributeSerializer + ###################################################### ## Custom values for selectors @@ -264,9 +265,6 @@ class ProjectSerializer(ModelSerializer): tags_colors = TagsColorsField(required=False) users = serializers.SerializerMethodField("get_users") total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") - userstory_custom_attributes = serializers.SerializerMethodField("get_userstory_custom_attributes") - task_custom_attributes = serializers.SerializerMethodField("get_task_custom_attributes") - issue_custom_attributes = serializers.SerializerMethodField("get_issue_custom_attributes") class Meta: model = models.Project @@ -303,15 +301,6 @@ class ProjectSerializer(ModelSerializer): raise serializers.ValidationError("Total milestones must be major or equal to zero") return attrs - def get_userstory_custom_attributes(self, obj): - return UserStoryCustomAttributeSerializer(obj.userstorycustomattributes.all(), many=True).data - - def get_task_custom_attributes(self, obj): - return TaskCustomAttributeSerializer(obj.taskcustomattributes.all(), many=True).data - - def get_issue_custom_attributes(self, obj): - return IssueCustomAttributeSerializer(obj.issuecustomattributes.all(), many=True).data - class ProjectDetailSerializer(ProjectSerializer): roles = serializers.SerializerMethodField("get_roles") memberships = serializers.SerializerMethodField("get_memberships") @@ -322,6 +311,12 @@ class ProjectDetailSerializer(ProjectSerializer): issue_types = IssueTypeSerializer(many=True, required=False) priorities = PrioritySerializer(many=True, required=False) # Issues severities = SeveritySerializer(many=True, required=False) + userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes", + many=True, required=False) + task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes", + many=True, required=False) + issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes", + many=True, required=False) def get_memberships(self, obj): qs = obj.memberships.filter(user__isnull=False) From d56ee4906418f0e481a79a56999c512d1da1587f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Mar 2015 11:26:01 +0100 Subject: [PATCH 27/28] US #55: Custom fields - Change the name of a var --- taiga/export_import/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index a5c9e22b..8eb8cd42 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -338,8 +338,8 @@ def store_user_story(project, data): if "status" not in data and project.default_us_status: data["status"] = project.default_us_status.name - data_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} - serialized = serializers.UserStoryExportSerializer(data=data_data, context={"project": project}) + us_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} + serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project}) if serialized.is_valid(): serialized.object.project = project From 5dfd88274b8e227def1317c87847c0c64e18e18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 4 Mar 2015 12:58:46 +0100 Subject: [PATCH 28/28] US #55: Custom fields - Send email notifications --- taiga/projects/custom_attributes/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 2a53d854..c93bb790 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -24,6 +24,7 @@ from taiga.base import response from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.occ.mixins import OCCResourceMixin from . import models @@ -73,7 +74,8 @@ class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): # Custom Attributes Values ViewSets ####################################################### -class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, ModelUpdateRetrieveViewSet): +class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, + ModelUpdateRetrieveViewSet): def get_object_for_snapshot(self, obj): return getattr(obj, self.content_object)