Epic custom attributes values

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-06-30 13:27:48 +02:00
parent f70923c064
commit 389a18026b
16 changed files with 532 additions and 37 deletions

View File

@ -38,6 +38,11 @@ class BaseCustomAttributeAdmin:
raw_id_fields = ["project"]
@admin.register(models.EpicCustomAttribute)
class EpicCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin):
pass
@admin.register(models.UserStoryCustomAttribute)
class UserStoryCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin):
pass

View File

@ -41,6 +41,18 @@ from . import services
# Custom Attribute ViewSets
#######################################################
class EpicCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.EpicCustomAttribute
serializer_class = serializers.EpicCustomAttributeSerializer
validator_class = validators.EpicCustomAttributeValidator
permission_classes = (permissions.EpicCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
bulk_update_param = "bulk_epic_custom_attributes"
bulk_update_perm = "change_epic_custom_attributes"
bulk_update_order_action = services.bulk_update_epic_custom_attribute_order
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer
@ -87,6 +99,20 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin,
return getattr(obj, self.content_object)
class EpicCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.EpicCustomAttributesValues
serializer_class = serializers.EpicCustomAttributesValuesSerializer
validator_class = validators.EpicCustomAttributesValuesValidator
permission_classes = (permissions.EpicCustomAttributesValuesPermission,)
lookup_field = "epic_id"
content_object = "epic"
def get_queryset(self):
qs = self.model.objects.all()
qs = qs.select_related("epic", "epic__project")
return qs
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.UserStoryCustomAttributesValues
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-06-30 08:49
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import django_pgjson.fields
class Migration(migrations.Migration):
dependencies = [
('epics', '0001_initial'),
('projects', '0049_auto_20160629_1443'),
('custom_attributes', '0007_auto_20160208_1751'),
]
operations = [
migrations.AlterModelOptions(
name='issuecustomattributesvalues',
options={'ordering': ['id'], 'verbose_name': 'issue custom attributes values', 'verbose_name_plural': 'issue custom attributes values'},
),
migrations.AlterModelOptions(
name='taskcustomattributesvalues',
options={'ordering': ['id'], 'verbose_name': 'task custom attributes values', 'verbose_name_plural': 'task custom attributes values'},
),
migrations.AlterModelOptions(
name='userstorycustomattributesvalues',
options={'ordering': ['id'], 'verbose_name': 'user story custom attributes values', 'verbose_name_plural': 'user story custom attributes values'},
),
migrations.CreateModel(
name='EpicCustomAttribute',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64, verbose_name='name')),
('description', models.TextField(blank=True, verbose_name='description')),
('type', models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')], default='text', max_length=16, verbose_name='type')),
('order', models.IntegerField(default=10000, verbose_name='order')),
('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')),
('modified_date', models.DateTimeField(verbose_name='modified date')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epiccustomattributes', to='projects.Project', verbose_name='project')),
],
options={
'verbose_name_plural': 'epic custom attributes',
'verbose_name': 'epic custom attribute',
'abstract': False,
'ordering': ['project', 'order', 'name'],
},
),
migrations.CreateModel(
name='EpicCustomAttributesValues',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.IntegerField(default=1, verbose_name='version')),
('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='values')),
('epic', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_attributes_values', to='epics.Epic', verbose_name='epic')),
],
options={
'verbose_name_plural': 'epic custom attributes values',
'verbose_name': 'epic custom attributes values',
'abstract': False,
'ordering': ['id'],
},
),
migrations.AlterUniqueTogether(
name='epiccustomattribute',
unique_together=set([('project', 'name')]),
),
# Trigger: Clean epiccustomattributes values before remove a epiccustomattribute
migrations.RunSQL(
"""
CREATE TRIGGER "update_epiccustomvalues_after_remove_epiccustomattribute"
AFTER DELETE ON custom_attributes_epiccustomattribute
FOR EACH ROW
EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_epiccustomattributesvalues');
""",
reverse_sql="""DROP TRIGGER IF EXISTS "update_epiccustomvalues_after_remove_epiccustomattribute"
ON custom_attributes_epiccustomattribute
CASCADE;"""
),
]

View File

