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] 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