From 195bdd2523ab93603611722fb243cd392e920dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 6 Aug 2014 12:37:38 +0200 Subject: [PATCH] Adding colorize tags on server functionality --- settings/common.py | 7 + taiga/base/serializers.py | 13 ++ taiga/base/tags.py | 2 - taiga/projects/issues/models.py | 12 ++ ...033_auto__add_field_project_tags_colors.py | 186 ++++++++++++++++++ taiga/projects/models.py | 2 + taiga/projects/serializers.py | 3 +- taiga/projects/services/__init__.py | 2 + taiga/projects/services/tags_colors.py | 51 +++++ taiga/projects/tasks/models.py | 12 ++ taiga/projects/userstories/models.py | 12 ++ 11 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 taiga/projects/migrations/0033_auto__add_field_project_tags_colors.py create mode 100644 taiga/projects/services/tags_colors.py diff --git a/settings/common.py b/settings/common.py index edd3a331..10755d73 100644 --- a/settings/common.py +++ b/settings/common.py @@ -333,3 +333,10 @@ except IndexError: IN_DEVELOPMENT_SERVER = False ATTACHMENTS_TOKEN_SALT = "ATTACHMENTS_TOKEN_SALT" + +TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234", + "#73d216", "#4e9a06", "#d3d7cf", "#fcaf3e", + "#f57900", "#ce5c00", "#729fcf", "#3465a4", + "#204a87", "#888a85", "#ad7fa8", "#75507b", + "#5c3566", "#ef2929", "#cc0000", "#a40000", + "#2e3436",] diff --git a/taiga/base/serializers.py b/taiga/base/serializers.py index f6cf1a6a..213a7746 100644 --- a/taiga/base/serializers.py +++ b/taiga/base/serializers.py @@ -58,6 +58,19 @@ class PgArrayField(serializers.WritableField): return data +class TagsColorsField(serializers.WritableField): + """ + PgArray objects serializer. + """ + widget = widgets.Textarea + + def to_native(self, obj): + return dict(obj) + + def from_native(self, data): + return list(data.items()) + + class NeighborsSerializerMixin: def __init__(self, *args, **kwargs): diff --git a/taiga/base/tags.py b/taiga/base/tags.py index e07632d7..36e03789 100644 --- a/taiga/base/tags.py +++ b/taiga/base/tags.py @@ -8,8 +8,6 @@ from django.utils.translation import ugettext_lazy as _ from djorm_pgarray.fields import TextArrayField -from picklefield.fields import PickledObjectField - class TaggedMixin(models.Model): tags = TextArrayField(default=None, verbose_name=_("tags")) diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 8e7821a8..21675d1c 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -26,6 +26,7 @@ from taiga.base.utils.slug import ref_uniquely from taiga.projects.notifications import WatchedModelMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.mixins.blocked import BlockedMixin +from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): @@ -100,3 +101,14 @@ def issue_finished_date_handler(sender, instance, **kwargs): def issue_tags_normalization(sender, instance, **kwargs): if isinstance(instance.tags, (list, tuple)): instance.tags = list(map(lambda x: x.lower(), instance.tags)) + + +@receiver(models.signals.post_save, sender=Issue, dispatch_uid="issue_update_project_colors") +def issue_update_project_tags(sender, instance, **kwargs): + update_project_tags_colors_handler(instance) + + +@receiver(models.signals.post_delete, sender=Issue, dispatch_uid="issue_update_project_colors_on_delete") +def issue_update_project_tags_on_delete(sender, instance, **kwargs): + remove_unused_tags(instance.project) + instance.project.save() diff --git a/taiga/projects/migrations/0033_auto__add_field_project_tags_colors.py b/taiga/projects/migrations/0033_auto__add_field_project_tags_colors.py new file mode 100644 index 00000000..45d5c40a --- /dev/null +++ b/taiga/projects/migrations/0033_auto__add_field_project_tags_colors.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Project.tags_colors' + db.add_column('projects_project', 'tags_colors', + self.gf('djorm_pgarray.fields.TextArrayField')(blank=True, default={}, dimension=2, dbtype='text'), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Project.tags_colors' + db.delete_column('projects_project', 'tags_colors') + + + models = { + 'projects.issuestatus': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueStatus', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.issuetype': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'IssueType', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'issue_types'", 'to': "orm['projects.Project']"}) + }, + 'projects.membership': { + 'Meta': {'ordering': "['project', 'user__full_name', 'user__username', 'user__email', 'email']", 'object_name': 'Membership', 'unique_together': "(('user', 'project'),)"}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True', 'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '255', 'null': 'True', 'default': 'None'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invited_by_id': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['projects.Project']"}), + 'role': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'memberships'", 'to': "orm['users.Role']"}), + 'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '60', 'null': 'True', 'default': 'None'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'to': "orm['users.User']", 'related_name': "'memberships'"}) + }, + 'projects.points': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Points', 'unique_together': "(('project', 'name'),)"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'points'", 'to': "orm['projects.Project']"}), + 'value': ('django.db.models.fields.FloatField', [], {'blank': 'True', 'null': 'True', 'default': 'None'}) + }, + 'projects.priority': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Priority', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'priorities'", 'to': "orm['projects.Project']"}) + }, + 'projects.project': { + 'Meta': {'ordering': "['name']", 'object_name': 'Project'}, + 'anon_permissions': ('djorm_pgarray.fields.TextArrayField', [], {'blank': 'True', 'null': 'True', 'default': '[]', 'dbtype': "'text'"}), + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'creation_template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'to': "orm['projects.ProjectTemplate']", 'related_name': "'projects'"}), + 'default_issue_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.IssueStatus']", 'unique': 'True'}), + 'default_issue_type': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.IssueType']", 'unique': 'True'}), + 'default_points': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.Points']", 'unique': 'True'}), + 'default_priority': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.Priority']", 'unique': 'True'}), + 'default_severity': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.Severity']", 'unique': 'True'}), + 'default_task_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.TaskStatus']", 'unique': 'True'}), + 'default_us_status': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'+'", 'on_delete': 'models.SET_NULL', 'blank': 'True', 'null': 'True', 'to': "orm['projects.UserStoryStatus']", 'unique': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'members': ('django.db.models.fields.related.ManyToManyField', [], {'through': "orm['projects.Membership']", 'related_name': "'projects'", 'to': "orm['users.User']", 'symmetrical': 'False'}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250', 'unique': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'owned_projects'", 'to': "orm['users.User']"}), + 'public_permissions': ('djorm_pgarray.fields.TextArrayField', [], {'blank': 'True', 'null': 'True', 'default': '[]', 'dbtype': "'text'"}), + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250', 'unique': 'True'}), + 'tags': ('djorm_pgarray.fields.TextArrayField', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'dbtype': "'text'"}), + 'tags_colors': ('djorm_pgarray.fields.TextArrayField', [], {'blank': 'True', 'default': '{}', 'dimension': '2', 'dbtype': "'text'"}), + 'total_milestones': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True', 'default': '0'}), + 'total_story_points': ('django.db.models.fields.FloatField', [], {'default': '0'}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '250', 'null': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '250', 'null': 'True'}) + }, + 'projects.projecttemplate': { + 'Meta': {'ordering': "['name']", 'object_name': 'ProjectTemplate'}, + 'created_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now_add': 'True'}), + 'default_options': ('django_pgjson.fields.JsonField', [], {}), + 'default_owner_role': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_backlog_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_issues_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_kanban_activated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_wiki_activated': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'issue_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'issue_types': ('django_pgjson.fields.JsonField', [], {}), + 'modified_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '250'}), + 'points': ('django_pgjson.fields.JsonField', [], {}), + 'priorities': ('django_pgjson.fields.JsonField', [], {}), + 'roles': ('django_pgjson.fields.JsonField', [], {}), + 'severities': ('django_pgjson.fields.JsonField', [], {}), + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250', 'unique': 'True'}), + 'task_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'us_statuses': ('django_pgjson.fields.JsonField', [], {}), + 'videoconferences': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '250', 'null': 'True'}), + 'videoconferences_salt': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '250', 'null': 'True'}) + }, + 'projects.severity': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'Severity', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'severities'", 'to': "orm['projects.Project']"}) + }, + 'projects.taskstatus': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'TaskStatus', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'task_statuses'", 'to': "orm['projects.Project']"}) + }, + 'projects.userstorystatus': { + 'Meta': {'ordering': "['project', 'order', 'name']", 'object_name': 'UserStoryStatus', 'unique_together': "(('project', 'name'),)"}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '20', 'default': "'#999999'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'us_statuses'", 'to': "orm['projects.Project']"}), + 'wip_limit': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True', 'default': 'None'}) + }, + 'users.role': { + 'Meta': {'ordering': "['order', 'slug']", 'object_name': 'Role', 'unique_together': "(('slug', 'project'),)"}, + 'computable': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10'}), + 'permissions': ('djorm_pgarray.fields.TextArrayField', [], {'blank': 'True', 'null': 'True', 'default': '[]', 'dbtype': "'text'"}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'roles'", 'to': "orm['projects.Project']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '250'}) + }, + 'users.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'bio': ('django.db.models.fields.TextField', [], {'blank': 'True', 'default': "''"}), + 'color': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '9', 'default': "'#31d025'"}), + 'colorize_tags': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'default_language': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '20', 'default': "''"}), + 'default_timezone': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '20', 'default': "''"}), + 'email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75'}), + 'email_token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '200', 'null': 'True', 'default': 'None'}), + 'full_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '256'}), + 'github_id': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'new_email': ('django.db.models.fields.EmailField', [], {'blank': 'True', 'max_length': '75', 'null': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'photo': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'max_length': '500', 'null': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '200', 'null': 'True', 'default': 'None'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}) + } + } + + complete_apps = ['projects'] \ No newline at end of file diff --git a/taiga/projects/models.py b/taiga/projects/models.py index b6a7fa70..6f943491 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -162,6 +162,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): is_private = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is private")) + tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default={}) + class Meta: verbose_name = "project" verbose_name_plural = "projects" diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 511b7144..d6d12b86 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -18,7 +18,7 @@ from os import path from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from taiga.base.serializers import JsonField, PgArrayField, ModelSerializer +from taiga.base.serializers import JsonField, PgArrayField, ModelSerializer, TagsColorsField from taiga.users.models import Role, User from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserSerializer @@ -141,6 +141,7 @@ class ProjectSerializer(ModelSerializer): stars = serializers.SerializerMethodField("get_stars_number") my_permissions = serializers.SerializerMethodField("get_my_permissions") i_am_owner = serializers.SerializerMethodField("get_i_am_owner") + tags_colors = TagsColorsField(required=False) class Meta: model = models.Project diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 236bbf54..cd394f9a 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -36,3 +36,5 @@ from .members import get_members_from_bulk from .invitations import send_invitation from .invitations import find_invited_user + +from .tags_colors import update_project_tags_colors_handler diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py new file mode 100644 index 00000000..5620154e --- /dev/null +++ b/taiga/projects/services/tags_colors.py @@ -0,0 +1,51 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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.conf import settings + +from taiga.projects.services.filters import get_all_tags + +from hashlib import sha1 + + +def _generate_color(tag): + color = sha1(tag.encode("utf-8")).hexdigest()[0:6] + return "#{}".format(color) + + +def _get_new_color(tag, predefined_colors, exclude=[]): + colors = list(set(predefined_colors) - set(exclude)) + if colors: + return colors[0] + return _generate_color(tag) + + +def remove_unused_tags(project): + current_tags = get_all_tags(project) + project.tags_colors = list(filter(lambda x: x[0] in current_tags, project.tags_colors)) + + +def update_project_tags_colors_handler(instance): + for tag in instance.tags: + defined_tags = map(lambda x: x[0], instance.project.tags_colors) + if tag not in defined_tags: + used_colors = map(lambda x: x[1], instance.project.tags_colors) + new_color = _get_new_color(tag, settings.TAGS_PREDEFINED_COLORS, + exclude=used_colors) + instance.project.tags_colors.append([tag, new_color]) + + remove_unused_tags(instance.project) + instance.project.save() diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index a7abd10f..0bf6d6e0 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -29,6 +29,7 @@ from taiga.projects.userstories.models import UserStory from taiga.projects.userstories import services as us_service from taiga.projects.milestones.models import Milestone from taiga.projects.mixins.blocked import BlockedMixin +from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): @@ -139,3 +140,14 @@ def tasks_milestone_close_handler(sender, instance, **kwargs): elif not instance.status.is_closed and instance.milestone.closed: instance.milestone.closed = False instance.milestone.save(update_fields=["closed"]) + + +@receiver(models.signals.post_save, sender=Task, dispatch_uid="task_update_project_colors") +def task_update_project_tags(sender, instance, **kwargs): + update_project_tags_colors_handler(instance) + + +@receiver(models.signals.post_delete, sender=Task, dispatch_uid="task_update_project_colors_on_delete") +def task_update_project_tags_on_delete(sender, instance, **kwargs): + remove_unused_tags(instance.project) + instance.project.save() diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 827f9974..b384deaa 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -25,6 +25,7 @@ from taiga.base.utils.slug import ref_uniquely from taiga.projects.notifications import WatchedModelMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.mixins.blocked import BlockedMixin +from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags class RolePoints(models.Model): @@ -165,3 +166,14 @@ def us_close_open_on_status_change(sender, instance, **kwargs): service.close_userstory(instance) else: service.open_userstory(instance) + + +@receiver(models.signals.post_save, sender=UserStory, dispatch_uid="user_story_update_project_colors") +def us_update_project_tags(sender, instance, **kwargs): + update_project_tags_colors_handler(instance) + + +@receiver(models.signals.post_delete, sender=UserStory, dispatch_uid="user_story_update_project_colors_on_delete") +def us_update_project_tags_on_delete(sender, instance, **kwargs): + remove_unused_tags(instance.project) + instance.project.save()