diff --git a/taiga/base/utils/collections.py b/taiga/base/utils/collections.py new file mode 100644 index 00000000..c5ca3c59 --- /dev/null +++ b/taiga/base/utils/collections.py @@ -0,0 +1,80 @@ +# -*- 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 . + +import collections + + +class OrderedSet(collections.MutableSet): + # Extract from: + # - https://docs.python.org/3/library/collections.abc.html?highlight=orderedset + # - https://code.activestate.com/recipes/576694/ + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py index d93ebe72..c2dbd38a 100644 --- a/taiga/projects/tagging/api.py +++ b/taiga/projects/tagging/api.py @@ -18,6 +18,7 @@ from taiga.base import response from taiga.base.decorators import detail_route +from taiga.base.utils.collections import OrderedSet from . import services from . import serializers @@ -101,11 +102,10 @@ 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_obj_tags = OrderedSet() new_tags_colors = {} for tag in obj.tags: