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