Move all tags code to projects.tags
parent
fde98473c4
commit
13ef1b9af5
|
@ -22,38 +22,38 @@ from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import signals, Prefetch
|
from django.db.models import signals, Prefetch
|
||||||
from django.db.models import Value as V
|
from django.db.models import Value as V
|
||||||
from django.db.models.functions import Coalesce
|
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.translation import ugettext as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.http import Http404
|
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
||||||
from taiga.base.api.permissions import AllowAnyPermission
|
from taiga.base.api.permissions import AllowAnyPermission
|
||||||
from taiga.base.api.utils import get_object_or_404
|
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.base.utils.slug import slugify_uniquely
|
||||||
|
|
||||||
|
from taiga.permissions import services as permissions_services
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
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.models import NotifyPolicy
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
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.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.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 taiga.users import services as users_services
|
||||||
|
|
||||||
from . import filters as project_filters
|
from . import filters as project_filters
|
||||||
|
@ -67,8 +67,8 @@ from . import services
|
||||||
## Project
|
## Project
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
|
||||||
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
TagsColorsResourceMixin, ModelCrudViewSet):
|
||||||
queryset = models.Project.objects.all()
|
queryset = models.Project.objects.all()
|
||||||
serializer_class = serializers.ProjectDetailSerializer
|
serializer_class = serializers.ProjectDetailSerializer
|
||||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
||||||
|
@ -327,12 +327,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
self.check_permissions(request, "issues_stats", project)
|
self.check_permissions(request, "issues_stats", project)
|
||||||
return response.Ok(services.get_stats_for_project_issues(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"])
|
@detail_route(methods=["POST"])
|
||||||
def transfer_validate_token(self, request, pk=None):
|
def transfer_validate_token(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
|
@ -405,63 +399,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
services.reject_project_transfer(project, request.user, token, reason)
|
services.reject_project_transfer(project, request.user, token, reason)
|
||||||
return response.Ok()
|
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):
|
def _raise_if_blocked(self, project):
|
||||||
if self.is_blocked(project):
|
if self.is_blocked(project):
|
||||||
raise exc.Blocked(_("Blocked element"))
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
|
@ -31,7 +31,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
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 taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
@ -416,94 +414,3 @@ class ProjectTemplateSerializer(serializers.ModelSerializer):
|
||||||
class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
|
class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
order = 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
|
|
||||||
|
|
|
@ -57,6 +57,3 @@ from .stats import get_member_stats_for_project
|
||||||
|
|
||||||
from .transfer import request_project_transfer, start_project_transfer
|
from .transfer import request_project_transfer, start_project_transfer
|
||||||
from .transfer import accept_project_transfer, reject_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
|
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from 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)
|
|
@ -16,11 +16,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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
|
import re
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext 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
|
|
@ -23,9 +23,14 @@ def tag_exist_for_project_elements(project, tag):
|
||||||
return tag in dict(project.tags_colors).keys()
|
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):
|
def create_tag(project, tag, color):
|
||||||
project.tags_colors.append([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):
|
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:
|
if to_tag is not None:
|
||||||
color = dict(project.tags_colors)[from_tag]
|
color = dict(project.tags_colors)[from_tag]
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
UPDATE userstories_userstory
|
||||||
UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
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)
|
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag)
|
||||||
cursor = connection.cursor()
|
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.tags_colors = list(tags_colors.items())
|
||||||
project.save()
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
def rename_tag(project, from_tag, to_tag, color=None):
|
def rename_tag(project, from_tag, to_tag, color=None):
|
||||||
color = color or dict(project.tags_colors)[from_tag]
|
color = color or dict(project.tags_colors)[from_tag]
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
UPDATE userstories_userstory
|
||||||
UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id};
|
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)
|
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color)
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
@ -68,14 +89,22 @@ def rename_tag(project, from_tag, to_tag, color=None):
|
||||||
tags_colors.pop(from_tag)
|
tags_colors.pop(from_tag)
|
||||||
tags_colors[to_tag] = color
|
tags_colors[to_tag] = color
|
||||||
project.tags_colors = list(tags_colors.items())
|
project.tags_colors = list(tags_colors.items())
|
||||||
project.save()
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
def delete_tag(project, tag):
|
def delete_tag(project, tag):
|
||||||
sql = """
|
sql = """
|
||||||
UPDATE userstories_userstory SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
|
UPDATE userstories_userstory
|
||||||
UPDATE tasks_task SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
|
SET tags = array_remove(tags, '{tag}')
|
||||||
UPDATE issues_issue SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id};
|
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)
|
sql = sql.format(project_id=project.id, tag=tag)
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
@ -84,7 +113,7 @@ def delete_tag(project, tag):
|
||||||
tags_colors = dict(project.tags_colors)
|
tags_colors = dict(project.tags_colors)
|
||||||
del tags_colors[tag]
|
del tags_colors[tag]
|
||||||
project.tags_colors = list(tags_colors.items())
|
project.tags_colors = list(tags_colors.items())
|
||||||
project.save()
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
def mix_tags(project, from_tags, to_tag):
|
def mix_tags(project, from_tags, to_tag):
|
|
@ -20,4 +20,3 @@
|
||||||
def tags_normalization(sender, instance, **kwargs):
|
def tags_normalization(sender, instance, **kwargs):
|
||||||
if isinstance(instance.tags, (list, tuple)):
|
if isinstance(instance.tags, (list, tuple)):
|
||||||
instance.tags = list(map(str.lower, instance.tags))
|
instance.tags = list(map(str.lower, instance.tags))
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.models import Project, TaskStatus
|
from taiga.projects.models import Project, TaskStatus
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
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 taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
|
@ -36,7 +36,7 @@ from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.models import Project, UserStoryStatus
|
from taiga.projects.models import Project, UserStoryStatus
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
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 taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
Loading…
Reference in New Issue