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)
|
||||
|
||||
### Features
|
||||
- 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.
|
||||
- Include created, modified and finished dates for tasks in CSV reports
|
||||
- Include created, modified and finished dates for tasks in CSV reports.
|
||||
- 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
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
|
|
@ -10,7 +10,7 @@ six==1.10.0
|
|||
amqp==1.4.9
|
||||
djmail==0.12.0.post1
|
||||
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
|
||||
jinja2==2.8
|
||||
pygments==2.0.2
|
||||
|
@ -28,7 +28,7 @@ raven==5.10.2
|
|||
bleach==1.4.2
|
||||
django-ipware==1.1.3
|
||||
premailer==2.9.7
|
||||
cssutils==1.0.1 # Compatible with python 3.5
|
||||
cssutils==1.0.1 # Compatible with python 3.5
|
||||
lxml==3.5.0
|
||||
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
|
||||
pyjwkest==1.1.5
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from taiga.base.api import serializers
|
||||
|
||||
|
||||
|
@ -99,35 +98,6 @@ class PickledObjectField(serializers.WritableField):
|
|||
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):
|
||||
def to_native(self, 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.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import signals, Prefetch
|
||||
from django.db.models import Value as V
|
||||
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 import timezone
|
||||
from django.http import Http404
|
||||
|
||||
from taiga.base import filters
|
||||
from taiga.base import response
|
||||
from taiga.base import exceptions as exc
|
||||
from taiga.base.decorators import list_route
|
||||
from taiga.base.decorators import detail_route
|
||||
from taiga.base import response
|
||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
|
||||
from taiga.base.api.permissions import AllowAnyPermission
|
||||
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.permissions import services as permissions_services
|
||||
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.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
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.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.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 . import filters as project_filters
|
||||
|
@ -66,9 +66,9 @@ from . import services
|
|||
######################################################
|
||||
## Project
|
||||
######################################################
|
||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
||||
BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
|
||||
|
||||
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
|
||||
TagsColorsResourceMixin, ModelCrudViewSet):
|
||||
queryset = models.Project.objects.all()
|
||||
serializer_class = serializers.ProjectDetailSerializer
|
||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
||||
|
@ -327,12 +327,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
|||
self.check_permissions(request, "issues_stats", 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"])
|
||||
def transfer_validate_token(self, request, pk=None):
|
||||
project = self.get_object()
|
||||
|
@ -405,6 +399,10 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
|
|||
services.reject_project_transfer(project, request.user, token, reason)
|
||||
return response.Ok()
|
||||
|
||||
def _raise_if_blocked(self, project):
|
||||
if self.is_blocked(project):
|
||||
raise exc.Blocked(_("Blocked element"))
|
||||
|
||||
def _set_base_permissions(self, obj):
|
||||
update_permissions = False
|
||||
if not obj.id:
|
||||
|
|
|
@ -25,18 +25,16 @@ from django.db.models import signals
|
|||
|
||||
def connect_projects_signals():
|
||||
from . import signals as handlers
|
||||
from .tagging import signals as tagging_handlers
|
||||
# On project object is created apply template.
|
||||
signals.post_save.connect(handlers.project_post_save,
|
||||
sender=apps.get_model("projects", "Project"),
|
||||
dispatch_uid='project_post_save')
|
||||
|
||||
# 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"),
|
||||
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():
|
||||
|
@ -44,8 +42,6 @@ def disconnect_projects_signals():
|
|||
dispatch_uid='project_post_save')
|
||||
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
|
||||
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
|
||||
|
|
|
@ -27,11 +27,11 @@ from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
|||
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||
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.occ import OCCResourceMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
|
||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||
|
||||
from . import models
|
||||
|
@ -41,7 +41,7 @@ from . import serializers
|
|||
|
||||
|
||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
permission_classes = (permissions.IssuePermission, )
|
||||
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)
|
||||
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)
|
||||
tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
querysets = {
|
||||
|
|
|
@ -23,6 +23,7 @@ from django.db.models import signals
|
|||
|
||||
def connect_issues_signals():
|
||||
from taiga.projects import signals as generic_handlers
|
||||
from taiga.projects.tagging import signals as tagging_handlers
|
||||
from . import signals as handlers
|
||||
|
||||
# Finished date
|
||||
|
@ -31,15 +32,9 @@ def connect_issues_signals():
|
|||
dispatch_uid="set_finished_date_when_edit_issue")
|
||||
|
||||
# Tags
|
||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
||||
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||
sender=apps.get_model("issues", "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():
|
||||
|
@ -56,14 +51,15 @@ def connect_all_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"), dispatch_uid="tags_normalization_issue")
|
||||
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue")
|
||||
signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_delete_taggable_item_issue")
|
||||
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"),
|
||||
dispatch_uid="tags_normalization_issue")
|
||||
|
||||
|
||||
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():
|
||||
|
|
|
@ -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.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from djorm_pgarray.fields import TextArrayField
|
||||
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.mixins.blocked import BlockedMixin
|
||||
from taiga.base.tags import TaggedMixin
|
||||
|
||||
from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags
|
||||
from taiga.projects.tagging.models import TaggedMixin
|
||||
|
||||
|
||||
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",
|
||||
verbose_name=_("assigned to"))
|
||||
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
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.base.fields import TagsField
|
||||
from taiga.base.fields import PgArrayField
|
||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
from taiga.projects.validators import ProjectExistsValidator
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
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.users.serializers import UserBasicInfoSerializer
|
||||
|
@ -33,8 +33,9 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
|||
from . import models
|
||||
|
||||
|
||||
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||
tags = TagsField(required=False)
|
||||
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||
serializers.ModelSerializer):
|
||||
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||
external_reference = PgArrayField(required=False)
|
||||
is_closed = serializers.Field(source="is_closed")
|
||||
comment = serializers.SerializerMethodField("get_comment")
|
||||
|
@ -71,7 +72,7 @@ class IssueListSerializer(IssueSerializer):
|
|||
class Meta:
|
||||
model = models.Issue
|
||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||
exclude=("description", "description_html")
|
||||
exclude = ("description", "description_html")
|
||||
|
||||
|
||||
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import random
|
||||
import datetime
|
||||
from os import path
|
||||
from hashlib import sha1
|
||||
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
@ -256,6 +257,13 @@ class Command(BaseCommand):
|
|||
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
|
||||
project_stats = get_stats_for_project(project)
|
||||
defined_points = project_stats["defined_points"]
|
||||
|
@ -264,7 +272,6 @@ class Command(BaseCommand):
|
|||
|
||||
self.create_likes(project)
|
||||
|
||||
|
||||
def create_attachment(self, obj, order):
|
||||
attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA)
|
||||
membership = self.sd.db_object_from_queryset(obj.project.memberships
|
||||
|
@ -551,3 +558,8 @@ class Command(BaseCommand):
|
|||
obj.add_watcher(user)
|
||||
else:
|
||||
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
|
||||
|
||||
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.db import models
|
||||
from django.db.models import signals, Q
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
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.files import get_file_path
|
||||
from taiga.base.utils.sequence import arithmetic_progression
|
||||
|
@ -141,7 +142,7 @@ class ProjectDefaults(models.Model):
|
|||
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,
|
||||
verbose_name=_("name"))
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
verbose_name=_("is featured"))
|
||||
|
@ -214,9 +211,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
|
|||
null=True, blank=True, default=None,
|
||||
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,
|
||||
verbose_name=_("project transfer token"))
|
||||
|
||||
|
|
|
@ -78,6 +78,10 @@ class ProjectPermission(TaigaResourcePermission):
|
|||
transfer_start_perms = IsObjectOwner()
|
||||
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
|
||||
create_tag_perms = IsProjectAdmin()
|
||||
edit_tag_perms = IsProjectAdmin()
|
||||
delete_tag_perms = IsProjectAdmin()
|
||||
mix_tags_perms = IsProjectAdmin()
|
||||
|
||||
|
||||
class ProjectFansPermission(TaigaResourcePermission):
|
||||
|
|
|
@ -16,35 +16,33 @@
|
|||
# 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 django.db.models import Q
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.base.fields import JsonField
|
||||
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.serializers import UserSerializer
|
||||
from taiga.users.serializers import UserBasicInfoSerializer
|
||||
from taiga.users.serializers import ProjectRoleSerializer
|
||||
from taiga.users.validators import RoleExistsValidator
|
||||
|
||||
from taiga.permissions.services import get_user_project_permissions
|
||||
from taiga.permissions.services import is_project_admin, is_project_owner
|
||||
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
||||
|
||||
from . import models
|
||||
from . import services
|
||||
from .notifications.mixins import WatchedResourceModelSerializer
|
||||
from .validators import ProjectExistsValidator
|
||||
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
|
||||
from .custom_attributes.serializers import TaskCustomAttributeSerializer
|
||||
from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
||||
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")
|
||||
|
||||
tags = TagsField(default=[], required=False)
|
||||
tags_colors = TagsColorsField(required=False)
|
||||
tags_colors = TagsColorsField(required=False, read_only=True)
|
||||
|
||||
notify_level = serializers.SerializerMethodField("get_notify_level")
|
||||
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
||||
|
|
|
@ -55,7 +55,5 @@ from .stats import get_stats_for_project_issues
|
|||
from .stats import get_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 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.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.base.utils.db import get_typename_for_model_class
|
||||
|
||||
|
@ -30,20 +29,7 @@ from easy_thumbnails.files import get_thumbnailer
|
|||
# Signals over project items
|
||||
####################################
|
||||
|
||||
## TAGS
|
||||
|
||||
def tags_normalization(sender, instance, **kwargs):
|
||||
if isinstance(instance.tags, (list, tuple)):
|
||||
instance.tags = list(map(str.lower, instance.tags))
|
||||
|
||||
|
||||
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()
|
||||
## Membership
|
||||
|
||||
def membership_post_delete(sender, instance, using, **kwargs):
|
||||
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 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
|
||||
|
@ -17,14 +16,7 @@
|
|||
# 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.utils.translation import ugettext_lazy as _
|
||||
|
||||
from djorm_pgarray.fields import TextArrayField
|
||||
|
||||
|
||||
class TaggedMixin(models.Model):
|
||||
tags = TextArrayField(default=None, verbose_name=_("tags"))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
def tags_normalization(sender, instance, **kwargs):
|
||||
if isinstance(instance.tags, (list, tuple)):
|
||||
instance.tags = list(map(str.lower, instance.tags))
|
|
@ -16,6 +16,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# 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 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.api import ModelCrudViewSet, ModelListViewSet
|
||||
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.models import Project, TaskStatus
|
||||
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 . import models
|
||||
from . import permissions
|
||||
from . import serializers
|
||||
|
@ -40,13 +39,18 @@ from . import services
|
|||
|
||||
|
||||
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
queryset = models.Task.objects.all()
|
||||
permission_classes = (permissions.TaskPermission,)
|
||||
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
||||
retrieve_exclude_filters = (filters.WatchersFilter,)
|
||||
filter_fields = ["user_story", "milestone", "project", "assigned_to",
|
||||
"status__is_closed"]
|
||||
filter_fields = [
|
||||
"user_story",
|
||||
"milestone",
|
||||
"project",
|
||||
"assigned_to",
|
||||
"status__is_closed"
|
||||
]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["retrieve", "by_ref"]:
|
||||
|
|
|
@ -23,21 +23,18 @@ from django.db.models import signals
|
|||
|
||||
def connect_tasks_signals():
|
||||
from taiga.projects import signals as generic_handlers
|
||||
from taiga.projects.tagging import signals as tagging_handlers
|
||||
from . import signals as handlers
|
||||
|
||||
# Finished date
|
||||
signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
|
||||
sender=apps.get_model("tasks", "Task"),
|
||||
dispatch_uid="set_finished_date_when_edit_task")
|
||||
# Tags
|
||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
||||
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||
sender=apps.get_model("tasks", "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():
|
||||
from . import signals as handlers
|
||||
|
@ -67,19 +64,24 @@ def connect_all_tasks_signals():
|
|||
|
||||
|
||||
def disconnect_tasks_signals():
|
||||
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization")
|
||||
signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item")
|
||||
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="set_finished_date_when_edit_task")
|
||||
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||
dispatch_uid="tags_normalization")
|
||||
|
||||
|
||||
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.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")
|
||||
signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
|
||||
dispatch_uid="cached_prev_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():
|
||||
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():
|
||||
|
|
|
@ -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.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from djorm_pgarray.fields import TextArrayField
|
||||
|
||||
from taiga.projects.occ import OCCModelMixin
|
||||
from taiga.projects.notifications.mixins import WatchedModelMixin
|
||||
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):
|
||||
|
@ -66,7 +65,8 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
|
|||
attachments = GenericRelation("attachments.Attachment")
|
||||
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
|
||||
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
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -17,19 +17,18 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from taiga.base.api import serializers
|
||||
|
||||
from taiga.base.fields import TagsField
|
||||
from taiga.base.fields import PgArrayField
|
||||
|
||||
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.tasks.validators import TaskExistsValidator
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
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.users.serializers import UserBasicInfoSerializer
|
||||
|
@ -37,14 +36,15 @@ from taiga.users.serializers import UserBasicInfoSerializer
|
|||
from . import models
|
||||
|
||||
|
||||
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||
tags = TagsField(required=False, default=[])
|
||||
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||
serializers.ModelSerializer):
|
||||
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||
external_reference = PgArrayField(required=False)
|
||||
comment = serializers.SerializerMethodField("get_comment")
|
||||
milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
|
||||
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
||||
description_html = serializers.SerializerMethodField("get_description_html")
|
||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
||||
status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
|
||||
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
||||
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
|
||||
|
@ -76,7 +76,7 @@ class TaskListSerializer(TaskSerializer):
|
|||
class Meta:
|
||||
model = models.Task
|
||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||
exclude=("description", "description_html")
|
||||
exclude = ("description", "description_html")
|
||||
|
||||
|
||||
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||
|
@ -101,6 +101,7 @@ class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
|
|||
us_id = serializers.IntegerField(required=False)
|
||||
bulk_tasks = serializers.CharField()
|
||||
|
||||
|
||||
## Order bulk serializers
|
||||
|
||||
class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import HttpResponse
|
||||
|
||||
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.utils import get_object_or_404
|
||||
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
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.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 . import models
|
||||
|
@ -46,7 +46,7 @@ from . import services
|
|||
|
||||
|
||||
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||
queryset = models.UserStory.objects.all()
|
||||
permission_classes = (permissions.UserStoryPermission,)
|
||||
filter_backends = (filters.CanViewUsFilterBackend,
|
||||
|
@ -113,8 +113,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
|||
def pre_save(self, obj):
|
||||
# This is very ugly hack, but having
|
||||
# restframework is the only way to do it.
|
||||
#
|
||||
# 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", {})
|
||||
self._role_points = related_data.pop("role_points", None)
|
||||
|
||||
|
@ -124,7 +125,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
|||
super().pre_save(obj)
|
||||
|
||||
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:
|
||||
Points = apps.get_model("projects", "Points")
|
||||
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__computable=True)
|
||||
except (ValueError, RolePoints.DoesNotExist):
|
||||
raise exc.BadRequest({"points": _("Invalid role id '{role_id}'").format(
|
||||
role_id=role_id)})
|
||||
raise exc.BadRequest({
|
||||
"points": _("Invalid role id '{role_id}'").format(role_id=role_id)
|
||||
})
|
||||
|
||||
try:
|
||||
role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id)
|
||||
except (ValueError, Points.DoesNotExist):
|
||||
raise exc.BadRequest({"points": _("Invalid points id '{points_id}'").format(
|
||||
points_id=points_id)})
|
||||
raise exc.BadRequest({
|
||||
"points": _("Invalid points id '{points_id}'").format(points_id=points_id)
|
||||
})
|
||||
|
||||
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)
|
||||
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)
|
||||
tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
querysets = {
|
||||
|
|
|
@ -23,6 +23,7 @@ from django.db.models import signals
|
|||
|
||||
def connect_userstories_signals():
|
||||
from taiga.projects import signals as generic_handlers
|
||||
from taiga.projects.tagging import signals as tagging_handlers
|
||||
from . import signals as handlers
|
||||
|
||||
# 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")
|
||||
|
||||
# Tags
|
||||
signals.pre_save.connect(generic_handlers.tags_normalization,
|
||||
signals.pre_save.connect(tagging_handlers.tags_normalization,
|
||||
sender=apps.get_model("userstories", "UserStory"),
|
||||
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():
|
||||
|
@ -83,18 +78,27 @@ def connect_all_userstories_signals():
|
|||
|
||||
|
||||
def disconnect_userstories_signals():
|
||||
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us")
|
||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_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_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")
|
||||
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_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
|
||||
signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||
dispatch_uid="cached_prev_us")
|
||||
|
||||
signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
|
||||
dispatch_uid="update_role_points_when_create_or_edit_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_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():
|
||||
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():
|
||||
|
|
|
@ -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.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from djorm_pgarray.fields import TextArrayField
|
||||
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.notifications.mixins import WatchedModelMixin
|
||||
from taiga.projects.mixins.blocked import BlockedMixin
|
||||
|
@ -103,7 +103,8 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
|
|||
on_delete=models.SET_NULL,
|
||||
related_name="generated_user_stories",
|
||||
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,
|
||||
verbose_name="taiga tribe gig")
|
||||
|
|
|
@ -16,23 +16,22 @@
|
|||
# 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.apps import apps
|
||||
from taiga.base.api import serializers
|
||||
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 PgArrayField
|
||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||
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.userstories.validators import UserStoryExistsValidator
|
||||
from taiga.projects.models import Project
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||
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.users.serializers import UserBasicInfoSerializer
|
||||
|
@ -50,9 +49,9 @@ class RolePointsField(serializers.WritableField):
|
|||
return json.loads(obj)
|
||||
|
||||
|
||||
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||
serializers.ModelSerializer):
|
||||
tags = TagsField(default=[], required=False)
|
||||
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
||||
EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||
external_reference = PgArrayField(required=False)
|
||||
points = RolePointsField(source="role_points", required=False)
|
||||
total_points = serializers.SerializerMethodField("get_total_points")
|
||||
|
@ -112,7 +111,7 @@ class UserStoryListSerializer(UserStorySerializer):
|
|||
model = models.UserStory
|
||||
depth = 0
|
||||
read_only_fields = ('created_date', 'modified_date')
|
||||
exclude=("description", "description_html")
|
||||
exclude = ("description", "description_html")
|
||||
|
||||
|
||||
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
||||
|
@ -142,7 +141,8 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial
|
|||
order = serializers.IntegerField()
|
||||
|
||||
|
||||
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
||||
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
|
||||
serializers.Serializer):
|
||||
project_id = serializers.IntegerField()
|
||||
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.contrib.auth.models import UserManager, AbstractBaseUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core import validators
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db import models
|
||||
|
@ -34,7 +35,6 @@ from django.utils import timezone
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django_pgjson.fields import JsonField
|
||||
from djorm_pgarray.fields import TextArrayField
|
||||
|
||||
from taiga.auth.tokens import get_token_for_user
|
||||
from taiga.base.utils.slug import slugify_uniquely
|
||||
|
@ -53,8 +53,8 @@ def get_user_model_safe():
|
|||
registry not being ready yet.
|
||||
Raises LookupError if model isn't found.
|
||||
|
||||
Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340
|
||||
Ongoing Django issue: https://code.djangoproject.com/ticket/22872
|
||||
Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340
|
||||
Ongoing Django issue: https://code.djangoproject.com/ticket/22872
|
||||
"""
|
||||
user_app, user_model = settings.AUTH_USER_MODEL.split('.')
|
||||
|
||||
|
@ -293,10 +293,8 @@ class Role(models.Model):
|
|||
verbose_name=_("name"))
|
||||
slug = models.SlugField(max_length=250, null=False, blank=True,
|
||||
verbose_name=_("slug"))
|
||||
permissions = TextArrayField(blank=True, null=True,
|
||||
default=[],
|
||||
verbose_name=_("permissions"),
|
||||
choices=MEMBERS_PERMISSIONS)
|
||||
permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS),
|
||||
null=True, blank=True, default=[], verbose_name=_("permissions"))
|
||||
order = models.IntegerField(default=10, null=False, blank=False,
|
||||
verbose_name=_("order"))
|
||||
# null=True is for make work django 1.7 migrations. project
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
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
|
||||
|
||||
|
@ -29,6 +29,7 @@ from taiga.projects.milestones import models as milestone_models
|
|||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.services import get_logo_big_thumbnail_url
|
||||
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.wiki import models as wiki_models
|
||||
|
||||
|
|
|
@ -27,20 +27,24 @@ def data():
|
|||
m.public_project = f.ProjectFactory(is_private=False,
|
||||
anon_permissions=['view_project'],
|
||||
public_permissions=['view_project'],
|
||||
owner=m.project_owner)
|
||||
owner=m.project_owner,
|
||||
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||
anon_permissions=['view_project'],
|
||||
public_permissions=['view_project'],
|
||||
owner=m.project_owner)
|
||||
owner=m.project_owner,
|
||||
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||
anon_permissions=[],
|
||||
public_permissions=[],
|
||||
owner=m.project_owner)
|
||||
owner=m.project_owner,
|
||||
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||
anon_permissions=[],
|
||||
public_permissions=[],
|
||||
owner=m.project_owner,
|
||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
||||
blocked_code=project_choices.BLOCKED_BY_STAFF,
|
||||
tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")])
|
||||
|
||||
m.public_membership = f.MembershipFactory(project=m.public_project,
|
||||
user=m.project_member_with_perms,
|
||||
|
@ -1911,3 +1915,127 @@ def test_project_template_patch(client, data):
|
|||
|
||||
results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users)
|
||||
assert results == [401, 403, 200]
|
||||
|
||||
|
||||
def test_create_tag(client, data):
|
||||
users = [
|
||||
None,
|
||||
data.registered_user,
|
||||
data.project_member_without_perms,
|
||||
data.project_member_with_perms,
|
||||
data.project_owner
|
||||
]
|
||||
|
||||
post_data = json.dumps({
|
||||
"tag": "testtest",
|
||||
"color": "#123123"
|
||||
})
|
||||
|
||||
url = reverse('projects-create-tag', kwargs={"pk": data.public_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-create-tag', kwargs={"pk": data.private_project1.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-create-tag', kwargs={"pk": data.private_project2.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 200]
|
||||
|
||||
url = reverse('projects-create-tag', kwargs={"pk": data.blocked_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 451]
|
||||
|
||||
|
||||
def test_edit_tag(client, data):
|
||||
users = [
|
||||
None,
|
||||
data.registered_user,
|
||||
data.project_member_without_perms,
|
||||
data.project_member_with_perms,
|
||||
data.project_owner
|
||||
]
|
||||
|
||||
post_data = json.dumps({
|
||||
"from_tag": "tag1",
|
||||
"to_tag": "renamedtag1",
|
||||
"color": "#123123"
|
||||
})
|
||||
|
||||
url = reverse('projects-edit-tag', kwargs={"pk": data.public_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-edit-tag', kwargs={"pk": data.private_project1.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-edit-tag', kwargs={"pk": data.private_project2.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 200]
|
||||
|
||||
url = reverse('projects-edit-tag', kwargs={"pk": data.blocked_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 451]
|
||||
|
||||
|
||||
def test_delete_tag(client, data):
|
||||
users = [
|
||||
None,
|
||||
data.registered_user,
|
||||
data.project_member_without_perms,
|
||||
data.project_member_with_perms,
|
||||
data.project_owner
|
||||
]
|
||||
|
||||
post_data = json.dumps({
|
||||
"tag": "tag2",
|
||||
})
|
||||
|
||||
url = reverse('projects-delete-tag', kwargs={"pk": data.public_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-delete-tag', kwargs={"pk": data.private_project1.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-delete-tag', kwargs={"pk": data.private_project2.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 200]
|
||||
|
||||
url = reverse('projects-delete-tag', kwargs={"pk": data.blocked_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 451]
|
||||
|
||||
|
||||
def test_mix_tags(client, data):
|
||||
users = [
|
||||
None,
|
||||
data.registered_user,
|
||||
data.project_member_without_perms,
|
||||
data.project_member_with_perms,
|
||||
data.project_owner
|
||||
]
|
||||
|
||||
post_data = json.dumps({
|
||||
"from_tags": ["tag1"],
|
||||
"to_tag": "tag3"
|
||||
})
|
||||
|
||||
url = reverse('projects-mix-tags', kwargs={"pk": data.public_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-mix-tags', kwargs={"pk": data.private_project1.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [401, 403, 403, 403, 200]
|
||||
|
||||
url = reverse('projects-mix-tags', kwargs={"pk": data.private_project2.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 200]
|
||||
|
||||
url = reverse('projects-mix-tags', kwargs={"pk": data.blocked_project.pk})
|
||||
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||
assert results == [404, 404, 404, 403, 451]
|
||||
|
|
|
@ -68,7 +68,7 @@ def test_valid_project_import_without_extra_data(client):
|
|||
}
|
||||
|
||||
response = client.json.post(url, json.dumps(data))
|
||||
assert response.status_code == 201
|
||||
assert response.status_code == 201, response.data
|
||||
must_empty_children = [
|
||||
"issues", "user_stories", "us_statuses", "wiki_pages", "priorities",
|
||||
"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.permissions.choices import ANON_PERMISSIONS
|
||||
from taiga.projects.models import Project
|
||||
from taiga.projects.userstories.models import UserStory
|
||||
from taiga.projects.tasks.models import Task
|
||||
from taiga.projects.issues.models import Issue
|
||||
from taiga.projects.choices import BLOCKED_BY_DELETING
|
||||
|
||||
from .. import factories as f
|
||||
|
@ -1852,3 +1855,209 @@ def test_delete_project_with_celery_disabled(client, settings):
|
|||
response = client.json.delete(url)
|
||||
assert response.status_code == 204
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
us = f.create_userstory()
|
||||
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()
|
||||
viewer_user = f.UserFactory()
|
||||
|
||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
||||
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
|
||||
project.add_watcher(fav_user)
|
||||
|
||||
|
@ -499,11 +499,6 @@ def test_get_watched_list_valid_info_for_project():
|
|||
assert project_watch_info["assigned_to"] == None
|
||||
assert project_watch_info["status"] == None
|
||||
assert project_watch_info["status_color"] == None
|
||||
|
||||
tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]}
|
||||
assert "test" in tags_colors
|
||||
assert "tag" in tags_colors
|
||||
|
||||
assert project_watch_info["is_private"] == project.is_private
|
||||
assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
||||
assert project_watch_info["is_fan"] == False
|
||||
|
@ -540,7 +535,7 @@ def test_get_liked_list_valid_info():
|
|||
fan_user = f.UserFactory()
|
||||
viewer_user = f.UserFactory()
|
||||
|
||||
project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag'])
|
||||
project = f.ProjectFactory(is_private=False, name="Testing project")
|
||||
content_type = ContentType.objects.get_for_model(project)
|
||||
like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user)
|
||||
project.refresh_totals()
|
||||
|
@ -558,11 +553,6 @@ def test_get_liked_list_valid_info():
|
|||
assert project_like_info["assigned_to"] == None
|
||||
assert project_like_info["status"] == None
|
||||
assert project_like_info["status_color"] == None
|
||||
|
||||
tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]}
|
||||
assert "test" in tags_colors
|
||||
assert "tag" in tags_colors
|
||||
|
||||
assert project_like_info["is_private"] == project.is_private
|
||||
assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
|
||||
|
||||
|
|
|
@ -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