diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 9c00901c..958ed3b3 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -405,6 +405,67 @@ 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"))
+
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index c43e842f..0cc95427 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -78,6 +78,10 @@ class ProjectPermission(TaigaResourcePermission):
transfer_start_perms = IsObjectOwner()
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ create_tag_perms = IsProjectAdmin()
+ edit_tag_perms = IsProjectAdmin()
+ delete_tag_perms = IsProjectAdmin()
+ mix_tags_perms = IsProjectAdmin()
class ProjectFansPermission(TaigaResourcePermission):
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 1b271590..f7403389 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -16,6 +16,7 @@
# 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
@@ -256,7 +257,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
i_am_member = serializers.SerializerMethodField("get_i_am_member")
tags = TagsField(default=[], required=False)
- tags_colors = TagsColorsField(required=False)
+ tags_colors = TagsColorsField(required=False, read_only=True)
notify_level = serializers.SerializerMethodField("get_notify_level")
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
@@ -416,3 +417,94 @@ 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 a115275b..f2fcc3c0 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -57,3 +57,6 @@ 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/services/tags.py b/taiga/projects/services/tags.py
new file mode 100644
index 00000000..20e1946a
--- /dev/null
+++ b/taiga/projects/services/tags.py
@@ -0,0 +1,90 @@
+# -*- 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.db import connection
+
+def tag_exist_for_project_elements(project, tag):
+ return tag in dict(project.tags_colors).keys()
+
+
+def create_tag(project, tag, color):
+ project.tags_colors.append([tag, color])
+ project.save()
+
+
+def edit_tag(project, from_tag, to_tag=None, color=None):
+ tags_colors = dict(project.tags_colors)
+
+ if color is not None:
+ tags_colors = dict(project.tags_colors)
+ tags_colors[from_tag] = color
+
+ 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};
+ """
+ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors[to_tag] = tags_colors.pop(from_tag)
+
+
+ project.tags_colors = list(tags_colors.items())
+ project.save()
+
+
+def rename_tag(project, from_tag, to_tag):
+ 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};
+ """
+ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors = dict(project.tags_colors)
+ tags_colors[to_tag] = tags_colors.pop(from_tag)
+ project.tags_colors = list(tags_colors.items())
+ project.save()
+
+
+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};
+ """
+ sql = sql.format(project_id=project.id, tag=tag)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors = dict(project.tags_colors)
+ del tags_colors[tag]
+ project.tags_colors = list(tags_colors.items())
+ project.save()
+
+
+def mix_tags(project, from_tags, to_tag):
+ for from_tag in from_tags:
+ rename_tag(project, from_tag, to_tag)
diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py
index 0115143a..2e95f731 100644
--- a/tests/integration/resources_permissions/test_projects_choices_resources.py
+++ b/tests/integration/resources_permissions/test_projects_choices_resources.py
@@ -27,20 +27,24 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=['view_project'],
public_permissions=['view_project'],
- owner=m.project_owner)
+ owner=m.project_owner,
+ tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=['view_project'],
public_permissions=['view_project'],
- owner=m.project_owner)
+ owner=m.project_owner,
+ tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
- owner=m.project_owner)
+ owner=m.project_owner,
+ tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
- blocked_code=project_choices.BLOCKED_BY_STAFF)
+ blocked_code=project_choices.BLOCKED_BY_STAFF,
+ tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -1911,3 +1915,127 @@ def test_project_template_patch(client, data):
results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users)
assert results == [401, 403, 200]
+
+
+def test_create_tag(client, data):
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ post_data = json.dumps({
+ "tag": "testtest",
+ "color": "#123123"
+ })
+
+ url = reverse('projects-create-tag', kwargs={"pk": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-create-tag', kwargs={"pk": data.private_project1.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-create-tag', kwargs={"pk": data.private_project2.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 200]
+
+ url = reverse('projects-create-tag', kwargs={"pk": data.blocked_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 451]
+
+
+def test_edit_tag(client, data):
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ post_data = json.dumps({
+ "from_tag": "tag1",
+ "to_tag": "renamedtag1",
+ "color": "#123123"
+ })
+
+ url = reverse('projects-edit-tag', kwargs={"pk": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-edit-tag', kwargs={"pk": data.private_project1.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-edit-tag', kwargs={"pk": data.private_project2.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 200]
+
+ url = reverse('projects-edit-tag', kwargs={"pk": data.blocked_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 451]
+
+
+def test_delete_tag(client, data):
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ post_data = json.dumps({
+ "tag": "tag2",
+ })
+
+ url = reverse('projects-delete-tag', kwargs={"pk": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-delete-tag', kwargs={"pk": data.private_project1.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-delete-tag', kwargs={"pk": data.private_project2.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 200]
+
+ url = reverse('projects-delete-tag', kwargs={"pk": data.blocked_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 451]
+
+
+def test_mix_tags(client, data):
+ users = [
+ None,
+ data.registered_user,
+ data.project_member_without_perms,
+ data.project_member_with_perms,
+ data.project_owner
+ ]
+
+ post_data = json.dumps({
+ "from_tags": ["tag1"],
+ "to_tag": "tag3"
+ })
+
+ url = reverse('projects-mix-tags', kwargs={"pk": data.public_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-mix-tags', kwargs={"pk": data.private_project1.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [401, 403, 403, 403, 200]
+
+ url = reverse('projects-mix-tags', kwargs={"pk": data.private_project2.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 200]
+
+ url = reverse('projects-mix-tags', kwargs={"pk": data.blocked_project.pk})
+ results = helper_test_http_method(client, 'post', url, post_data, users)
+ assert results == [404, 404, 404, 403, 451]
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 4c2e121e..29d57c50 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -10,6 +10,9 @@ from taiga.projects.services import stats as stats_services
from taiga.projects.history.services import take_snapshot
from taiga.permissions.choices import ANON_PERMISSIONS
from taiga.projects.models import Project
+from taiga.projects.userstories.models import UserStory
+from taiga.projects.tasks.models import Task
+from taiga.projects.issues.models import Issue
from taiga.projects.choices import BLOCKED_BY_DELETING
from .. import factories as f
@@ -1854,6 +1857,189 @@ def test_delete_project_with_celery_disabled(client, settings):
assert Project.objects.filter(id=project.id).count() == 0
+def test_create_tag(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-create-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "tag": "newtag",
+ "color": "#123123"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors == [["newtag", "#123123"]]
+
+
+def test_create_tag_without_color(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-create-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "tag": "newtag",
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors[0][0] == "newtag"
+
+
+def test_edit_tag_only_name(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
+ user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
+ task = f.TaskFactory.create(project=project, tags=["tag"])
+ issue = f.IssueFactory.create(project=project, tags=["tag"])
+
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-edit-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "from_tag": "tag",
+ "to_tag": "renamed_tag"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ print(response.data)
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors == [["renamed_tag", "#123123"]]
+ user_story = UserStory.objects.get(id=user_story.pk)
+ assert user_story.tags == ["renamed_tag"]
+ task = Task.objects.get(id=task.pk)
+ assert task.tags == ["renamed_tag"]
+ issue = Issue.objects.get(id=issue.pk)
+ assert issue.tags == ["renamed_tag"]
+
+
+def test_edit_tag_only_color(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
+ user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
+ task = f.TaskFactory.create(project=project, tags=["tag"])
+ issue = f.IssueFactory.create(project=project, tags=["tag"])
+
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-edit-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "from_tag": "tag",
+ "color": "#AAABBB"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors == [["tag", "#AAABBB"]]
+ user_story = UserStory.objects.get(id=user_story.pk)
+ assert user_story.tags == ["tag"]
+ task = Task.objects.get(id=task.pk)
+ assert task.tags == ["tag"]
+ issue = Issue.objects.get(id=issue.pk)
+ assert issue.tags == ["tag"]
+
+
+def test_edit_tag(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
+ user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
+ task = f.TaskFactory.create(project=project, tags=["tag"])
+ issue = f.IssueFactory.create(project=project, tags=["tag"])
+
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-edit-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "from_tag": "tag",
+ "to_tag": "renamed_tag",
+ "color": "#AAABBB"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors == [["renamed_tag", "#AAABBB"]]
+ user_story = UserStory.objects.get(id=user_story.pk)
+ assert user_story.tags == ["renamed_tag"]
+ task = Task.objects.get(id=task.pk)
+ assert task.tags == ["renamed_tag"]
+ issue = Issue.objects.get(id=issue.pk)
+ assert issue.tags == ["renamed_tag"]
+
+
+def test_delete_tag(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
+ user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
+ task = f.TaskFactory.create(project=project, tags=["tag"])
+ issue = f.IssueFactory.create(project=project, tags=["tag"])
+
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-delete-tag", args=(project.id,))
+ client.login(user)
+ data = {
+ "tag": "tag"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert project.tags_colors == []
+ user_story = UserStory.objects.get(id=user_story.pk)
+ assert user_story.tags == []
+ task = Task.objects.get(id=task.pk)
+ assert task.tags == []
+ issue = Issue.objects.get(id=issue.pk)
+ assert issue.tags == []
+
+
+def test_mix_tags(client, settings):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user, tags_colors=[("tag1", "#123123"), ("tag2", "#123123"), ("tag3", "#123123")])
+ user_story = f.UserStoryFactory.create(project=project, tags=["tag1", "tag3"])
+ task = f.TaskFactory.create(project=project, tags=["tag2", "tag3"])
+ issue = f.IssueFactory.create(project=project, tags=["tag1", "tag2", "tag3"])
+
+ role = f.RoleFactory.create(project=project, permissions=["view_project"])
+ membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
+ url = reverse("projects-mix-tags", args=(project.id,))
+ client.login(user)
+ data = {
+ "from_tags": ["tag1", "tag2"],
+ "to_tag": "tag2"
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 200
+ project = Project.objects.get(id=project.pk)
+ assert set(["tag2", "tag3"]) == set(dict(project.tags_colors).keys())
+ user_story = UserStory.objects.get(id=user_story.pk)
+ assert set(user_story.tags) == set(["tag2", "tag3"])
+ task = Task.objects.get(id=task.pk)
+ assert set(task.tags) == set(["tag2", "tag3"])
+ issue = Issue.objects.get(id=issue.pk)
+ assert set(issue.tags) == set(["tag2", "tag3"])
+
+
def test_color_tags_project_fired_on_element_create():
user_story = f.UserStoryFactory.create(tags=["tag"])
project = Project.objects.get(id=user_story.project.id)
@@ -1875,4 +2061,3 @@ def test_color_tags_project_fired_on_element_update_respecting_color():
user_story.save()
project = Project.objects.get(id=user_story.project.id)
assert project.tags_colors == [["tag", "#123123"]]
->>>>>>> d64d158... WIP: migrations, removing automatic color generation
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index d65b2451..2c25b29d 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -481,7 +481,7 @@ def test_get_watched_list_valid_info_for_project():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
- project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
+ project = f.ProjectFactory(is_private=False, name="Testing project")
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
project.add_watcher(fav_user)
@@ -499,11 +499,6 @@ def test_get_watched_list_valid_info_for_project():
assert project_watch_info["assigned_to"] == None
assert project_watch_info["status"] == None
assert project_watch_info["status_color"] == None
-
- tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]}
- assert "test" in tags_colors
- assert "tag" in tags_colors
-
assert project_watch_info["is_private"] == project.is_private
assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
assert project_watch_info["is_fan"] == False
@@ -540,7 +535,7 @@ def test_get_liked_list_valid_info():
fan_user = f.UserFactory()
viewer_user = f.UserFactory()
- project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
+ project = f.ProjectFactory(is_private=False, name="Testing project")
content_type = ContentType.objects.get_for_model(project)
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
project.refresh_totals()
@@ -558,11 +553,6 @@ def test_get_liked_list_valid_info():
assert project_like_info["assigned_to"] == None
assert project_like_info["status"] == None
assert project_like_info["status_color"] == None
-
- tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]}
- assert "test" in tags_colors
- assert "tag" in tags_colors
-
assert project_like_info["is_private"] == project.is_private
assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)