diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 77d9a5d1..6c7a4ec9 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -22,38 +22,38 @@ from dateutil.relativedelta import relativedelta
from django.apps import apps
from django.conf import settings
+from django.core.exceptions import ValidationError
from django.db.models import signals, Prefetch
from django.db.models import Value as V
from django.db.models.functions import Coalesce
-from django.core.exceptions import ValidationError
+from django.http import Http404
from django.utils.translation import ugettext as _
from django.utils import timezone
-from django.http import Http404
from taiga.base import filters
-from taiga.base import response
from taiga.base import exceptions as exc
-from taiga.base.decorators import list_route
-from taiga.base.decorators import detail_route
+from taiga.base import response
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
from taiga.base.api.permissions import AllowAnyPermission
from taiga.base.api.utils import get_object_or_404
+from taiga.base.decorators import list_route
+from taiga.base.decorators import detail_route
from taiga.base.utils.slug import slugify_uniquely
+from taiga.permissions import services as permissions_services
from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.issues.models import Issue
+from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
from taiga.projects.notifications.models import NotifyPolicy
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.notifications.choices import NotifyLevel
-
-from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
+from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
+from taiga.projects.tasks.models import Task
+from taiga.projects.tagging.api import TagsColorsResourceMixin
from taiga.projects.userstories.models import UserStory, RolePoints
-from taiga.projects.tasks.models import Task
-from taiga.projects.issues.models import Issue
-from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
-from taiga.permissions import services as permissions_services
from taiga.users import services as users_services
from . import filters as project_filters
@@ -67,8 +67,8 @@ from . import services
## Project
######################################################
-class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
- BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
+class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
+ TagsColorsResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
@@ -327,12 +327,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "issues_stats", project)
return response.Ok(services.get_stats_for_project_issues(project))
- @detail_route(methods=["GET"])
- def tags_colors(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "tags_colors", project)
- return response.Ok(dict(project.tags_colors))
-
@detail_route(methods=["POST"])
def transfer_validate_token(self, request, pk=None):
project = self.get_object()
@@ -405,63 +399,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
services.reject_project_transfer(project, request.user, token, reason)
return response.Ok()
- @detail_route(methods=["POST"])
- def create_tag(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "create_tag", project)
- self._raise_if_blocked(project)
- serializer = serializers.CreateTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
-
- data = serializer.data
- services.create_tag(project, data.get("tag"), data.get("color"))
- return response.Ok()
-
-
- @detail_route(methods=["POST"])
- def edit_tag(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "edit_tag", project)
- self._raise_if_blocked(project)
- serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
-
- data = serializer.data
- services.edit_tag(project, data.get("from_tag"),
- to_tag=data.get("to_tag", None),
- color=data.get("color", None))
-
- return response.Ok()
-
-
- @detail_route(methods=["POST"])
- def delete_tag(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "delete_tag", project)
- self._raise_if_blocked(project)
- serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
-
- data = serializer.data
- services.delete_tag(project, data.get("tag"))
- return response.Ok()
-
- @detail_route(methods=["POST"])
- def mix_tags(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "mix_tags", project)
- self._raise_if_blocked(project)
- serializer = serializers.MixTagsSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
-
- data = serializer.data
- services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
- return response.Ok()
-
def _raise_if_blocked(self, project):
if self.is_blocked(project):
raise exc.Blocked(_("Blocked element"))
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index 20befba7..cae23be3 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -31,7 +31,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.tagging.mixins import TaggedResourceMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index f18bee1d..f388528f 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import re
-
from django.utils.translation import ugettext as _
from django.db.models import Q
@@ -416,94 +414,3 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
order = serializers.IntegerField()
-
-
-######################################################
-## Project tags serializers
-######################################################
-
-
-class ProjectTagSerializer(serializers.Serializer):
- def __init__(self, *args, **kwargs):
- # Don't pass the extra project arg
- self.project = kwargs.pop("project")
-
- # Instantiate the superclass normally
- super().__init__(*args, **kwargs)
-
-
-class CreateTagSerializer(ProjectTagSerializer):
- tag = serializers.CharField()
- color = serializers.CharField(required=False)
-
- def validate_tag(self, attrs, source):
- tag = attrs.get(source, None)
- if services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag exists."))
-
- return attrs
-
- def validate_color(self, attrs, source):
- color = attrs.get(source, None)
- if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
- raise serializers.ValidationError(_("The color is not a valid HEX color."))
-
- return attrs
-
-
-class EditTagTagSerializer(ProjectTagSerializer):
- from_tag = serializers.CharField()
- to_tag = serializers.CharField(required=False)
- color = serializers.CharField(required=False)
-
- def validate_from_tag(self, attrs, source):
- tag = attrs.get(source, None)
- if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
-
- return attrs
-
- def validate_to_tag(self, attrs, source):
- tag = attrs.get(source, None)
- if services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag exists yet"))
-
- return attrs
-
- def validate_color(self, attrs, source):
- color = attrs.get(source, None)
- if len(color) != 7 or not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
- raise serializers.ValidationError(_("The color is not a valid HEX color."))
-
- return attrs
-
-
-class DeleteTagSerializer(ProjectTagSerializer):
- tag = serializers.CharField()
-
- def validate_tag(self, attrs, source):
- tag = attrs.get(source, None)
- if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
-
- return attrs
-
-
-class MixTagsSerializer(ProjectTagSerializer):
- from_tags = TagsField()
- to_tag = serializers.CharField()
-
- def validate_from_tags(self, attrs, source):
- tags = attrs.get(source, None)
- for tag in tags:
- if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
-
- return attrs
-
- def validate_to_tag(self, attrs, source):
- tag = attrs.get(source, None)
- if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
-
- return attrs
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index f2fcc3c0..a115275b 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -57,6 +57,3 @@ from .stats import get_member_stats_for_project
from .transfer import request_project_transfer, start_project_transfer
from .transfer import accept_project_transfer, reject_project_transfer
-
-from .tags import tag_exist_for_project_elements, create_tag
-from .tags import edit_tag, delete_tag, mix_tags
diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py
new file mode 100644
index 00000000..d93ebe72
--- /dev/null
+++ b/taiga/projects/tagging/api.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# 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 import response
+from taiga.base.decorators import detail_route
+
+from . import services
+from . import serializers
+
+
+class TagsColorsResourceMixin:
+ @detail_route(methods=["GET"])
+ def tags_colors(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "tags_colors", project)
+
+ return response.Ok(dict(project.tags_colors))
+
+ @detail_route(methods=["POST"])
+ def create_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "create_tag", project)
+ self._raise_if_blocked(project)
+
+ serializer = serializers.CreateTagSerializer(data=request.DATA, project=project)
+ if not serializer.is_valid():
+ return response.BadRequest(serializer.errors)
+
+ data = serializer.data
+ services.create_tag(project, data.get("tag"), data.get("color"))
+
+ return response.Ok()
+
+
+ @detail_route(methods=["POST"])
+ def edit_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "edit_tag", project)
+ self._raise_if_blocked(project)
+
+ serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project)
+ if not serializer.is_valid():
+ return response.BadRequest(serializer.errors)
+
+ data = serializer.data
+ services.edit_tag(project,
+ data.get("from_tag"),
+ to_tag=data.get("to_tag", None),
+ color=data.get("color", None))
+
+ return response.Ok()
+
+
+ @detail_route(methods=["POST"])
+ def delete_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "delete_tag", project)
+ self._raise_if_blocked(project)
+
+ serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project)
+ if not serializer.is_valid():
+ return response.BadRequest(serializer.errors)
+
+ data = serializer.data
+ services.delete_tag(project, data.get("tag"))
+
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def mix_tags(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "mix_tags", project)
+ self._raise_if_blocked(project)
+
+ serializer = serializers.MixTagsSerializer(data=request.DATA, project=project)
+ if not serializer.is_valid():
+ return response.BadRequest(serializer.errors)
+
+ data = serializer.data
+ services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
+
+ return response.Ok()
+
+
+class TaggedResourceMixin:
+ def pre_save(self, obj):
+ if obj.tags:
+ self._pre_save_new_tags_in_project_tagss_colors(obj)
+
+ super().pre_save(obj)
+
+ def _pre_save_new_tags_in_project_tagss_colors(self, obj):
+ new_obj_tags = set()
+ new_tags_colors = {}
+
+ for tag in obj.tags:
+ if isinstance(tag, (list, tuple)):
+ name, color = tag
+
+ if color and not services.tag_exist_for_project_elements(obj.project, name):
+ new_tags_colors[name] = color
+
+ new_obj_tags.add(name)
+ elif isinstance(tag, str):
+ new_obj_tags.add(tag.lower())
+
+ obj.tags = list(new_obj_tags)
+
+ if new_tags_colors:
+ services.create_tags(obj.project, new_tags_colors)
diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py
index b56e3cc1..24f92f23 100644
--- a/taiga/projects/tagging/fields.py
+++ b/taiga/projects/tagging/fields.py
@@ -16,11 +16,11 @@
# 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 ValidationError
from django.forms import widgets
from django.utils.translation import ugettext_lazy as _
-from taiga.base.api import serializers
-from django.core.exceptions import ValidationError
+from taiga.base.api import serializers
import re
diff --git a/taiga/projects/tagging/mixins.py b/taiga/projects/tagging/mixins.py
deleted file mode 100644
index aa5df99f..00000000
--- a/taiga/projects/tagging/mixins.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2014-2016 Andrey Antukh
-# Copyright (C) 2014-2016 Jesús Espino
-# Copyright (C) 2014-2016 David Barragán
-# Copyright (C) 2014-2016 Alejandro Alonso
-# 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 .
-
-
-def _pre_save_new_tags_in_project_tagss_colors(obj):
- current_project_tags = [t[0] for t in obj.project.tags_colors]
- new_obj_tags = set()
- new_tags_colors = {}
-
- for tag in obj.tags:
- if isinstance(tag, (list, tuple)):
- name, color = tag
-
- if color and name not in current_project_tags:
- new_tags_colors[name] = color
-
- new_obj_tags.add(name)
- elif isinstance(tag, str):
- new_obj_tags.add(tag.lower())
-
- obj.tags = list(new_obj_tags)
-
- if new_tags_colors:
- obj.project.tags_colors += [[k, v] for k,v in new_tags_colors.items()]
- obj.project.save(update_fields=["tags_colors"])
-
-
-class TaggedResourceMixin:
- def pre_save(self, obj):
- if obj.tags:
- _pre_save_new_tags_in_project_tagss_colors(obj)
-
- super().pre_save(obj)
diff --git a/taiga/projects/tagging/serializers.py b/taiga/projects/tagging/serializers.py
new file mode 100644
index 00000000..dc25b73a
--- /dev/null
+++ b/taiga/projects/tagging/serializers.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# 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 as _
+
+from taiga.base.api import serializers
+
+from . import services
+from . import fields
+
+import re
+
+
+class ProjectTagSerializer(serializers.Serializer):
+ def __init__(self, *args, **kwargs):
+ # Don't pass the extra project arg
+ self.project = kwargs.pop("project")
+
+ # Instantiate the superclass normally
+ super().__init__(*args, **kwargs)
+
+
+class CreateTagSerializer(ProjectTagSerializer):
+ tag = serializers.CharField()
+ color = serializers.CharField(required=False)
+
+ def validate_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag exists."))
+
+ return attrs
+
+ def validate_color(self, attrs, source):
+ color = attrs.get(source, None)
+ if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
+ raise serializers.ValidationError(_("The color is not a valid HEX color."))
+
+ return attrs
+
+
+class EditTagTagSerializer(ProjectTagSerializer):
+ from_tag = serializers.CharField()
+ to_tag = serializers.CharField(required=False)
+ color = serializers.CharField(required=False)
+
+ def validate_from_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+ def validate_to_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag exists yet"))
+
+ return attrs
+
+ def validate_color(self, attrs, source):
+ color = attrs.get(source, None)
+ if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
+ raise serializers.ValidationError(_("The color is not a valid HEX color."))
+
+ return attrs
+
+
+class DeleteTagSerializer(ProjectTagSerializer):
+ tag = serializers.CharField()
+
+ def validate_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+
+class MixTagsSerializer(ProjectTagSerializer):
+ from_tags = fields.TagsField()
+ to_tag = serializers.CharField()
+
+ def validate_from_tags(self, attrs, source):
+ tags = attrs.get(source, None)
+ for tag in tags:
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+ def validate_to_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise serializers.ValidationError(_("The tag doesn't exist."))
+
+ return attrs
diff --git a/taiga/projects/services/tags.py b/taiga/projects/tagging/services.py
similarity index 60%
rename from taiga/projects/services/tags.py
rename to taiga/projects/tagging/services.py
index 010a23fb..30e9f9dc 100644
--- a/taiga/projects/services/tags.py
+++ b/taiga/projects/tagging/services.py
@@ -23,9 +23,14 @@ def tag_exist_for_project_elements(project, tag):
return tag in dict(project.tags_colors).keys()
+def create_tags(project, new_tags_colors):
+ project.tags_colors += [[k, v] for k,v in new_tags_colors.items()]
+ project.save(update_fields=["tags_colors"])
+
+
def create_tag(project, tag, color):
project.tags_colors.append([tag, color])
- project.save()
+ project.save(update_fields=["tags_colors"])
def edit_tag(project, from_tag, to_tag=None, color=None):
@@ -38,9 +43,17 @@ def edit_tag(project, from_tag, to_tag=None, color=None):
if to_tag is not None:
color = dict(project.tags_colors)[from_tag]
sql = """
- UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
- UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
- UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
+ UPDATE userstories_userstory
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
"""
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag)
cursor = connection.cursor()
@@ -50,15 +63,23 @@ def edit_tag(project, from_tag, to_tag=None, color=None):
project.tags_colors = list(tags_colors.items())
- project.save()
+ project.save(update_fields=["tags_colors"])
def rename_tag(project, from_tag, to_tag, color=None):
color = color or dict(project.tags_colors)[from_tag]
sql = """
- UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
- UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
- UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
+ UPDATE userstories_userstory
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
"""
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color)
cursor = connection.cursor()
@@ -68,14 +89,22 @@ def rename_tag(project, from_tag, to_tag, color=None):
tags_colors.pop(from_tag)
tags_colors[to_tag] = color
project.tags_colors = list(tags_colors.items())
- project.save()
+ project.save(update_fields=["tags_colors"])
def delete_tag(project, tag):
sql = """
- UPDATE userstories_userstory SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
- UPDATE tasks_task SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
- UPDATE issues_issue SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
+ UPDATE userstories_userstory
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
"""
sql = sql.format(project_id=project.id, tag=tag)
cursor = connection.cursor()
@@ -84,7 +113,7 @@ def delete_tag(project, tag):
tags_colors = dict(project.tags_colors)
del tags_colors[tag]
project.tags_colors = list(tags_colors.items())
- project.save()
+ project.save(update_fields=["tags_colors"])
def mix_tags(project, from_tags, to_tag):
diff --git a/taiga/projects/tagging/signals.py b/taiga/projects/tagging/signals.py
index 562fcba5..cc94461a 100644
--- a/taiga/projects/tagging/signals.py
+++ b/taiga/projects/tagging/signals.py
@@ -20,4 +20,3 @@
def tags_normalization(sender, instance, **kwargs):
if isinstance(instance.tags, (list, tuple)):
instance.tags = list(map(str.lower, instance.tags))
-
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 7b5de5f3..9ebf6dfe 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -29,7 +29,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.tagging.mixins import TaggedResourceMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 028bfe35..b6693c36 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -36,7 +36,7 @@ from taiga.projects.milestones.models import Milestone
from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.tagging.mixins import TaggedResourceMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models