@ -31,7 +31,6 @@ from . import choices
# Custom Attribute Models
#######################################################
class AbstractCustomAttribute(models.Model):
name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name"))
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
@ -63,6 +62,12 @@ class AbstractCustomAttribute(models.Model):
return super().save(*args, **kwargs)
class EpicCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "epic custom attribute"
verbose_name_plural = "epic custom attributes"
class UserStoryCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "user story custom attribute"
@ -93,13 +98,28 @@ class AbstractCustomAttributesValues(OCCModelMixin, models.Model):
ordering = ["id"]
class EpicCustomAttributesValues(AbstractCustomAttributesValues):
epic = models.OneToOneField("epics.Epic",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("epic"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "epic custom attributes values"
verbose_name_plural = "epic custom attributes values"
@property
def project(self):
# NOTE: This property simplifies checking permissions
return self.epic.project
class UserStoryCustomAttributesValues(AbstractCustomAttributesValues):
user_story = models.OneToOneField("userstories.UserStory",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("user story"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "user story ustom attributes values"
verbose_name = "user story custom attributes values"
verbose_name_plural = "user story custom attributes values"
index_together = [("user_story",)]
@ -115,7 +135,7 @@ class TaskCustomAttributesValues(AbstractCustomAttributesValues):
verbose_name=_("task"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "task ustom attributes values"
verbose_name = "task custom attributes values"
verbose_name_plural = "task custom attributes values"
index_together = [("task",)]
@ -131,7 +151,7 @@ class IssueCustomAttributesValues(AbstractCustomAttributesValues):
verbose_name=_("issue"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "issue ustom attributes values"
verbose_name = "issue custom attributes values"
verbose_name_plural = "issue custom attributes values"
index_together = [("issue",)]

View File

@ -27,6 +27,18 @@ from taiga.base.api.permissions import IsSuperUser
# Custom Attribute Permissions
#######################################################
class EpicCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectAdmin()
update_perms = IsProjectAdmin()
partial_update_perms = IsProjectAdmin()
destroy_perms = IsProjectAdmin()
list_perms = AllowAny()
bulk_update_order_perms = IsProjectAdmin()
class UserStoryCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
@ -67,6 +79,14 @@ class IssueCustomAttributePermission(TaigaResourcePermission):
# Custom Attributes Values Permissions
#######################################################
class EpicCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
update_perms = HasProjectPerm('modify_us')
partial_update_perms = HasProjectPerm('modify_us')
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None

View File

@ -36,6 +36,10 @@ class BaseCustomAttributeSerializer(serializers.LightSerializer):
modified_date = Field()
class EpicCustomAttributeSerializer(BaseCustomAttributeSerializer):
pass
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
pass
@ -56,6 +60,10 @@ class BaseCustomAttributesValuesSerializer(serializers.LightSerializer):
version = Field()
class EpicCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
epic = Field(attr="epic.id")
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
user_story = Field(attr="user_story.id")

View File

@ -20,6 +20,23 @@ from django.db import transaction
from django.db import connection
@transaction.atomic
def bulk_update_epic_custom_attribute_order(project, user, data):
cursor = connection.cursor()
sql = """
prepare bulk_update_order as update custom_attributes_epiccustomattribute set "order" = $1
where custom_attributes_epiccustomattribute.id = $2 and
custom_attributes_epiccustomattribute.project_id = $3;
"""
cursor.execute(sql)
for id, order in data:
cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
(order, id, project.id))
cursor.execute("DEALLOCATE bulk_update_order")
cursor.close()
@transaction.atomic
def bulk_update_userstory_custom_attribute_order(project, user, data):
cursor = connection.cursor()

View File

@ -19,6 +19,12 @@
from . import models
def create_custom_attribute_value_when_create_epic(sender, instance, created, **kwargs):
if created:
models.EpicCustomAttributesValues.objects.get_or_create(epic=instance,
defaults={"attributes_values":{}})
def create_custom_attribute_value_when_create_user_story(sender, instance, created, **kwargs):
if created:
models.UserStoryCustomAttributesValues.objects.get_or_create(user_story=instance,

View File

@ -66,6 +66,11 @@ class BaseCustomAttributeValidator(ModelValidator):
return self._validate_integrity_between_project_and_name(attrs, source)
class EpicCustomAttributeValidator(BaseCustomAttributeValidator):
class Meta(BaseCustomAttributeValidator.Meta):
model = models.EpicCustomAttribute
class UserStoryCustomAttributeValidator(BaseCustomAttributeValidator):
class Meta(BaseCustomAttributeValidator.Meta):
model = models.UserStoryCustomAttribute
@ -121,6 +126,15 @@ class BaseCustomAttributesValuesValidator(ModelValidator):
return attrs
class EpicCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator):
_custom_attribute_model = models.EpicCustomAttribute
_container_model = "epics.Epic"
_container_field = "epic"
class Meta(BaseCustomAttributesValuesValidator.Meta):
model = models.EpicCustomAttributesValues
class UserStoryCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator):
_custom_attribute_model = models.UserStoryCustomAttribute
_container_model = "userstories.UserStory"

View File

@ -30,8 +30,16 @@ def connect_epics_signals():
dispatch_uid="tags_normalization_epic")
def connect_epics_custom_attributes_signals():
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_epic,
sender=apps.get_model("epics", "Epic"),
dispatch_uid="create_custom_attribute_value_when_create_epic")
def connect_all_epics_signals():
connect_epics_signals()
connect_epics_custom_attributes_signals()
def disconnect_epics_signals():
@ -39,8 +47,14 @@ def disconnect_epics_signals():
dispatch_uid="tags_normalization")
def disconnect_epics_custom_attributes_signals():
signals.post_save.disconnect(sender=apps.get_model("epics", "Epic"),
dispatch_uid="create_custom_attribute_value_when_create_epic")
def disconnect_all_epics_signals():
disconnect_epics_signals()
disconnect_epics_custom_attributes_signals()
class EpicsAppConfig(AppConfig):

View File

@ -131,7 +131,7 @@ LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS = [0, 1, 2]
class Command(BaseCommand):
sd = SampleDataHelper(seed=12345678901)
@transaction.atomic
#@transaction.atomic
def handle(self, *args, **options):
# Prevent events emission when sample data is running
disconnect_events_signals()
@ -193,6 +193,13 @@ class Command(BaseCommand):
# added custom attributes
names = set([self.sd.words(1, 3) for i in range(1, 6)])
for name in names:
EpicCustomAttribute.objects.create(name=name,
description=self.sd.words(3, 12),
type=self.sd.choice(TYPES_CHOICES)[0],
project=project,
order=i)
names = set([self.sd.words(1, 3) for i in range(1, 6)])
for name in names:
UserStoryCustomAttribute.objects.create(name=name,
description=self.sd.words(3, 12),
@ -511,14 +518,13 @@ class Command(BaseCommand):
status=self.sd.db_object_from_queryset(project.epic_statuses.filter(
is_closed=False)),
tags=self.sd.words(1, 3).split(" "))
epic.save()
# TODO: Epic custom attributes
#custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
# in project.epiccustomattributes.all() if self.sd.boolean()}
#if custom_attributes_values:
# epic.custom_attributes_values.attributes_values = custom_attributes_values
# epic.custom_attributes_values.save()
custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
in project.epiccustomattributes.all() if self.sd.boolean()}
if custom_attributes_values:
epic.custom_attributes_values.attributes_values = custom_attributes_values
epic.custom_attributes_values.save()
for i in range(self.sd.int(*NUM_ATTACHMENTS)):
attachment = self.create_attachment(epic, i+1)

