Merge pull request #751 from taigaio/us/4302/improve_tagging_system_v2
US #4302 - Improve tagging system (v2)remotes/origin/issue/4795/notification_even_they_are_disabled
commit
ef0b7dfe64
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -3,10 +3,15 @@
|
||||||
## 2.2.0 ??? (unreleased)
|
## 2.2.0 ??? (unreleased)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Now comment owners and project admins can edit existing comments with the history Entry endpoint.
|
- Include created, modified and finished dates for tasks in CSV reports.
|
||||||
- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose.
|
|
||||||
- Include created, modified and finished dates for tasks in CSV reports
|
|
||||||
- Add gravatar url to Users API endpoint.
|
- Add gravatar url to Users API endpoint.
|
||||||
|
- Comments:
|
||||||
|
- Now comment owners and project admins can edit existing comments with the history Entry endpoint.
|
||||||
|
- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose.
|
||||||
|
- Tags:
|
||||||
|
- New API endpoints over projects to create, rename, edit, delete and mix tags.
|
||||||
|
- Tag color assignation is not automatic.
|
||||||
|
- Select a color (or not) to a tag when add it to stories, issues and tasks.
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
- Lots of small and not so small bugfixes.
|
- Lots of small and not so small bugfixes.
|
||||||
|
|
|
@ -10,7 +10,7 @@ six==1.10.0
|
||||||
amqp==1.4.9
|
amqp==1.4.9
|
||||||
djmail==0.12.0.post1
|
djmail==0.12.0.post1
|
||||||
django-pgjson==0.3.1
|
django-pgjson==0.3.1
|
||||||
djorm-pgarray==1.2
|
djorm-pgarray==1.2 # Use until Taiga 2.1. Keep compatibility with old migrations
|
||||||
django-jinja==2.1.2
|
django-jinja==2.1.2
|
||||||
jinja2==2.8
|
jinja2==2.8
|
||||||
pygments==2.0.2
|
pygments==2.0.2
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
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 taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,35 +98,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
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
|
||||||
|
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)
|
|
@ -22,38 +22,38 @@ from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import signals, Prefetch
|
from django.db.models import signals, Prefetch
|
||||||
from django.db.models import Value as V
|
from django.db.models import Value as V
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.core.exceptions import ValidationError
|
from django.http import Http404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.http import Http404
|
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
||||||
from taiga.base.api.permissions import AllowAnyPermission
|
from taiga.base.api.permissions import AllowAnyPermission
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
from taiga.base.decorators import list_route
|
||||||
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
|
||||||
|
from taiga.permissions import services as permissions_services
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
from taiga.projects.issues.models import Issue
|
||||||
|
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||||
from taiga.projects.notifications.models import NotifyPolicy
|
from taiga.projects.notifications.models import NotifyPolicy
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
|
|
||||||
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
|
||||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||||
|
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
||||||
|
from taiga.projects.tasks.models import Task
|
||||||
|
from taiga.projects.tagging.api import TagsColorsResourceMixin
|
||||||
|
|
||||||
from taiga.projects.userstories.models import UserStory, RolePoints
|
from taiga.projects.userstories.models import UserStory, RolePoints
|
||||||
from taiga.projects.tasks.models import Task
|
|
||||||
from taiga.projects.issues.models import Issue
|
|
||||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
|
||||||
from taiga.permissions import services as permissions_services
|
|
||||||
from taiga.users import services as users_services
|
from taiga.users import services as users_services
|
||||||
|
|
||||||
from . import filters as project_filters
|
from . import filters as project_filters
|
||||||
|
@ -66,9 +66,9 @@ from . import services
|
||||||
######################################################
|
######################################################
|
||||||
## Project
|
## Project
|
||||||
######################################################
|
######################################################
|
||||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
|
||||||
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
|
||||||
|
|
||||||
|
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
|
||||||
|
TagsColorsResourceMixin, 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
|
||||||
|
@ -327,12 +327,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
self.check_permissions(request, "issues_stats", project)
|
self.check_permissions(request, "issues_stats", project)
|
||||||
return response.Ok(services.get_stats_for_project_issues(project))
|
return response.Ok(services.get_stats_for_project_issues(project))
|
||||||
|
|
||||||
@detail_route(methods=["GET"])
|
|
||||||
def tags_colors(self, request, pk=None):
|
|
||||||
project = self.get_object()
|
|
||||||
self.check_permissions(request, "tags_colors", project)
|
|
||||||
return response.Ok(dict(project.tags_colors))
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def transfer_validate_token(self, request, pk=None):
|
def transfer_validate_token(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
|
@ -405,6 +399,10 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||||
services.reject_project_transfer(project, request.user, token, reason)
|
services.reject_project_transfer(project, request.user, token, reason)
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
|
def _raise_if_blocked(self, project):
|
||||||
|
if self.is_blocked(project):
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
def _set_base_permissions(self, obj):
|
def _set_base_permissions(self, obj):
|
||||||
update_permissions = False
|
update_permissions = False
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
|
|
|
@ -25,18 +25,16 @@ 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")
|
||||||
signals.pre_save.connect(handlers.update_project_tags_when_create_or_edit_taggable_item,
|
|
||||||
sender=apps.get_model("projects", "Project"),
|
|
||||||
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
|
|
||||||
|
|
||||||
|
|
||||||
def disconnect_projects_signals():
|
def disconnect_projects_signals():
|
||||||
|
@ -44,8 +42,6 @@ def disconnect_projects_signals():
|
||||||
dispatch_uid='project_post_save')
|
dispatch_uid='project_post_save')
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
|
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
|
||||||
dispatch_uid="tags_normalization_projects")
|
dispatch_uid="tags_normalization_projects")
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
|
|
||||||
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
|
|
||||||
|
|
||||||
|
|
||||||
## Memberships Signals
|
## Memberships Signals
|
||||||
|
|
|
@ -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.api 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,15 +32,9 @@ 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")
|
||||||
signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
|
|
||||||
sender=apps.get_model("issues", "Issue"),
|
|
||||||
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue")
|
|
||||||
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
|
|
||||||
sender=apps.get_model("issues", "Issue"),
|
|
||||||
dispatch_uid="update_project_tags_when_delete_taggable_item_issue")
|
|
||||||
|
|
||||||
|
|
||||||
def connect_issues_custom_attributes_signals():
|
def connect_issues_custom_attributes_signals():
|
||||||
|
@ -56,14 +51,15 @@ def connect_all_issues_signals():
|
||||||
|
|
||||||
|
|
||||||
def disconnect_issues_signals():
|
def disconnect_issues_signals():
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="set_finished_date_when_edit_issue")
|
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="tags_normalization_issue")
|
dispatch_uid="set_finished_date_when_edit_issue")
|
||||||
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue")
|
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_delete_taggable_item_issue")
|
dispatch_uid="tags_normalization_issue")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_issues_custom_attributes_signals():
|
def disconnect_issues_custom_attributes_signals():
|
||||||
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="create_custom_attribute_value_when_create_issue")
|
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
|
dispatch_uid="create_custom_attribute_value_when_create_issue")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_all_issues_signals():
|
def disconnect_all_issues_signals():
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-14 12:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('issues', '0006_remove_issue_watchers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='external_reference',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='tags',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,19 +18,16 @@
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from djorm_pgarray.fields import TextArrayField
|
|
||||||
|
|
||||||
from taiga.projects.occ import OCCModelMixin
|
from taiga.projects.occ import OCCModelMixin
|
||||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||||
from taiga.projects.mixins.blocked import BlockedMixin
|
from taiga.projects.mixins.blocked import BlockedMixin
|
||||||
from taiga.base.tags import TaggedMixin
|
from taiga.projects.tagging.models import TaggedMixin
|
||||||
|
|
||||||
from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags
|
|
||||||
|
|
||||||
|
|
||||||
class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
|
class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
|
||||||
|
@ -65,7 +62,8 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
|
||||||
default=None, related_name="issues_assigned_to_me",
|
default=None, related_name="issues_assigned_to_me",
|
||||||
verbose_name=_("assigned to"))
|
verbose_name=_("assigned to"))
|
||||||
attachments = GenericRelation("attachments.Attachment")
|
attachments = GenericRelation("attachments.Attachment")
|
||||||
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("external reference"))
|
||||||
_importing = None
|
_importing = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import random
|
import random
|
||||||
import datetime
|
import datetime
|
||||||
from os import path
|
from os import path
|
||||||
|
from hashlib import sha1
|
||||||
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
@ -256,6 +257,13 @@ class Command(BaseCommand):
|
||||||
self.create_wiki_page(project, wiki_link.href)
|
self.create_wiki_page(project, wiki_link.href)
|
||||||
|
|
||||||
|
|
||||||
|
project.refresh_from_db()
|
||||||
|
|
||||||
|
# Set color for some tags:
|
||||||
|
for tag in project.tags_colors:
|
||||||
|
if self.sd.boolean():
|
||||||
|
tag[1] = self.generate_color(tag[0])
|
||||||
|
|
||||||
# Set a value to total_story_points to show the deadline in the backlog
|
# Set a value to total_story_points to show the deadline in the backlog
|
||||||
project_stats = get_stats_for_project(project)
|
project_stats = get_stats_for_project(project)
|
||||||
defined_points = project_stats["defined_points"]
|
defined_points = project_stats["defined_points"]
|
||||||
|
@ -264,7 +272,6 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
self.create_likes(project)
|
self.create_likes(project)
|
||||||
|
|
||||||
|
|
||||||
def create_attachment(self, obj, order):
|
def create_attachment(self, obj, order):
|
||||||
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
|
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
|
||||||
membership = self.sd.db_object_from_queryset(obj.project.memberships
|
membership = self.sd.db_object_from_queryset(obj.project.memberships
|
||||||
|
@ -551,3 +558,8 @@ class Command(BaseCommand):
|
||||||
obj.add_watcher(user)
|
obj.add_watcher(user)
|
||||||
else:
|
else:
|
||||||
obj.add_watcher(user, notify_level)
|
obj.add_watcher(user, notify_level)
|
||||||
|
|
||||||
|
def generate_color(self, tag):
|
||||||
|
color = sha1(tag.encode("utf-8")).hexdigest()[0:6]
|
||||||
|
return "#{}".format(color)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-07 06:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0045_merge'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Function: Reduce a multidimensional array only on its first level
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION public.reduce_dim(anyarray)
|
||||||
|
RETURNS SETOF anyarray
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
s $1%TYPE;
|
||||||
|
BEGIN
|
||||||
|
FOREACH s SLICE 1 IN ARRAY $1 LOOP
|
||||||
|
RETURN NEXT s;
|
||||||
|
END LOOP;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
LANGUAGE plpgsql IMMUTABLE;
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Function: aggregates multi dimensional arrays
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP AGGREGATE IF EXISTS array_agg_mult (anyarray);
|
||||||
|
CREATE AGGREGATE array_agg_mult (anyarray) (
|
||||||
|
SFUNC = array_cat
|
||||||
|
,STYPE = anyarray
|
||||||
|
,INITCOND = '{}'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Function: array_distinct
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION array_distinct(anyarray)
|
||||||
|
RETURNS anyarray AS $$
|
||||||
|
SELECT ARRAY(SELECT DISTINCT unnest($1))
|
||||||
|
$$ LANGUAGE sql;
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Rebuild the color tags so it's consisten in any project
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
WITH
|
||||||
|
tags_colors AS (
|
||||||
|
SELECT id project_id, reduce_dim(tags_colors) tags_colors
|
||||||
|
FROM projects_project
|
||||||
|
WHERE tags_colors != '{}'
|
||||||
|
),
|
||||||
|
tags AS (
|
||||||
|
SELECT unnest(tags) tag, NULL color, project_id FROM userstories_userstory
|
||||||
|
UNION
|
||||||
|
SELECT unnest(tags) tag, NULL color, project_id FROM tasks_task
|
||||||
|
UNION
|
||||||
|
SELECT unnest(tags) tag, NULL color, project_id FROM issues_issue
|
||||||
|
UNION
|
||||||
|
SELECT unnest(tags) tag, NULL color, id project_id FROM projects_project
|
||||||
|
),
|
||||||
|
rebuilt_tags_colors AS (
|
||||||
|
SELECT tags.project_id project_id,
|
||||||
|
array_agg_mult(ARRAY[[tags.tag, tags_colors.tags_colors[2]]]) tags_colors
|
||||||
|
FROM tags
|
||||||
|
LEFT JOIN tags_colors ON
|
||||||
|
tags_colors.project_id = tags.project_id AND
|
||||||
|
tags_colors[1] = tags.tag
|
||||||
|
GROUP BY tags.project_id
|
||||||
|
)
|
||||||
|
UPDATE projects_project
|
||||||
|
SET tags_colors = rebuilt_tags_colors.tags_colors
|
||||||
|
FROM rebuilt_tags_colors
|
||||||
|
WHERE rebuilt_tags_colors.project_id = projects_project.id;
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Trigger for auto updating projects_project.tags_colors
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION update_project_tags_colors()
|
||||||
|
RETURNS trigger AS $update_project_tags_colors$
|
||||||
|
DECLARE
|
||||||
|
tags text[];
|
||||||
|
project_tags_colors text[];
|
||||||
|
tag_color text[];
|
||||||
|
project_tags text[];
|
||||||
|
tag text;
|
||||||
|
project_id integer;
|
||||||
|
BEGIN
|
||||||
|
tags := NEW.tags::text[];
|
||||||
|
project_id := NEW.project_id::integer;
|
||||||
|
project_tags := '{}';
|
||||||
|
|
||||||
|
-- Read project tags_colors into project_tags_colors
|
||||||
|
SELECT projects_project.tags_colors INTO project_tags_colors
|
||||||
|
FROM projects_project
|
||||||
|
WHERE id = project_id;
|
||||||
|
|
||||||
|
-- Extract just the project tags to project_tags_colors
|
||||||
|
IF project_tags_colors != ARRAY[]::text[] THEN
|
||||||
|
FOREACH tag_color SLICE 1 in ARRAY project_tags_colors
|
||||||
|
LOOP
|
||||||
|
project_tags := array_append(project_tags, tag_color[1]);
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add to project_tags_colors the new tags
|
||||||
|
IF tags IS NOT NULL THEN
|
||||||
|
FOREACH tag in ARRAY tags
|
||||||
|
LOOP
|
||||||
|
IF tag != ALL(project_tags) THEN
|
||||||
|
project_tags_colors := array_cat(project_tags_colors,
|
||||||
|
ARRAY[ARRAY[tag, NULL]]);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Save the result in the tags_colors column
|
||||||
|
UPDATE projects_project
|
||||||
|
SET tags_colors = project_tags_colors
|
||||||
|
WHERE id = project_id;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END; $update_project_tags_colors$
|
||||||
|
LANGUAGE plpgsql;
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
|
||||||
|
# Execute trigger after user_story update
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_update ON userstories_userstory;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_userstory_update
|
||||||
|
AFTER UPDATE ON userstories_userstory
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Execute trigger after user_story insert
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_insert ON userstories_userstory;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_userstory_insert
|
||||||
|
AFTER INSERT ON userstories_userstory
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Execute trigger after task update
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_update ON tasks_task;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_task_update
|
||||||
|
AFTER UPDATE ON tasks_task
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Execute trigger after task insert
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_insert ON tasks_task;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_task_insert
|
||||||
|
AFTER INSERT ON tasks_task
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Execute trigger after issue update
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_update ON issues_issue;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_issue_update
|
||||||
|
AFTER UPDATE ON issues_issue
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
# Execute trigger after issue insert
|
||||||
|
migrations.RunSQL(
|
||||||
|
"""
|
||||||
|
DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_insert ON issues_issue;
|
||||||
|
CREATE TRIGGER update_project_tags_colors_on_issue_insert
|
||||||
|
AFTER INSERT ON issues_issue
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-14 12:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('projects', '0046_triggers_to_update_tags_colors'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='anon_permissions',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='public_permissions',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='tags',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='project',
|
||||||
|
name='tags_colors',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='tags colors'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,21 +20,22 @@ import itertools
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import signals, Q
|
from django.db.models import signals, Q
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
from djorm_pgarray.fields import TextArrayField
|
|
||||||
|
|
||||||
from taiga.base.tags import TaggedMixin
|
from taiga.projects.tagging.models import TaggedMixin
|
||||||
|
from taiga.projects.tagging.models import TagsColorsdMixin
|
||||||
from taiga.base.utils.dicts import dict_sum
|
from taiga.base.utils.dicts import dict_sum
|
||||||
from taiga.base.utils.files import get_file_path
|
from taiga.base.utils.files import get_file_path
|
||||||
from taiga.base.utils.sequence import arithmetic_progression
|
from taiga.base.utils.sequence import arithmetic_progression
|
||||||
|
@ -141,7 +142,7 @@ class ProjectDefaults(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Project(ProjectDefaults, TaggedMixin, models.Model):
|
class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
|
||||||
name = models.CharField(max_length=250, null=False, blank=False,
|
name = models.CharField(max_length=250, null=False, blank=False,
|
||||||
verbose_name=_("name"))
|
verbose_name=_("name"))
|
||||||
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
||||||
|
@ -186,16 +187,12 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
blank=True, default=None,
|
blank=True, default=None,
|
||||||
verbose_name=_("creation template"))
|
verbose_name=_("creation template"))
|
||||||
|
|
||||||
anon_permissions = TextArrayField(blank=True, null=True,
|
|
||||||
default=[],
|
|
||||||
verbose_name=_("anonymous permissions"),
|
|
||||||
choices=ANON_PERMISSIONS)
|
|
||||||
public_permissions = TextArrayField(blank=True, null=True,
|
|
||||||
default=[],
|
|
||||||
verbose_name=_("user permissions"),
|
|
||||||
choices=MEMBERS_PERMISSIONS)
|
|
||||||
is_private = models.BooleanField(default=True, null=False, blank=True,
|
is_private = models.BooleanField(default=True, null=False, blank=True,
|
||||||
verbose_name=_("is private"))
|
verbose_name=_("is private"))
|
||||||
|
anon_permissions = ArrayField(models.TextField(null=False, blank=False, choices=ANON_PERMISSIONS),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("anonymous permissions"))
|
||||||
|
public_permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("user permissions"))
|
||||||
|
|
||||||
is_featured = models.BooleanField(default=False, null=False, blank=True,
|
is_featured = models.BooleanField(default=False, null=False, blank=True,
|
||||||
verbose_name=_("is featured"))
|
verbose_name=_("is featured"))
|
||||||
|
@ -214,9 +211,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
||||||
null=True, blank=True, default=None,
|
null=True, blank=True, default=None,
|
||||||
db_index=True)
|
db_index=True)
|
||||||
|
|
||||||
tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
|
|
||||||
verbose_name=_("tags colors"))
|
|
||||||
|
|
||||||
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
|
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
|
||||||
verbose_name=_("project transfer token"))
|
verbose_name=_("project transfer token"))
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,10 @@ class ProjectPermission(TaigaResourcePermission):
|
||||||
transfer_start_perms = IsObjectOwner()
|
transfer_start_perms = IsObjectOwner()
|
||||||
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||||
|
create_tag_perms = IsProjectAdmin()
|
||||||
|
edit_tag_perms = IsProjectAdmin()
|
||||||
|
delete_tag_perms = IsProjectAdmin()
|
||||||
|
mix_tags_perms = IsProjectAdmin()
|
||||||
|
|
||||||
|
|
||||||
class ProjectFansPermission(TaigaResourcePermission):
|
class ProjectFansPermission(TaigaResourcePermission):
|
||||||
|
|
|
@ -16,35 +16,33 @@
|
||||||
# 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.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
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
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
|
@ -256,7 +254,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
|
||||||
i_am_member = serializers.SerializerMethodField("get_i_am_member")
|
i_am_member = serializers.SerializerMethodField("get_i_am_member")
|
||||||
|
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
tags_colors = TagsColorsField(required=False)
|
tags_colors = TagsColorsField(required=False, read_only=True)
|
||||||
|
|
||||||
notify_level = serializers.SerializerMethodField("get_notify_level")
|
notify_level = serializers.SerializerMethodField("get_notify_level")
|
||||||
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
||||||
|
|
|
@ -55,7 +55,5 @@ from .stats import get_stats_for_project_issues
|
||||||
from .stats import get_stats_for_project
|
from .stats import get_stats_for_project
|
||||||
from .stats import get_member_stats_for_project
|
from .stats import get_member_stats_for_project
|
||||||
|
|
||||||
from .tags_colors import update_project_tags_colors_handler
|
|
||||||
|
|
||||||
from .transfer import request_project_transfer, start_project_transfer
|
from .transfer import request_project_transfer, start_project_transfer
|
||||||
from .transfer import accept_project_transfer, reject_project_transfer
|
from .transfer import accept_project_transfer, reject_project_transfer
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from taiga.projects.services.filters import get_all_tags
|
|
||||||
from taiga.projects.models import Project
|
|
||||||
|
|
||||||
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):
|
|
||||||
if instance.tags is None:
|
|
||||||
instance.tags = []
|
|
||||||
|
|
||||||
if not isinstance(instance.project.tags_colors, list):
|
|
||||||
instance.project.tags_colors = []
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if not isinstance(instance, Project):
|
|
||||||
instance.project.save()
|
|
|
@ -19,7 +19,6 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags
|
|
||||||
from taiga.projects.notifications.services import create_notify_policy_if_not_exists
|
from taiga.projects.notifications.services import create_notify_policy_if_not_exists
|
||||||
from taiga.base.utils.db import get_typename_for_model_class
|
from taiga.base.utils.db import get_typename_for_model_class
|
||||||
|
|
||||||
|
@ -30,20 +29,7 @@ from easy_thumbnails.files import get_thumbnailer
|
||||||
# Signals over project items
|
# Signals over project items
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
## TAGS
|
## Membership
|
||||||
|
|
||||||
def tags_normalization(sender, instance, **kwargs):
|
|
||||||
if isinstance(instance.tags, (list, tuple)):
|
|
||||||
instance.tags = list(map(str.lower, instance.tags))
|
|
||||||
|
|
||||||
|
|
||||||
def update_project_tags_when_create_or_edit_taggable_item(sender, instance, **kwargs):
|
|
||||||
update_project_tags_colors_handler(instance)
|
|
||||||
|
|
||||||
|
|
||||||
def update_project_tags_when_delete_taggable_item(sender, instance, **kwargs):
|
|
||||||
remove_unused_tags(instance.project)
|
|
||||||
instance.project.save()
|
|
||||||
|
|
||||||
def membership_post_delete(sender, instance, using, **kwargs):
|
def membership_post_delete(sender, instance, using, **kwargs):
|
||||||
instance.project.update_role_points()
|
instance.project.update_role_points()
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base import response
|
||||||
|
from taiga.base.decorators import detail_route
|
||||||
|
from taiga.base.utils.collections import OrderedSet
|
||||||
|
|
||||||
|
from . import services
|
||||||
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class TagsColorsResourceMixin:
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def tags_colors(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "tags_colors", project)
|
||||||
|
|
||||||
|
return response.Ok(dict(project.tags_colors))
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def create_tag(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "create_tag", project)
|
||||||
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
|
serializer = serializers.CreateTagSerializer(data=request.DATA, project=project)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return response.BadRequest(serializer.errors)
|
||||||
|
|
||||||
|
data = serializer.data
|
||||||
|
services.create_tag(project, data.get("tag"), data.get("color"))
|
||||||
|
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def edit_tag(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "edit_tag", project)
|
||||||
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
|
serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return response.BadRequest(serializer.errors)
|
||||||
|
|
||||||
|
data = serializer.data
|
||||||
|
services.edit_tag(project,
|
||||||
|
data.get("from_tag"),
|
||||||
|
to_tag=data.get("to_tag", None),
|
||||||
|
color=data.get("color", None))
|
||||||
|
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def delete_tag(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "delete_tag", project)
|
||||||
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
|
serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return response.BadRequest(serializer.errors)
|
||||||
|
|
||||||
|
data = serializer.data
|
||||||
|
services.delete_tag(project, data.get("tag"))
|
||||||
|
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
@detail_route(methods=["POST"])
|
||||||
|
def mix_tags(self, request, pk=None):
|
||||||
|
project = self.get_object()
|
||||||
|
self.check_permissions(request, "mix_tags", project)
|
||||||
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
|
serializer = serializers.MixTagsSerializer(data=request.DATA, project=project)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return response.BadRequest(serializer.errors)
|
||||||
|
|
||||||
|
data = serializer.data
|
||||||
|
services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
|
||||||
|
|
||||||
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedResourceMixin:
|
||||||
|
def pre_save(self, obj):
|
||||||
|
if obj.tags:
|
||||||
|
self._pre_save_new_tags_in_project_tagss_colors(obj)
|
||||||
|
super().pre_save(obj)
|
||||||
|
|
||||||
|
def _pre_save_new_tags_in_project_tagss_colors(self, obj):
|
||||||
|
new_obj_tags = OrderedSet()
|
||||||
|
new_tags_colors = {}
|
||||||
|
|
||||||
|
for tag in obj.tags:
|
||||||
|
if isinstance(tag, (list, tuple)):
|
||||||
|
name, color = tag
|
||||||
|
|
||||||
|
if color and not services.tag_exist_for_project_elements(obj.project, name):
|
||||||
|
new_tags_colors[name] = color
|
||||||
|
|
||||||
|
new_obj_tags.add(name)
|
||||||
|
elif isinstance(tag, str):
|
||||||
|
new_obj_tags.add(tag.lower())
|
||||||
|
|
||||||
|
obj.tags = list(new_obj_tags)
|
||||||
|
|
||||||
|
if new_tags_colors:
|
||||||
|
services.create_tags(obj.project, new_tags_colors)
|
|
@ -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.core.exceptions import ValidationError
|
||||||
|
from django.forms import widgets
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
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,38 @@
|
||||||
|
# -*- 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>
|
||||||
|
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||||
|
# 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.db import models
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedMixin(models.Model):
|
||||||
|
tags = ArrayField(models.TextField(),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("tags"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class TagsColorsdMixin(models.Model):
|
||||||
|
tags_colors = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("tags colors"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
|
@ -0,0 +1,112 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
from . import services
|
||||||
|
from . import fields
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectTagSerializer(serializers.Serializer):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Don't pass the extra project arg
|
||||||
|
self.project = kwargs.pop("project")
|
||||||
|
|
||||||
|
# Instantiate the superclass normally
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTagSerializer(ProjectTagSerializer):
|
||||||
|
tag = serializers.CharField()
|
||||||
|
color = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
def validate_tag(self, attrs, source):
|
||||||
|
tag = attrs.get(source, None)
|
||||||
|
if services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag exists."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def validate_color(self, attrs, source):
|
||||||
|
color = attrs.get(source, None)
|
||||||
|
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
|
||||||
|
raise serializers.ValidationError(_("The color is not a valid HEX color."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class EditTagTagSerializer(ProjectTagSerializer):
|
||||||
|
from_tag = serializers.CharField()
|
||||||
|
to_tag = serializers.CharField(required=False)
|
||||||
|
color = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
def validate_from_tag(self, attrs, source):
|
||||||
|
tag = attrs.get(source, None)
|
||||||
|
if not services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag doesn't exist."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def validate_to_tag(self, attrs, source):
|
||||||
|
tag = attrs.get(source, None)
|
||||||
|
if services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag exists yet"))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def validate_color(self, attrs, source):
|
||||||
|
color = attrs.get(source, None)
|
||||||
|
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
|
||||||
|
raise serializers.ValidationError(_("The color is not a valid HEX color."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteTagSerializer(ProjectTagSerializer):
|
||||||
|
tag = serializers.CharField()
|
||||||
|
|
||||||
|
def validate_tag(self, attrs, source):
|
||||||
|
tag = attrs.get(source, None)
|
||||||
|
if not services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag doesn't exist."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class MixTagsSerializer(ProjectTagSerializer):
|
||||||
|
from_tags = fields.TagsField()
|
||||||
|
to_tag = serializers.CharField()
|
||||||
|
|
||||||
|
def validate_from_tags(self, attrs, source):
|
||||||
|
tags = attrs.get(source, None)
|
||||||
|
for tag in tags:
|
||||||
|
if not services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag doesn't exist."))
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def validate_to_tag(self, attrs, source):
|
||||||
|
tag = attrs.get(source, None)
|
||||||
|
if not services.tag_exist_for_project_elements(self.project, tag):
|
||||||
|
raise serializers.ValidationError(_("The tag doesn't exist."))
|
||||||
|
|
||||||
|
return attrs
|
|
@ -0,0 +1,122 @@
|
||||||
|
# -*- 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.db import connection
|
||||||
|
|
||||||
|
|
||||||
|
def tag_exist_for_project_elements(project, tag):
|
||||||
|
return tag in dict(project.tags_colors).keys()
|
||||||
|
|
||||||
|
|
||||||
|
def create_tags(project, new_tags_colors):
|
||||||
|
project.tags_colors += [[k, v] for k,v in new_tags_colors.items()]
|
||||||
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
|
def create_tag(project, tag, color):
|
||||||
|
project.tags_colors.append([tag, color])
|
||||||
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
|
def edit_tag(project, from_tag, to_tag=None, color=None):
|
||||||
|
tags_colors = dict(project.tags_colors)
|
||||||
|
|
||||||
|
if color is not None:
|
||||||
|
tags_colors = dict(project.tags_colors)
|
||||||
|
tags_colors[from_tag] = color
|
||||||
|
|
||||||
|
if to_tag is not None:
|
||||||
|
color = dict(project.tags_colors)[from_tag]
|
||||||
|
sql = """
|
||||||
|
UPDATE userstories_userstory
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE tasks_task
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE issues_issue
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
"""
|
||||||
|
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
tags_colors[to_tag] = tags_colors.pop(from_tag)
|
||||||
|
|
||||||
|
|
||||||
|
project.tags_colors = list(tags_colors.items())
|
||||||
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
|
def rename_tag(project, from_tag, to_tag, color=None):
|
||||||
|
color = color or dict(project.tags_colors)[from_tag]
|
||||||
|
sql = """
|
||||||
|
UPDATE userstories_userstory
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE tasks_task
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE issues_issue
|
||||||
|
SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
"""
|
||||||
|
sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
tags_colors = dict(project.tags_colors)
|
||||||
|
tags_colors.pop(from_tag)
|
||||||
|
tags_colors[to_tag] = color
|
||||||
|
project.tags_colors = list(tags_colors.items())
|
||||||
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tag(project, tag):
|
||||||
|
sql = """
|
||||||
|
UPDATE userstories_userstory
|
||||||
|
SET tags = array_remove(tags, '{tag}')
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE tasks_task
|
||||||
|
SET tags = array_remove(tags, '{tag}')
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
|
||||||
|
UPDATE issues_issue
|
||||||
|
SET tags = array_remove(tags, '{tag}')
|
||||||
|
WHERE project_id = {project_id};
|
||||||
|
"""
|
||||||
|
sql = sql.format(project_id=project.id, tag=tag)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(sql)
|
||||||
|
|
||||||
|
tags_colors = dict(project.tags_colors)
|
||||||
|
del tags_colors[tag]
|
||||||
|
project.tags_colors = list(tags_colors.items())
|
||||||
|
project.save(update_fields=["tags_colors"])
|
||||||
|
|
||||||
|
|
||||||
|
def mix_tags(project, from_tags, to_tag):
|
||||||
|
color = dict(project.tags_colors)[to_tag]
|
||||||
|
for from_tag in from_tags:
|
||||||
|
rename_tag(project, from_tag, to_tag, color)
|
|
@ -3,7 +3,6 @@
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
@ -17,14 +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.db import models
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from djorm_pgarray.fields import TextArrayField
|
def tags_normalization(sender, instance, **kwargs):
|
||||||
|
if isinstance(instance.tags, (list, tuple)):
|
||||||
|
instance.tags = list(map(str.lower, instance.tags))
|
||||||
class TaggedMixin(models.Model):
|
|
||||||
tags = TextArrayField(default=None, verbose_name=_("tags"))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
|
@ -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.api 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,21 +23,18 @@ 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")
|
||||||
signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
|
|
||||||
sender=apps.get_model("tasks", "Task"),
|
|
||||||
dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item_task")
|
|
||||||
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
|
|
||||||
sender=apps.get_model("tasks", "Task"),
|
|
||||||
dispatch_uid="update_project_tags_when_delete_tagglabe_item_task")
|
|
||||||
|
|
||||||
def connect_tasks_close_or_open_us_and_milestone_signals():
|
def connect_tasks_close_or_open_us_and_milestone_signals():
|
||||||
from . import signals as handlers
|
from . import signals as handlers
|
||||||
|
@ -67,19 +64,24 @@ def connect_all_tasks_signals():
|
||||||
|
|
||||||
|
|
||||||
def disconnect_tasks_signals():
|
def disconnect_tasks_signals():
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization")
|
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item")
|
dispatch_uid="set_finished_date_when_edit_task")
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_delete_tagglabe_item")
|
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
|
dispatch_uid="tags_normalization")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_tasks_close_or_open_us_and_milestone_signals():
|
def disconnect_tasks_close_or_open_us_and_milestone_signals():
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="cached_prev_task")
|
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task")
|
dispatch_uid="cached_prev_task")
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
|
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
|
dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task")
|
||||||
|
signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
|
dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_tasks_custom_attributes_signals():
|
def disconnect_tasks_custom_attributes_signals():
|
||||||
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task")
|
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||||
|
dispatch_uid="create_custom_attribute_value_when_create_task")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_all_tasks_signals():
|
def disconnect_all_tasks_signals():
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-14 12:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tasks', '0009_auto_20151104_1131'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='external_reference',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='task',
|
||||||
|
name='tags',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,16 +18,15 @@
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from djorm_pgarray.fields import TextArrayField
|
|
||||||
|
|
||||||
from taiga.projects.occ import OCCModelMixin
|
from taiga.projects.occ import OCCModelMixin
|
||||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||||
from taiga.projects.mixins.blocked import BlockedMixin
|
from taiga.projects.mixins.blocked import BlockedMixin
|
||||||
from taiga.base.tags import TaggedMixin
|
from taiga.projects.tagging.models import TaggedMixin
|
||||||
|
|
||||||
|
|
||||||
class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
|
class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
|
||||||
|
@ -66,7 +65,8 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
|
||||||
attachments = GenericRelation("attachments.Attachment")
|
attachments = GenericRelation("attachments.Attachment")
|
||||||
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
|
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
|
||||||
verbose_name=_("is iocaine"))
|
verbose_name=_("is iocaine"))
|
||||||
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("external reference"))
|
||||||
_importing = None
|
_importing = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -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,8 +36,9 @@ 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")
|
||||||
|
@ -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.api 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,6 +113,7 @@ 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", {})
|
||||||
|
@ -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,15 +60,9 @@ 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")
|
||||||
signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
|
|
||||||
sender=apps.get_model("userstories", "UserStory"),
|
|
||||||
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
|
|
||||||
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
|
|
||||||
sender=apps.get_model("userstories", "UserStory"),
|
|
||||||
dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
|
|
||||||
|
|
||||||
|
|
||||||
def connect_userstories_custom_attributes_signals():
|
def connect_userstories_custom_attributes_signals():
|
||||||
|
@ -83,18 +78,27 @@ def connect_all_userstories_signals():
|
||||||
|
|
||||||
|
|
||||||
def disconnect_userstories_signals():
|
def disconnect_userstories_signals():
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us")
|
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us")
|
dispatch_uid="cached_prev_us")
|
||||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us")
|
|
||||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
|
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us")
|
dispatch_uid="update_role_points_when_create_or_edit_us")
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story")
|
|
||||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
|
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
|
dispatch_uid="update_milestone_of_tasks_when_edit_us")
|
||||||
|
|
||||||
|
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
|
dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
|
||||||
|
signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
|
dispatch_uid="try_to_close_milestone_when_delete_us")
|
||||||
|
|
||||||
|
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
|
dispatch_uid="tags_normalization_user_story")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_userstories_custom_attributes_signals():
|
def disconnect_userstories_custom_attributes_signals():
|
||||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story")
|
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||||
|
dispatch_uid="create_custom_attribute_value_when_create_user_story")
|
||||||
|
|
||||||
|
|
||||||
def disconnect_all_userstories_signals():
|
def disconnect_all_userstories_signals():
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-14 12:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('userstories', '0011_userstory_tribe_gig'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstory',
|
||||||
|
name='external_reference',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstory',
|
||||||
|
name='tags',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -18,14 +18,14 @@
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from djorm_pgarray.fields import TextArrayField
|
|
||||||
from picklefield.fields import PickledObjectField
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
from taiga.base.tags import TaggedMixin
|
from taiga.projects.tagging.models import TaggedMixin
|
||||||
from taiga.projects.occ import OCCModelMixin
|
from taiga.projects.occ import OCCModelMixin
|
||||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||||
from taiga.projects.mixins.blocked import BlockedMixin
|
from taiga.projects.mixins.blocked import BlockedMixin
|
||||||
|
@ -103,7 +103,8 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="generated_user_stories",
|
related_name="generated_user_stories",
|
||||||
verbose_name=_("generated from issue"))
|
verbose_name=_("generated from issue"))
|
||||||
external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
|
external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2),
|
||||||
|
null=True, blank=True, default=[], verbose_name=_("external reference"))
|
||||||
|
|
||||||
tribe_gig = PickledObjectField(null=True, blank=True, default=None,
|
tribe_gig = PickledObjectField(null=True, blank=True, default=None,
|
||||||
verbose_name="taiga tribe gig")
|
verbose_name="taiga tribe gig")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.2 on 2016-06-14 12:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0020_auto_20160525_1229'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='role',
|
||||||
|
name='permissions',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -26,6 +26,7 @@ from django.apps.config import MODELS_MODULE_NAME
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import UserManager, AbstractBaseUser
|
from django.contrib.auth.models import UserManager, AbstractBaseUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -34,7 +35,6 @@ from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
from djorm_pgarray.fields import TextArrayField
|
|
||||||
|
|
||||||
from taiga.auth.tokens import get_token_for_user
|
from taiga.auth.tokens import get_token_for_user
|
||||||
from taiga.base.utils.slug import slugify_uniquely
|
from taiga.base.utils.slug import slugify_uniquely
|
||||||
|
@ -293,10 +293,8 @@ class Role(models.Model):
|
||||||
verbose_name=_("name"))
|
verbose_name=_("name"))
|
||||||
slug = models.SlugField(max_length=250, null=False, blank=True,
|
slug = models.SlugField(max_length=250, null=False, blank=True,
|
||||||
verbose_name=_("slug"))
|
verbose_name=_("slug"))
|
||||||
permissions = TextArrayField(blank=True, null=True,
|
permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS),
|
||||||
default=[],
|
null=True, blank=True, default=[], verbose_name=_("permissions"))
|
||||||
verbose_name=_("permissions"),
|
|
||||||
choices=MEMBERS_PERMISSIONS)
|
|
||||||
order = models.IntegerField(default=10, null=False, blank=False,
|
order = models.IntegerField(default=10, null=False, blank=False,
|
||||||
verbose_name=_("order"))
|
verbose_name=_("order"))
|
||||||
# null=True is for make work django 1.7 migrations. project
|
# null=True is for make work django 1.7 migrations. project
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -27,20 +27,24 @@ def data():
|
||||||
m.public_project = f.ProjectFactory(is_private=False,
|
m.public_project = f.ProjectFactory(is_private=False,
|
||||||
anon_permissions=['view_project'],
|
anon_permissions=['view_project'],
|
||||||
public_permissions=['view_project'],
|
public_permissions=['view_project'],
|
||||||
owner=m.project_owner)
|
owner=m.project_owner,
|
||||||
|
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||||
m.private_project1 = f.ProjectFactory(is_private=True,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=['view_project'],
|
anon_permissions=['view_project'],
|
||||||
public_permissions=['view_project'],
|
public_permissions=['view_project'],
|
||||||
owner=m.project_owner)
|
owner=m.project_owner,
|
||||||
|
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||||
m.private_project2 = f.ProjectFactory(is_private=True,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner)
|
owner=m.project_owner,
|
||||||
|
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||||
m.blocked_project = f.ProjectFactory(is_private=True,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
blocked_code=project_choices.BLOCKED_BY_STAFF,
|
||||||
|
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||||
|
|
||||||
m.public_membership = f.MembershipFactory(project=m.public_project,
|
m.public_membership = f.MembershipFactory(project=m.public_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
|
@ -1911,3 +1915,127 @@ def test_project_template_patch(client, data):
|
||||||
|
|
||||||
results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users)
|
results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users)
|
||||||
assert results == [401, 403, 200]
|
assert results == [401, 403, 200]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_tag(client, data):
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
post_data = json.dumps({
|
||||||
|
"tag": "testtest",
|
||||||
|
"color": "#123123"
|
||||||
|
})
|
||||||
|
|
||||||
|
url = reverse('projects-create-tag', kwargs={"pk": data.public_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-create-tag', kwargs={"pk": data.private_project1.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-create-tag', kwargs={"pk": data.private_project2.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-create-tag', kwargs={"pk": data.blocked_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 451]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_tag(client, data):
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
post_data = json.dumps({
|
||||||
|
"from_tag": "tag1",
|
||||||
|
"to_tag": "renamedtag1",
|
||||||
|
"color": "#123123"
|
||||||
|
})
|
||||||
|
|
||||||
|
url = reverse('projects-edit-tag', kwargs={"pk": data.public_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-edit-tag', kwargs={"pk": data.private_project1.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-edit-tag', kwargs={"pk": data.private_project2.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-edit-tag', kwargs={"pk": data.blocked_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 451]
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_tag(client, data):
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
post_data = json.dumps({
|
||||||
|
"tag": "tag2",
|
||||||
|
})
|
||||||
|
|
||||||
|
url = reverse('projects-delete-tag', kwargs={"pk": data.public_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-delete-tag', kwargs={"pk": data.private_project1.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-delete-tag', kwargs={"pk": data.private_project2.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-delete-tag', kwargs={"pk": data.blocked_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 451]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mix_tags(client, data):
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
post_data = json.dumps({
|
||||||
|
"from_tags": ["tag1"],
|
||||||
|
"to_tag": "tag3"
|
||||||
|
})
|
||||||
|
|
||||||
|
url = reverse('projects-mix-tags', kwargs={"pk": data.public_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-mix-tags', kwargs={"pk": data.private_project1.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-mix-tags', kwargs={"pk": data.private_project2.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 200]
|
||||||
|
|
||||||
|
url = reverse('projects-mix-tags', kwargs={"pk": data.blocked_project.pk})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [404, 404, 404, 403, 451]
|
||||||
|
|
|
@ -68,7 +68,7 @@ def test_valid_project_import_without_extra_data(client):
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201, response.data
|
||||||
must_empty_children = [
|
must_empty_children = [
|
||||||
"issues", "user_stories", "us_statuses", "wiki_pages", "priorities",
|
"issues", "user_stories", "us_statuses", "wiki_pages", "priorities",
|
||||||
"severities", "milestones", "points", "issue_types", "task_statuses",
|
"severities", "milestones", "points", "issue_types", "task_statuses",
|
||||||
|
|
|
@ -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"
|
|
@ -10,6 +10,9 @@ from taiga.projects.services import stats as stats_services
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
from taiga.permissions.choices import ANON_PERMISSIONS
|
from taiga.permissions.choices import ANON_PERMISSIONS
|
||||||
from taiga.projects.models import Project
|
from taiga.projects.models import Project
|
||||||
|
from taiga.projects.userstories.models import UserStory
|
||||||
|
from taiga.projects.tasks.models import Task
|
||||||
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.choices import BLOCKED_BY_DELETING
|
from taiga.projects.choices import BLOCKED_BY_DELETING
|
||||||
|
|
||||||
from .. import factories as f
|
from .. import factories as f
|
||||||
|
@ -1852,3 +1855,209 @@ def test_delete_project_with_celery_disabled(client, settings):
|
||||||
response = client.json.delete(url)
|
response = client.json.delete(url)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert Project.objects.filter(id=project.id).count() == 0
|
assert Project.objects.filter(id=project.id).count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_tag(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-create-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"tag": "newtag",
|
||||||
|
"color": "#123123"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors == [["newtag", "#123123"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_tag_without_color(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-create-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"tag": "newtag",
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors[0][0] == "newtag"
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_tag_only_name(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
|
||||||
|
task = f.TaskFactory.create(project=project, tags=["tag"])
|
||||||
|
issue = f.IssueFactory.create(project=project, tags=["tag"])
|
||||||
|
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-edit-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"from_tag": "tag",
|
||||||
|
"to_tag": "renamed_tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
print(response.data)
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors == [["renamed_tag", "#123123"]]
|
||||||
|
user_story = UserStory.objects.get(id=user_story.pk)
|
||||||
|
assert user_story.tags == ["renamed_tag"]
|
||||||
|
task = Task.objects.get(id=task.pk)
|
||||||
|
assert task.tags == ["renamed_tag"]
|
||||||
|
issue = Issue.objects.get(id=issue.pk)
|
||||||
|
assert issue.tags == ["renamed_tag"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_tag_only_color(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
|
||||||
|
task = f.TaskFactory.create(project=project, tags=["tag"])
|
||||||
|
issue = f.IssueFactory.create(project=project, tags=["tag"])
|
||||||
|
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-edit-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"from_tag": "tag",
|
||||||
|
"color": "#AAABBB"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors == [["tag", "#AAABBB"]]
|
||||||
|
user_story = UserStory.objects.get(id=user_story.pk)
|
||||||
|
assert user_story.tags == ["tag"]
|
||||||
|
task = Task.objects.get(id=task.pk)
|
||||||
|
assert task.tags == ["tag"]
|
||||||
|
issue = Issue.objects.get(id=issue.pk)
|
||||||
|
assert issue.tags == ["tag"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edit_tag(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
|
||||||
|
task = f.TaskFactory.create(project=project, tags=["tag"])
|
||||||
|
issue = f.IssueFactory.create(project=project, tags=["tag"])
|
||||||
|
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-edit-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"from_tag": "tag",
|
||||||
|
"to_tag": "renamed_tag",
|
||||||
|
"color": "#AAABBB"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors == [["renamed_tag", "#AAABBB"]]
|
||||||
|
user_story = UserStory.objects.get(id=user_story.pk)
|
||||||
|
assert user_story.tags == ["renamed_tag"]
|
||||||
|
task = Task.objects.get(id=task.pk)
|
||||||
|
assert task.tags == ["renamed_tag"]
|
||||||
|
issue = Issue.objects.get(id=issue.pk)
|
||||||
|
assert issue.tags == ["renamed_tag"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_tag(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project, tags=["tag"])
|
||||||
|
task = f.TaskFactory.create(project=project, tags=["tag"])
|
||||||
|
issue = f.IssueFactory.create(project=project, tags=["tag"])
|
||||||
|
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-delete-tag", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"tag": "tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert project.tags_colors == []
|
||||||
|
user_story = UserStory.objects.get(id=user_story.pk)
|
||||||
|
assert user_story.tags == []
|
||||||
|
task = Task.objects.get(id=task.pk)
|
||||||
|
assert task.tags == []
|
||||||
|
issue = Issue.objects.get(id=issue.pk)
|
||||||
|
assert issue.tags == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_mix_tags(client, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, tags_colors=[("tag1", "#123123"), ("tag2", "#123123"), ("tag3", "#123123")])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project, tags=["tag1", "tag3"])
|
||||||
|
task = f.TaskFactory.create(project=project, tags=["tag2", "tag3"])
|
||||||
|
issue = f.IssueFactory.create(project=project, tags=["tag1", "tag2", "tag3"])
|
||||||
|
|
||||||
|
role = f.RoleFactory.create(project=project, permissions=["view_project"])
|
||||||
|
membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True)
|
||||||
|
url = reverse("projects-mix-tags", args=(project.id,))
|
||||||
|
client.login(user)
|
||||||
|
data = {
|
||||||
|
"from_tags": ["tag1", "tag2"],
|
||||||
|
"to_tag": "tag2"
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(user)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
assert response.status_code == 200
|
||||||
|
project = Project.objects.get(id=project.pk)
|
||||||
|
assert set(["tag2", "tag3"]) == set(dict(project.tags_colors).keys())
|
||||||
|
user_story = UserStory.objects.get(id=user_story.pk)
|
||||||
|
assert set(user_story.tags) == set(["tag2", "tag3"])
|
||||||
|
task = Task.objects.get(id=task.pk)
|
||||||
|
assert set(task.tags) == set(["tag2", "tag3"])
|
||||||
|
issue = Issue.objects.get(id=issue.pk)
|
||||||
|
assert set(issue.tags) == set(["tag2", "tag3"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_color_tags_project_fired_on_element_create():
|
||||||
|
user_story = f.UserStoryFactory.create(tags=["tag"])
|
||||||
|
project = Project.objects.get(id=user_story.project.id)
|
||||||
|
assert project.tags_colors == [["tag", None]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_color_tags_project_fired_on_element_update():
|
||||||
|
user_story = f.UserStoryFactory.create()
|
||||||
|
user_story.tags = ["tag"]
|
||||||
|
user_story.save()
|
||||||
|
project = Project.objects.get(id=user_story.project.id)
|
||||||
|
assert project.tags_colors == [["tag", None]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_color_tags_project_fired_on_element_update_respecting_color():
|
||||||
|
project = f.ProjectFactory.create(tags_colors=[["tag", "#123123"]])
|
||||||
|
user_story = f.UserStoryFactory.create(project=project)
|
||||||
|
user_story.tags = ["tag"]
|
||||||
|
user_story.save()
|
||||||
|
project = Project.objects.get(id=user_story.project.id)
|
||||||
|
assert project.tags_colors == [["tag", "#123123"]]
|
||||||
|
|
|
@ -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"
|
|
@ -481,7 +481,7 @@ def test_get_watched_list_valid_info_for_project():
|
||||||
fav_user = f.UserFactory()
|
fav_user = f.UserFactory()
|
||||||
viewer_user = f.UserFactory()
|
viewer_user = f.UserFactory()
|
||||||
|
|
||||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||||
project.add_watcher(fav_user)
|
project.add_watcher(fav_user)
|
||||||
|
|
||||||
|
@ -499,11 +499,6 @@ def test_get_watched_list_valid_info_for_project():
|
||||||
assert project_watch_info["assigned_to"] == None
|
assert project_watch_info["assigned_to"] == None
|
||||||
assert project_watch_info["status"] == None
|
assert project_watch_info["status"] == None
|
||||||
assert project_watch_info["status_color"] == None
|
assert project_watch_info["status_color"] == None
|
||||||
|
|
||||||
tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]}
|
|
||||||
assert "test" in tags_colors
|
|
||||||
assert "tag" in tags_colors
|
|
||||||
|
|
||||||
assert project_watch_info["is_private"] == project.is_private
|
assert project_watch_info["is_private"] == project.is_private
|
||||||
assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
||||||
assert project_watch_info["is_fan"] == False
|
assert project_watch_info["is_fan"] == False
|
||||||
|
@ -540,7 +535,7 @@ def test_get_liked_list_valid_info():
|
||||||
fan_user = f.UserFactory()
|
fan_user = f.UserFactory()
|
||||||
viewer_user = f.UserFactory()
|
viewer_user = f.UserFactory()
|
||||||
|
|
||||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||||
content_type = ContentType.objects.get_for_model(project)
|
content_type = ContentType.objects.get_for_model(project)
|
||||||
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||||
project.refresh_totals()
|
project.refresh_totals()
|
||||||
|
@ -558,11 +553,6 @@ def test_get_liked_list_valid_info():
|
||||||
assert project_like_info["assigned_to"] == None
|
assert project_like_info["assigned_to"] == None
|
||||||
assert project_like_info["status"] == None
|
assert project_like_info["status"] == None
|
||||||
assert project_like_info["status_color"] == None
|
assert project_like_info["status_color"] == None
|
||||||
|
|
||||||
tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]}
|
|
||||||
assert "test" in tags_colors
|
|
||||||
assert "tag" in tags_colors
|
|
||||||
|
|
||||||
assert project_like_info["is_private"] == project.is_private
|
assert project_like_info["is_private"] == project.is_private
|
||||||
assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -1,26 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
|
||||||
# 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.db import models
|
|
||||||
from taiga.base import tags
|
|
||||||
|
|
||||||
|
|
||||||
class TaggedModel(tags.TaggedMixin, models.Model):
|
|
||||||
class Meta:
|
|
||||||
app_label = "tests"
|
|
Loading…
Reference in New Issue