Assign a color when create a new tag
parent
3e555de7c4
commit
8c45033f18
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.utils.translation import ugettext_lazy
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,35 +99,6 @@ class PickledObjectField(serializers.WritableField):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class TagsField(serializers.WritableField):
|
|
||||||
"""
|
|
||||||
Pickle objects serializer.
|
|
||||||
"""
|
|
||||||
def to_native(self, obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def from_native(self, data):
|
|
||||||
if not data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
ret = sum([tag.split(",") for tag in data], [])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
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 WatchersField(serializers.WritableField):
|
class WatchersField(serializers.WritableField):
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
return obj
|
return obj
|
||||||
|
|
|
@ -66,9 +66,9 @@ from . import services
|
||||||
######################################################
|
######################################################
|
||||||
## Project
|
## Project
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
BlockeableSaveMixin, BlockeableDeleteMixin, 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
|
||||||
|
|
|
@ -25,13 +25,14 @@ from django.db.models import signals
|
||||||
|
|
||||||
def connect_projects_signals():
|
def connect_projects_signals():
|
||||||
from . import signals as handlers
|
from . import signals as handlers
|
||||||
|
from .tagging import signals as tagging_handlers
|
||||||
# On project object is created apply template.
|
# On project object is created apply template.
|
||||||
signals.post_save.connect(handlers.project_post_save,
|
signals.post_save.connect(handlers.project_post_save,
|
||||||
sender=apps.get_model("projects", "Project"),
|
sender=apps.get_model("projects", "Project"),
|
||||||
dispatch_uid='project_post_save')
|
dispatch_uid='project_post_save')
|
||||||
|
|
||||||
# Tags normalization after save a project
|
# Tags normalization after save a project
|
||||||
signals.pre_save.connect(handlers.tags_normalization,
|
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||||
sender=apps.get_model("projects", "Project"),
|
sender=apps.get_model("projects", "Project"),
|
||||||
dispatch_uid="tags_normalization_projects")
|
dispatch_uid="tags_normalization_projects")
|
||||||
|
|
||||||
|
|
|
@ -27,11 +27,11 @@ from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
|
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.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.tagging.mixins import TaggedResourceMixin
|
||||||
|
|
||||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
|
||||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -41,7 +41,7 @@ from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
BlockedByProjectMixin, ModelCrudViewSet):
|
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
queryset = models.Issue.objects.all()
|
queryset = models.Issue.objects.all()
|
||||||
permission_classes = (permissions.IssuePermission, )
|
permission_classes = (permissions.IssuePermission, )
|
||||||
filter_backends = (filters.CanViewIssuesFilterBackend,
|
filter_backends = (filters.CanViewIssuesFilterBackend,
|
||||||
|
@ -196,7 +196,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
|
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
|
||||||
priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter)
|
priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter)
|
||||||
severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter)
|
severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter)
|
||||||
tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
|
|
||||||
|
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
querysets = {
|
querysets = {
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.db.models import signals
|
||||||
|
|
||||||
def connect_issues_signals():
|
def connect_issues_signals():
|
||||||
from taiga.projects import signals as generic_handlers
|
from taiga.projects import signals as generic_handlers
|
||||||
|
from taiga.projects.tagging import signals as tagging_handlers
|
||||||
from . import signals as handlers
|
from . import signals as handlers
|
||||||
|
|
||||||
# Finished date
|
# Finished date
|
||||||
|
@ -31,7 +32,7 @@ def connect_issues_signals():
|
||||||
dispatch_uid="set_finished_date_when_edit_issue")
|
dispatch_uid="set_finished_date_when_edit_issue")
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||||
sender=apps.get_model("issues", "Issue"),
|
sender=apps.get_model("issues", "Issue"),
|
||||||
dispatch_uid="tags_normalization_issue")
|
dispatch_uid="tags_normalization_issue")
|
||||||
|
|
||||||
|
|
|
@ -17,15 +17,15 @@
|
||||||
# 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 taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import TagsField
|
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicIssueStatusSerializer
|
from taiga.projects.serializers import BasicIssueStatusSerializer
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||||
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -33,8 +33,9 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||||
tags = TagsField(required=False)
|
serializers.ModelSerializer):
|
||||||
|
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
is_closed = serializers.Field(source="is_closed")
|
is_closed = serializers.Field(source="is_closed")
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
@ -71,7 +72,7 @@ class IssueListSerializer(IssueSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Issue
|
model = models.Issue
|
||||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||||
exclude=("description", "description_html")
|
exclude = ("description", "description_html")
|
||||||
|
|
||||||
|
|
||||||
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||||
|
|
|
@ -24,28 +24,27 @@ from django.db.models import Q
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import JsonField
|
from taiga.base.fields import JsonField
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.fields import TagsField
|
|
||||||
from taiga.base.fields import TagsColorsField
|
|
||||||
|
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
from taiga.users.serializers import UserSerializer
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
from taiga.users.serializers import ProjectRoleSerializer
|
from taiga.users.serializers import ProjectRoleSerializer
|
||||||
from taiga.users.validators import RoleExistsValidator
|
from taiga.users.validators import RoleExistsValidator
|
||||||
|
|
||||||
from taiga.permissions.services import get_user_project_permissions
|
from taiga.permissions.services import get_user_project_permissions
|
||||||
from taiga.permissions.services import is_project_admin, is_project_owner
|
from taiga.permissions.services import is_project_admin, is_project_owner
|
||||||
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import services
|
from . import services
|
||||||
from .notifications.mixins import WatchedResourceModelSerializer
|
|
||||||
from .validators import ProjectExistsValidator
|
|
||||||
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
||||||
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
||||||
from .likes.mixins.serializers import FanResourceSerializerMixin
|
from .likes.mixins.serializers import FanResourceSerializerMixin
|
||||||
|
from .mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
||||||
|
from .notifications.choices import NotifyLevel
|
||||||
|
from .notifications.mixins import WatchedResourceModelSerializer
|
||||||
|
from .tagging.fields import TagsField
|
||||||
|
from .tagging.fields import TagsColorsField
|
||||||
|
from .validators import ProjectExistsValidator
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
|
|
||||||
def tag_exist_for_project_elements(project, tag):
|
def tag_exist_for_project_elements(project, tag):
|
||||||
return tag in dict(project.tags_colors).keys()
|
return tag in dict(project.tags_colors).keys()
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,6 @@ from easy_thumbnails.files import get_thumbnailer
|
||||||
# Signals over project items
|
# Signals over project items
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
## TAGS
|
|
||||||
|
|
||||||
def tags_normalization(sender, instance, **kwargs):
|
|
||||||
if isinstance(instance.tags, (list, tuple)):
|
|
||||||
instance.tags = list(map(str.lower, instance.tags))
|
|
||||||
|
|
||||||
|
|
||||||
## Membership
|
## Membership
|
||||||
|
|
||||||
def membership_post_delete(sender, instance, using, **kwargs):
|
def membership_post_delete(sender, instance, using, **kwargs):
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- 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.forms import widgets
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class TagsAndTagsColorsField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
Pickle objects serializer fior stories, tasks and issues tags.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
def _validate_tag_field(value):
|
||||||
|
# Valid field:
|
||||||
|
# - ["tag1", "tag2", "tag3"...]
|
||||||
|
# - ["tag1", ["tag2", None], ["tag3", "#ccc"], [tag4, #cccccc]...]
|
||||||
|
for tag in value:
|
||||||
|
if isinstance(tag, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(tag, (list, tuple)) and len(tag) == 2:
|
||||||
|
name = tag[0]
|
||||||
|
color = tag[1]
|
||||||
|
|
||||||
|
if isinstance(name, str):
|
||||||
|
if color is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(color, str) and re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ValidationError(_("Invalid tag '{value}'. The color is not a "
|
||||||
|
"valid HEX color or null.").format(value=tag))
|
||||||
|
|
||||||
|
raise ValidationError(_("Invalid tag '{value}'. it must be the name or a pair "
|
||||||
|
"'[\"name\", \"hex color/\" | null]'.").format(value=tag))
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validators.append(_validate_tag_field)
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class TagsField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
Pickle objects serializer for tags names.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
def _validate_tag_field(value):
|
||||||
|
for tag in value:
|
||||||
|
if isinstance(tag, str):
|
||||||
|
continue
|
||||||
|
raise ValidationError(_("Invalid tag '{value}'. It must be the tag name.").format(value=tag))
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.validators.append(_validate_tag_field)
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
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())
|
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- 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,23 @@
|
||||||
|
# -*- 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 tags_normalization(sender, instance, **kwargs):
|
||||||
|
if isinstance(instance.tags, (list, tuple)):
|
||||||
|
instance.tags = list(map(str.lower, instance.tags))
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
# 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.http import HttpResponse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
@ -24,15 +25,13 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.projects.models import Project, TaskStatus
|
|
||||||
from django.http import HttpResponse
|
|
||||||
|
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
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.occ import OCCResourceMixin
|
||||||
|
from taiga.projects.tagging.mixins 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
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -40,13 +39,18 @@ from . import services
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
BlockedByProjectMixin, ModelCrudViewSet):
|
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
queryset = models.Task.objects.all()
|
queryset = models.Task.objects.all()
|
||||||
permission_classes = (permissions.TaskPermission,)
|
permission_classes = (permissions.TaskPermission,)
|
||||||
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
||||||
retrieve_exclude_filters = (filters.WatchersFilter,)
|
retrieve_exclude_filters = (filters.WatchersFilter,)
|
||||||
filter_fields = ["user_story", "milestone", "project", "assigned_to",
|
filter_fields = [
|
||||||
"status__is_closed"]
|
"user_story",
|
||||||
|
"milestone",
|
||||||
|
"project",
|
||||||
|
"assigned_to",
|
||||||
|
"status__is_closed"
|
||||||
|
]
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
if self.action in ["retrieve", "by_ref"]:
|
if self.action in ["retrieve", "by_ref"]:
|
||||||
|
|
|
@ -23,13 +23,15 @@ from django.db.models import signals
|
||||||
|
|
||||||
def connect_tasks_signals():
|
def connect_tasks_signals():
|
||||||
from taiga.projects import signals as generic_handlers
|
from taiga.projects import signals as generic_handlers
|
||||||
|
from taiga.projects.tagging import signals as tagging_handlers
|
||||||
from . import signals as handlers
|
from . import signals as handlers
|
||||||
|
|
||||||
# Finished date
|
# Finished date
|
||||||
signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
|
signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
|
||||||
sender=apps.get_model("tasks", "Task"),
|
sender=apps.get_model("tasks", "Task"),
|
||||||
dispatch_uid="set_finished_date_when_edit_task")
|
dispatch_uid="set_finished_date_when_edit_task")
|
||||||
# Tags
|
# Tags
|
||||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||||
sender=apps.get_model("tasks", "Task"),
|
sender=apps.get_model("tasks", "Task"),
|
||||||
dispatch_uid="tags_normalization_task")
|
dispatch_uid="tags_normalization_task")
|
||||||
|
|
||||||
|
|
|
@ -17,19 +17,18 @@
|
||||||
# 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 taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.fields import TagsField
|
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
|
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
|
||||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
from taiga.projects.milestones.validators import SprintExistsValidator
|
||||||
from taiga.projects.tasks.validators import TaskExistsValidator
|
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||||
|
from taiga.projects.tasks.validators import TaskExistsValidator
|
||||||
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -37,14 +36,15 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||||
tags = TagsField(required=False, default=[])
|
serializers.ModelSerializer):
|
||||||
|
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
|
milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
|
||||||
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
||||||
description_html = serializers.SerializerMethodField("get_description_html")
|
description_html = serializers.SerializerMethodField("get_description_html")
|
||||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
is_closed = serializers.SerializerMethodField("get_is_closed")
|
||||||
status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
|
status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
|
||||||
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
||||||
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
|
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
|
||||||
|
@ -76,7 +76,7 @@ class TaskListSerializer(TaskSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Task
|
model = models.Task
|
||||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||||
exclude=("description", "description_html")
|
exclude = ("description", "description_html")
|
||||||
|
|
||||||
|
|
||||||
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||||
|
@ -101,6 +101,7 @@ class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
|
||||||
us_id = serializers.IntegerField(required=False)
|
us_id = serializers.IntegerField(required=False)
|
||||||
bulk_tasks = serializers.CharField()
|
bulk_tasks = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
## Order bulk serializers
|
## Order bulk serializers
|
||||||
|
|
||||||
class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
|
class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
|
@ -31,12 +30,13 @@ from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
|
||||||
from taiga.projects.models import Project, UserStoryStatus
|
|
||||||
from taiga.projects.milestones.models import Milestone
|
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
|
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.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
@ -46,7 +46,7 @@ from . import services
|
||||||
|
|
||||||
|
|
||||||
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
BlockedByProjectMixin, ModelCrudViewSet):
|
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
queryset = models.UserStory.objects.all()
|
queryset = models.UserStory.objects.all()
|
||||||
permission_classes = (permissions.UserStoryPermission,)
|
permission_classes = (permissions.UserStoryPermission,)
|
||||||
filter_backends = (filters.CanViewUsFilterBackend,
|
filter_backends = (filters.CanViewUsFilterBackend,
|
||||||
|
@ -113,8 +113,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
# This is very ugly hack, but having
|
# This is very ugly hack, but having
|
||||||
# restframework is the only way to do it.
|
# restframework is the only way to do it.
|
||||||
|
#
|
||||||
# NOTE: code moved as is from serializer
|
# NOTE: code moved as is from serializer
|
||||||
# to api because is not serializer logic.
|
# to api because is not serializer logic.
|
||||||
related_data = getattr(obj, "_related_data", {})
|
related_data = getattr(obj, "_related_data", {})
|
||||||
self._role_points = related_data.pop("role_points", None)
|
self._role_points = related_data.pop("role_points", None)
|
||||||
|
|
||||||
|
@ -124,7 +125,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
super().pre_save(obj)
|
super().pre_save(obj)
|
||||||
|
|
||||||
def post_save(self, obj, created=False):
|
def post_save(self, obj, created=False):
|
||||||
# Code related to the hack of pre_save method. Rather, this is the continuation of it.
|
# Code related to the hack of pre_save method.
|
||||||
|
# Rather, this is the continuation of it.
|
||||||
if self._role_points:
|
if self._role_points:
|
||||||
Points = apps.get_model("projects", "Points")
|
Points = apps.get_model("projects", "Points")
|
||||||
RolePoints = apps.get_model("userstories", "RolePoints")
|
RolePoints = apps.get_model("userstories", "RolePoints")
|
||||||
|
@ -134,14 +136,16 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk,
|
role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk,
|
||||||
role__computable=True)
|
role__computable=True)
|
||||||
except (ValueError, RolePoints.DoesNotExist):
|
except (ValueError, RolePoints.DoesNotExist):
|
||||||
raise exc.BadRequest({"points": _("Invalid role id '{role_id}'").format(
|
raise exc.BadRequest({
|
||||||
role_id=role_id)})
|
"points": _("Invalid role id '{role_id}'").format(role_id=role_id)
|
||||||
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id)
|
role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id)
|
||||||
except (ValueError, Points.DoesNotExist):
|
except (ValueError, Points.DoesNotExist):
|
||||||
raise exc.BadRequest({"points": _("Invalid points id '{points_id}'").format(
|
raise exc.BadRequest({
|
||||||
points_id=points_id)})
|
"points": _("Invalid points id '{points_id}'").format(points_id=points_id)
|
||||||
|
})
|
||||||
|
|
||||||
role_points.save()
|
role_points.save()
|
||||||
|
|
||||||
|
@ -200,7 +204,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
|
statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
|
||||||
assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
|
assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
|
||||||
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
|
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
|
||||||
tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
|
|
||||||
|
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
querysets = {
|
querysets = {
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.db.models import signals
|
||||||
|
|
||||||
def connect_userstories_signals():
|
def connect_userstories_signals():
|
||||||
from taiga.projects import signals as generic_handlers
|
from taiga.projects import signals as generic_handlers
|
||||||
|
from taiga.projects.tagging import signals as tagging_handlers
|
||||||
from . import signals as handlers
|
from . import signals as handlers
|
||||||
|
|
||||||
# When deleting user stories we must disable task signals while delating and
|
# When deleting user stories we must disable task signals while delating and
|
||||||
|
@ -59,7 +60,7 @@ def connect_userstories_signals():
|
||||||
dispatch_uid="try_to_close_milestone_when_delete_us")
|
dispatch_uid="try_to_close_milestone_when_delete_us")
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||||
sender=apps.get_model("userstories", "UserStory"),
|
sender=apps.get_model("userstories", "UserStory"),
|
||||||
dispatch_uid="tags_normalization_user_story")
|
dispatch_uid="tags_normalization_user_story")
|
||||||
|
|
||||||
|
|
|
@ -16,23 +16,22 @@
|
||||||
# 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.apps import apps
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.fields import TagsField
|
|
||||||
from taiga.base.fields import PickledObjectField
|
from taiga.base.fields import PickledObjectField
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
|
||||||
from taiga.projects.models import Project
|
|
||||||
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
|
|
||||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
from taiga.projects.milestones.validators import SprintExistsValidator
|
||||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
from taiga.projects.models import Project
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||||
|
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||||
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||||
|
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
||||||
|
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
|
||||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
from taiga.users.serializers import UserBasicInfoSerializer
|
||||||
|
@ -50,9 +49,9 @@ class RolePointsField(serializers.WritableField):
|
||||||
return json.loads(obj)
|
return json.loads(obj)
|
||||||
|
|
||||||
|
|
||||||
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
||||||
serializers.ModelSerializer):
|
EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
points = RolePointsField(source="role_points", required=False)
|
points = RolePointsField(source="role_points", required=False)
|
||||||
total_points = serializers.SerializerMethodField("get_total_points")
|
total_points = serializers.SerializerMethodField("get_total_points")
|
||||||
|
@ -112,7 +111,7 @@ class UserStoryListSerializer(UserStorySerializer):
|
||||||
model = models.UserStory
|
model = models.UserStory
|
||||||
depth = 0
|
depth = 0
|
||||||
read_only_fields = ('created_date', 'modified_date')
|
read_only_fields = ('created_date', 'modified_date')
|
||||||
exclude=("description", "description_html")
|
exclude = ("description", "description_html")
|
||||||
|
|
||||||
|
|
||||||
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
||||||
|
@ -142,7 +141,8 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial
|
||||||
order = serializers.IntegerField()
|
order = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
|
||||||
|
serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
|
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import TagsField, PgArrayField, JsonField
|
from taiga.base.fields import PgArrayField, JsonField
|
||||||
|
|
||||||
from taiga.front.templatetags.functions import resolve as resolve_front_url
|
from taiga.front.templatetags.functions import resolve as resolve_front_url
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ from taiga.projects.milestones import models as milestone_models
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||||
from taiga.projects.services import get_logo_big_thumbnail_url
|
from taiga.projects.services import get_logo_big_thumbnail_url
|
||||||
from taiga.projects.tasks import models as task_models
|
from taiga.projects.tasks import models as task_models
|
||||||
|
from taiga.projects.tagging.fields import TagsField
|
||||||
from taiga.projects.userstories import models as us_models
|
from taiga.projects.userstories import models as us_models
|
||||||
from taiga.projects.wiki import models as wiki_models
|
from taiga.projects.wiki import models as wiki_models
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest import mock
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_issue_add_new_tags_with_error(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
issue = f.create_issue(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True)
|
||||||
|
url = reverse("issues-detail", kwargs={"pk": issue.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [],
|
||||||
|
"version": issue.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
|
||||||
|
data["tags"] = [1]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back", "#cccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [[1, "#ccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_issue_add_new_tags_without_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
issue = f.create_issue(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True)
|
||||||
|
url = reverse("issues-detail", kwargs={"pk": issue.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", None],
|
||||||
|
["front", None],
|
||||||
|
["ux", None]
|
||||||
|
],
|
||||||
|
"version": issue.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_issue_add_new_tags_with_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
issue = f.create_issue(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True)
|
||||||
|
url = reverse("issues-detail", kwargs={"pk": issue.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
],
|
||||||
|
"version": issue.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(issue.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_create_new_issue_with_tags(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
status = f.IssueStatusFactory.create(project=project)
|
||||||
|
project.default_issue_status = status
|
||||||
|
project.save()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
|
||||||
|
url = reverse("issues-list")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"subject": "Test user story",
|
||||||
|
"project": project.id,
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 201, response.data
|
||||||
|
|
||||||
|
assert ("back" in response.data["tags"] and
|
||||||
|
"front" in response.data["tags"] and
|
||||||
|
"ux" in response.data["tags"])
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
|
@ -67,19 +67,6 @@ def test_create_task_without_default_values(client):
|
||||||
assert response.data['status'] == None
|
assert response.data['status'] == None
|
||||||
|
|
||||||
|
|
||||||
def test_api_update_task_tags(client):
|
|
||||||
project = f.ProjectFactory.create()
|
|
||||||
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
|
|
||||||
f.MembershipFactory.create(project=project, user=task.owner, is_admin=True)
|
|
||||||
url = reverse("tasks-detail", kwargs={"pk": task.pk})
|
|
||||||
data = {"tags": ["back", "front"], "version": task.version}
|
|
||||||
|
|
||||||
client.login(task.owner)
|
|
||||||
response = client.json.patch(url, json.dumps(data))
|
|
||||||
|
|
||||||
assert response.status_code == 200, response.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_api_create_in_bulk_with_status(client):
|
def test_api_create_in_bulk_with_status(client):
|
||||||
us = f.create_userstory()
|
us = f.create_userstory()
|
||||||
f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
|
f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True)
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest import mock
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_task_add_new_tags_with_error(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
|
||||||
|
f.MembershipFactory.create(project=project, user=task.owner, is_admin=True)
|
||||||
|
url = reverse("tasks-detail", kwargs={"pk": task.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [],
|
||||||
|
"version": task.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(task.owner)
|
||||||
|
|
||||||
|
data["tags"] = [1]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back", "#cccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [[1, "#ccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_task_add_new_tags_without_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
|
||||||
|
f.MembershipFactory.create(project=project, user=task.owner, is_admin=True)
|
||||||
|
url = reverse("tasks-detail", kwargs={"pk": task.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", None],
|
||||||
|
["front", None],
|
||||||
|
["ux", None]
|
||||||
|
],
|
||||||
|
"version": task.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(task.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_task_add_new_tags_with_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
task = f.create_task(project=project, status__project=project, milestone=None, user_story=None)
|
||||||
|
f.MembershipFactory.create(project=project, user=task.owner, is_admin=True)
|
||||||
|
url = reverse("tasks-detail", kwargs={"pk": task.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
],
|
||||||
|
"version": task.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(task.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_create_new_task_with_tags(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
status = f.TaskStatusFactory.create(project=project)
|
||||||
|
project.default_task_status = status
|
||||||
|
project.save()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
|
||||||
|
url = reverse("tasks-list")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"subject": "Test user story",
|
||||||
|
"project": project.id,
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 201, response.data
|
||||||
|
|
||||||
|
assert ("back" in response.data["tags"] and
|
||||||
|
"front" in response.data["tags"] and
|
||||||
|
"ux" in response.data["tags"])
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
|
@ -0,0 +1,142 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from unittest import mock
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_user_story_add_new_tags_with_error(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
user_story = f.create_userstory(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True)
|
||||||
|
url = reverse("userstories-detail", kwargs={"pk": user_story.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [],
|
||||||
|
"version": user_story.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user_story.owner)
|
||||||
|
|
||||||
|
data["tags"] = [1]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [["back", "#cccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
data["tags"] = [[1, "#ccc"]]
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 400, response.data
|
||||||
|
assert "tags" in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_user_story_add_new_tags_without_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
user_story = f.create_userstory(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True)
|
||||||
|
url = reverse("userstories-detail", kwargs={"pk": user_story.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", None],
|
||||||
|
["front", None],
|
||||||
|
["ux", None]
|
||||||
|
],
|
||||||
|
"version": user_story.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user_story.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_user_story_add_new_tags_with_colors(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
user_story = f.create_userstory(project=project, status__project=project)
|
||||||
|
f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True)
|
||||||
|
url = reverse("userstories-detail", kwargs={"pk": user_story.pk})
|
||||||
|
data = {
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
],
|
||||||
|
"version": user_story.version
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user_story.owner)
|
||||||
|
|
||||||
|
response = client.json.patch(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_create_new_user_story_with_tags(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
status = f.UserStoryStatusFactory.create(project=project)
|
||||||
|
project.default_userstory_status = status
|
||||||
|
project.save()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
|
||||||
|
url = reverse("userstories-list")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"subject": "Test user story",
|
||||||
|
"project": project.id,
|
||||||
|
"tags": [
|
||||||
|
["back", "#fff8e7"],
|
||||||
|
["front", None],
|
||||||
|
["ux", "#fabada"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 201, response.data
|
||||||
|
|
||||||
|
assert ("back" in response.data["tags"] and
|
||||||
|
"front" in response.data["tags"] and
|
||||||
|
"ux" in response.data["tags"])
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert not tags_colors.keys()
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
tags_colors = OrderedDict(project.tags_colors)
|
||||||
|
assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors
|
||||||
|
assert tags_colors["back"] == "#fff8e7"
|
||||||
|
assert tags_colors["ux"] == "#fabada"
|
Loading…
Reference in New Issue