View File

@ -30,6 +30,15 @@ from taiga.permissions.services import calculate_permissions
from taiga.permissions.services import is_project_admin, is_project_owner
from . import services
<<<<<<< HEAD
=======
from .custom_attributes.serializers import EpicCustomAttributeSerializer
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
>>>>>>> 5f3559d... Epic custom attributes values
from .notifications.choices import NotifyLevel
@ -352,6 +361,7 @@ class ProjectSerializer(serializers.LightSerializer):
class ProjectDetailSerializer(ProjectSerializer):
epic_statuses = Field(attr="epic_statuses_attr")
us_statuses = Field(attr="userstory_statuses_attr")
points = Field(attr="points_attr")
task_statuses = Field(attr="task_statuses_attr")
@ -359,6 +369,7 @@ class ProjectDetailSerializer(ProjectSerializer):
issue_types = Field(attr="issue_types_attr")
priorities = Field(attr="priorities_attr")
severities = Field(attr="severities_attr")
epic_custom_attributes = Field(attr="epic_custom_attributes_attr")
userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr")
task_custom_attributes = Field(attr="task_custom_attributes_attr")
issue_custom_attributes = Field(attr="issue_custom_attributes_attr")
@ -385,9 +396,9 @@ class ProjectDetailSerializer(ProjectSerializer):
def to_value(self, instance):
# Name attributes must be translated
for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr",
"issue_statuses_attr", "issue_types_attr", "priorities_attr",
"severities_attr", "userstory_custom_attributes_attr",
for attr in ["epic_statuses_attr", "userstory_statuses_attr", "points_attr", "task_statuses_attr",
"issue_statuses_attr", "issue_types_attr", "priorities_attr", "severities_attr",
"epic_custom_attributes_attr", "userstory_custom_attributes_attr",
"task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]:
assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
@ -430,8 +441,10 @@ class ProjectDetailSerializer(ProjectSerializer):
return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj):
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
assert (hasattr(obj, "private_projects_same_owner_attr"),
"instance must have a private_projects_same_owner_attr attribute"
assert (hasattr(obj, "public_projects_same_owner_attr"),
"instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_is_out_of_owner_limits(
obj,
current_memberships=self.get_total_memberships(obj),
@ -440,8 +453,10 @@ class ProjectDetailSerializer(ProjectSerializer):
)
def get_is_private_extra_info(self, obj):
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
assert (hasattr(obj, "private_projects_same_owner_attr"),
"instance must have a private_projects_same_owner_attr attribute"
assert (hasattr(obj, "public_projects_same_owner_attr"),
"instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_privacity_can_be_changed(
obj,
current_memberships=self.get_total_memberships(obj),
@ -466,6 +481,7 @@ class ProjectTemplateSerializer(serializers.LightSerializer):
created_date = Field()
modified_date = Field()
default_owner_role = Field()
is_epics_activated = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()
is_wiki_activated = Field()
@ -473,6 +489,7 @@ class ProjectTemplateSerializer(serializers.LightSerializer):
videoconferences = Field()
videoconferences_extra_data = Field()
default_options = Field()
epic_statuses = Field()
us_statuses = Field()
points = Field()
task_statuses = Field()

View File

@ -277,6 +277,26 @@ def attach_severities(queryset, as_field="severities_attr"):
return queryset
def attach_epic_custom_attributes(queryset, as_field="epic_custom_attributes_attr"):
"""Attach a json epic custom attributes representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the epic custom attributes as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(custom_attributes_epiccustomattribute))
FROM custom_attributes_epiccustomattribute
WHERE
custom_attributes_epiccustomattribute.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"):
"""Attach a json userstory custom attributes representation to each object of the queryset.
@ -471,6 +491,7 @@ def attach_extra_info(queryset, user=None):
queryset = attach_issue_types(queryset)
queryset = attach_priorities(queryset)
queryset = attach_severities(queryset)
queryset = attach_epic_custom_attributes(queryset)
queryset = attach_userstory_custom_attributes(queryset)
queryset = attach_task_custom_attributes(queryset)
queryset = attach_issue_custom_attributes(queryset)

View File

@ -54,6 +54,7 @@ from taiga.projects.api import ProjectFansViewSet
from taiga.projects.api import ProjectWatchersViewSet
from taiga.projects.api import MembershipViewSet
from taiga.projects.api import InvitationViewSet
from taiga.projects.api import EpicStatusViewSet
from taiga.projects.api import UserStoryStatusViewSet
from taiga.projects.api import PointsViewSet
from taiga.projects.api import TaskStatusViewSet
@ -69,6 +70,7 @@ router.register(r"projects/(?P<resource_id>\d+)/watchers", ProjectWatchersViewSe
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations")
router.register(r"epic-statuses", EpicStatusViewSet, base_name="epic-statuses")
router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses")
router.register(r"points", PointsViewSet, base_name="points")
router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses")
@ -79,13 +81,18 @@ router.register(r"severities",SeverityViewSet , base_name="severities")
# Custom Attributes
from taiga.projects.custom_attributes.api import EpicCustomAttributeViewSet
from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet
from taiga.projects.custom_attributes.api import EpicCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import UserStoryCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributesValuesViewSet
router.register(r"epic-custom-attributes", EpicCustomAttributeViewSet,
base_name="epic-custom-attributes")
router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet,
base_name="userstory-custom-attributes")
router.register(r"task-custom-attributes", TaskCustomAttributeViewSet,
@ -93,6 +100,8 @@ router.register(r"task-custom-attributes", TaskCustomAttributeViewSet,
router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet,
base_name="issue-custom-attributes")
router.register(r"epics/custom-attributes-values", EpicCustomAttributesValuesViewSet,
base_name="epic-custom-attributes-values")
router.register(r"userstories/custom-attributes-values", UserStoryCustomAttributesValuesViewSet,
base_name="userstory-custom-attributes-values")
router.register(r"tasks/custom-attributes-values", TaskCustomAttributesValuesViewSet,
@ -114,50 +123,76 @@ router.register(r"resolver", ResolverViewSet, base_name="resolver")
# Attachments
from taiga.projects.attachments.api import EpicAttachmentViewSet
from taiga.projects.attachments.api import UserStoryAttachmentViewSet
from taiga.projects.attachments.api import IssueAttachmentViewSet
from taiga.projects.attachments.api import TaskAttachmentViewSet
from taiga.projects.attachments.api import WikiAttachmentViewSet
router.register(r"epics/attachments", EpicAttachmentViewSet,
base_name="epic-attachments")
router.register(r"userstories/attachments", UserStoryAttachmentViewSet,
base_name="userstory-attachments")
router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-attachments")
router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments")
router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
router.register(r"tasks/attachments", TaskAttachmentViewSet,
base_name="task-attachments")
router.register(r"issues/attachments", IssueAttachmentViewSet,
base_name="issue-attachments")
router.register(r"wiki/attachments", WikiAttachmentViewSet,
base_name="wiki-attachments")
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.milestones.api import MilestoneWatchersViewSet
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.userstories.api import UserStoryVotersViewSet
from taiga.projects.userstories.api import UserStoryWatchersViewSet
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.tasks.api import TaskVotersViewSet
from taiga.projects.tasks.api import TaskWatchersViewSet
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import IssueVotersViewSet
from taiga.projects.issues.api import IssueWatchersViewSet
from taiga.projects.wiki.api import WikiViewSet
from taiga.projects.wiki.api import WikiLinkViewSet
from taiga.projects.wiki.api import WikiWatchersViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"milestones/(?P<resource_id>\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"userstories/(?P<resource_id>\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
router.register(r"userstories/(?P<resource_id>\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"tasks/(?P<resource_id>\d+)/voters", TaskVotersViewSet, base_name="task-voters")
router.register(r"tasks/(?P<resource_id>\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<resource_id>\d+)/voters", IssueVotersViewSet, base_name="issue-voters")
router.register(r"issues/(?P<resource_id>\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers")
router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki/(?P<resource_id>\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
router.register(r"milestones", MilestoneViewSet,
base_name="milestones")
router.register(r"milestones/(?P<resource_id>\d+)/watchers", MilestoneWatchersViewSet,
base_name="milestone-watchers")
router.register(r"userstories", UserStoryViewSet,
base_name="userstories")
router.register(r"userstories/(?P<resource_id>\d+)/voters", UserStoryVotersViewSet,
base_name="userstory-voters")
router.register(r"userstories/(?P<resource_id>\d+)/watchers", UserStoryWatchersViewSet,
base_name="userstory-watchers")
router.register(r"tasks", TaskViewSet,
base_name="tasks")
router.register(r"tasks/(?P<resource_id>\d+)/voters", TaskVotersViewSet,
base_name="task-voters")
router.register(r"tasks/(?P<resource_id>\d+)/watchers", TaskWatchersViewSet,
base_name="task-watchers")
router.register(r"issues", IssueViewSet,
base_name="issues")
router.register(r"issues/(?P<resource_id>\d+)/voters", IssueVotersViewSet,
base_name="issue-voters")
router.register(r"issues/(?P<resource_id>\d+)/watchers", IssueWatchersViewSet,
base_name="issue-watchers")
router.register(r"wiki", WikiViewSet,
base_name="wiki")
router.register(r"wiki/(?P<resource_id>\d+)/watchers", WikiWatchersViewSet,
base_name="wiki-watchers")
router.register(r"wiki-links", WikiLinkViewSet,
base_name="wiki-links")
# History & Components
@ -223,11 +258,11 @@ router.register(r"exporter", ProjectExporterViewSet, base_name="exporter")
# External apps
from taiga.external_apps.api import Application, ApplicationToken
router.register(r"applications", Application, base_name="applications")
router.register(r"application-tokens", ApplicationToken, base_name="application-tokens")
# Stats
# - see taiga.stats.routers and taiga.stats.apps

View File

@ -57,6 +57,7 @@ class ProjectTemplateFactory(Factory):
slug = settings.DEFAULT_PROJECT_TEMPLATE
description = factory.Sequence(lambda n: "Description {}".format(n))
epic_statuses = []
us_statuses = []
points = []
task_statuses = []

View File

@ -0,0 +1,201 @@
# -*- 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.transaction import atomic
from django.core.urlresolvers import reverse
from taiga.base.utils import json
from .. import factories as f
import pytest
pytestmark = pytest.mark.django_db
#########################################################
# Epic Custom Attributes
#########################################################
def test_epic_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.EpicCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_admin=True)
url = reverse("epic-custom-attributes-list")
data = {"name": custom_attr_1.name,
"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_epic_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_1 = f.EpicCustomAttributeFactory()
custom_attr_2 = f.EpicCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_admin=True)
url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"name": custom_attr_1.name}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_epic_custom_attribute_duplicate_name_error_on_move_between_projects(client):
custom_attr_1 = f.EpicCustomAttributeFactory()
custom_attr_2 = f.EpicCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_admin=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
is_admin=True)
url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
#########################################################
# Epic Custom Attributes Values
#########################################################
def test_epic_custom_attributes_values_when_create_us(client):
epic = f.EpicFactory()
assert epic.custom_attributes_values.attributes_values == {}
def test_epic_custom_attributes_values_update(client):
epic = f.EpicFactory()
member = f.MembershipFactory(user=epic.project.owner,
project=epic.project,
is_admin=True)
custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = epic.custom_attributes_values
url = reverse("epic-custom-attributes-values-detail", args=[epic.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
ct2_id: "test_2_updated"
},
"version": custom_attrs_val.version
}
assert epic.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
assert response.data["attributes_values"] == data["attributes_values"]
epic = epic.__class__.objects.get(id=epic.id)
assert epic.custom_attributes_values.attributes_values == data["attributes_values"]
def test_epic_custom_attributes_values_update_with_error_invalid_key(client):
epic = f.EpicFactory()
member = f.MembershipFactory(user=epic.project.owner,
project=epic.project,
is_admin=True)
custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project)
custom_attrs_val = epic.custom_attributes_values
url = reverse("epic-custom-attributes-values-detail", args=[epic.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
"123456": "test_2_updated"
},
"version": custom_attrs_val.version
}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_epic_custom_attributes_values_delete_epic(client):
epic = f.EpicFactory()
member = f.MembershipFactory(user=epic.project.owner,
project=epic.project,
is_admin=True)
custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = epic.custom_attributes_values
url = reverse("epics-detail", args=[epic.id])
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
assert not epic.__class__.objects.filter(id=epic.id).exists()
assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists()
#########################################################
# Test tristres triggers :-P
#########################################################
def test_trigger_update_epiccustomvalues_afeter_remove_epiccustomattribute(client):
epic = f.EpicFactory()
member = f.MembershipFactory(user=epic.project.owner,
project=epic.project,
is_admin=True)
custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = epic.custom_attributes_values
custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"}
custom_attrs_val.save()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id in custom_attrs_val.attributes_values.keys()
url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id)
assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id not in custom_attrs_val.attributes_values.keys()