Move all tags code to projects.tags

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-06-10 18:39:28 +02:00
parent fde98473c4
commit 13ef1b9af5
12 changed files with 297 additions and 239 deletions

View File

@ -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"))

View File

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

View File

@ -16,8 +16,6 @@
# 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/>.
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

View File

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

View File

@ -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)

View File

@ -16,11 +16,11 @@
# 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.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

View File

@ -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)

View File

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

View File

@ -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):

View File

@ -20,4 +20,3 @@
def tags_normalization(sender, instance, **kwargs):
if isinstance(instance.tags, (list, tuple)):
instance.tags = list(map(str.lower, instance.tags))

View File

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

View File

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