Initial epic model + epic status + epcic attachments + project template + project defaults

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-06-29 17:25:01 +02:00
parent 75a9751136
commit f70923c064
26 changed files with 931 additions and 84 deletions

View File

@ -300,6 +300,7 @@ INSTALLED_APPS = [
"taiga.projects.likes",
"taiga.projects.votes",
"taiga.projects.milestones",
"taiga.projects.epics",
"taiga.projects.userstories",
"taiga.projects.tasks",
"taiga.projects.issues",

View File

@ -198,6 +198,10 @@ class PermissionBasedAttachmentFilterBackend(PermissionBasedFilterBackend):
return qs.filter(content_type=ct)
class CanViewEpicAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend):
permission = "view_epic"
class CanViewUserStoryAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend):
permission = "view_us"

View File

@ -22,12 +22,14 @@ from django.utils.translation import ugettext_lazy as _
ANON_PERMISSIONS = [
('view_project', _('View project')),
('view_milestones', _('View milestones')),
('view_epic', _('View epic')),
('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')),
]
MEMBERS_PERMISSIONS = [
('view_project', _('View project')),
# Milestone permissions
@ -36,6 +38,12 @@ MEMBERS_PERMISSIONS = [
('modify_milestone', _('Modify milestone')),
('delete_milestone', _('Delete milestone')),
# US permissions
('view_epic', _('View epic')),
('add_epic', _('Add epic')),
('modify_epic', _('Modify epic')),
('comment_epic', _('Comment epic')),
('delete_epic', _('Delete epic')),
# US permissions
('view_us', _('View user story')),
('add_us', _('Add user story')),
('modify_us', _('Modify user story')),

View File

@ -40,6 +40,8 @@ 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.epics.models import Epic
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
@ -49,7 +51,6 @@ 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 . import filters as project_filters
@ -110,17 +111,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
now = timezone.now()
order_by_field_name = self._get_order_by_field_name()
if order_by_field_name == "total_fans_last_week":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1))
elif order_by_field_name == "total_fans_last_month":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1))
elif order_by_field_name == "total_fans_last_year":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1))
elif order_by_field_name == "total_activity_last_week":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1))
elif order_by_field_name == "total_activity_last_month":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1))
elif order_by_field_name == "total_activity_last_year":
qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1))
return qs
@ -449,21 +450,21 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
## Custom values for selectors
######################################################
class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
class EpicStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Points
serializer_class = serializers.PointsSerializer
validator_class = validators.PointsValidator
permission_classes = (permissions.PointsPermission,)
model = models.EpicStatus
serializer_class = serializers.EpicStatusSerializer
validator_class = validators.EpicStatusValidator
permission_classes = (permissions.EpicStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
bulk_update_param = "bulk_points"
bulk_update_perm = "change_points"
bulk_update_order_action = services.bulk_update_points_order
move_on_destroy_related_class = RolePoints
move_on_destroy_related_field = "points"
move_on_destroy_project_default_field = "default_points"
bulk_update_param = "bulk_epic_statuses"
bulk_update_perm = "change_epicstatus"
bulk_update_order_action = services.bulk_update_epic_status_order
move_on_destroy_related_class = Epic
move_on_destroy_related_field = "status"
move_on_destroy_project_default_field = "default_epic_status"
class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
@ -483,6 +484,23 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
move_on_destroy_project_default_field = "default_us_status"
class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Points
serializer_class = serializers.PointsSerializer
validator_class = validators.PointsValidator
permission_classes = (permissions.PointsPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
bulk_update_param = "bulk_points"
bulk_update_perm = "change_points"
bulk_update_order_action = services.bulk_update_points_order
move_on_destroy_related_class = RolePoints
move_on_destroy_related_field = "points"
move_on_destroy_project_default_field = "default_points"
class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):

View File

@ -83,6 +83,12 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
return obj.content_object
class EpicAttachmentViewSet(BaseAttachmentViewSet):
permission_classes = (permissions.EpicAttachmentPermission,)
filter_backends = (filters.CanViewEpicAttachmentFilterBackend,)
content_type = "epics.epic"
class UserStoryAttachmentViewSet(BaseAttachmentViewSet):
permission_classes = (permissions.UserStoryAttachmentPermission,)
filter_backends = (filters.CanViewUserStoryAttachmentFilterBackend,)

View File

@ -28,6 +28,15 @@ class IsAttachmentOwnerPerm(PermissionComponent):
return False
class EpicAttachmentPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_epic') | IsAttachmentOwnerPerm()
create_perms = HasProjectPerm('modify_epic')
update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
partial_update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
destroy_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
list_perms = AllowAny()
class UserStoryAttachmentPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_us') | IsAttachmentOwnerPerm()
create_perms = HasProjectPerm('modify_us')
@ -67,7 +76,9 @@ class WikiAttachmentPermission(TaigaResourcePermission):
class RawAttachmentPerm(PermissionComponent):
def check_permissions(self, request, view, obj=None):
is_owner = IsAttachmentOwnerPerm().check_permissions(request, view, obj)
if obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory":
if obj.content_type.app_label == "epics" and obj.content_type.model == "epic":
return EpicAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner
elif obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory":
return UserStoryAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner
elif obj.content_type.app_label == "tasks" and obj.content_type.model == "task":
return TaskAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner

View File

@ -0,0 +1,20 @@
# -*- 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/>.
default_app_config = "taiga.projects.epics.apps.EpicsAppConfig"

View File

@ -0,0 +1,51 @@
# -*- 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.apps import AppConfig
from django.apps import apps
from django.db.models import signals
def connect_epics_signals():
from taiga.projects.tagging import signals as tagging_handlers
# Tags
signals.pre_save.connect(tagging_handlers.tags_normalization,
sender=apps.get_model("epics", "Epic"),
dispatch_uid="tags_normalization_epic")
def connect_all_epics_signals():
connect_epics_signals()
def disconnect_epics_signals():
signals.pre_save.disconnect(sender=apps.get_model("epics", "Task"),
dispatch_uid="tags_normalization")
def disconnect_all_epics_signals():
disconnect_epics_signals()
class EpicsAppConfig(AppConfig):
name = "taiga.projects.epics"
verbose_name = "Epics"
def ready(self):
connect_all_epics_signals()

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-06-29 14:43
from __future__ import unicode_literals
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import taiga.projects.notifications.mixins
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('projects', '0049_auto_20160629_1443'),
('userstories', '0012_auto_20160614_1201'),
]
operations = [
migrations.CreateModel(
name='Epic',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tags', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags')),
('version', models.IntegerField(default=1, verbose_name='version')),
('is_blocked', models.BooleanField(default=False, verbose_name='is blocked')),
('blocked_note', models.TextField(blank=True, default='', verbose_name='blocked note')),
('ref', models.BigIntegerField(blank=True, db_index=True, default=None, null=True, verbose_name='ref')),
('is_closed', models.BooleanField(default=False)),
('epics_order', models.IntegerField(default=10000, verbose_name='epics order')),
('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')),
('modified_date', models.DateTimeField(verbose_name='modified date')),
('finish_date', models.DateTimeField(blank=True, null=True, verbose_name='finish date')),
('subject', models.TextField(verbose_name='subject')),
('description', models.TextField(blank=True, verbose_name='description')),
('client_requirement', models.BooleanField(default=False, verbose_name='is client requirement')),
('team_requirement', models.BooleanField(default=False, verbose_name='is team requirement')),
('assigned_to', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='epics_assigned_to_me', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_epics', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epics', to='projects.Project', verbose_name='project')),
('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='epics', to='projects.EpicStatus', verbose_name='status')),
('user_stories', models.ManyToManyField(related_name='epics', to='userstories.UserStory', verbose_name='user stories')),
],
options={
'verbose_name_plural': 'epics',
'ordering': ['project', 'ref'],
'verbose_name': 'epic',
},
bases=(taiga.projects.notifications.mixins.WatchedModelMixin, models.Model),
),
# Execute trigger after epic update
migrations.RunSQL(
"""
DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_update ON epics_epic;
CREATE TRIGGER update_project_tags_colors_on_epic_update
AFTER UPDATE ON epics_epic
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
"""
),
# Execute trigger after epic insert
migrations.RunSQL(
"""
DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_insert ON epics_epic;
CREATE TRIGGER update_project_tags_colors_on_epic_insert
AFTER INSERT ON epics_epic
FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
"""
),
]

View File

@ -0,0 +1,91 @@
# -*- 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 models
from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
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
class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_("ref"))
project = models.ForeignKey("projects.Project", null=False, blank=False,
related_name="epics", verbose_name=_("project"))
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
related_name="owned_epics", verbose_name=_("owner"),
on_delete=models.SET_NULL)
status = models.ForeignKey("projects.EpicStatus", null=True, blank=True,
related_name="epics", verbose_name=_("status"),
on_delete=models.SET_NULL)
is_closed = models.BooleanField(default=False)
epics_order = models.IntegerField(null=False, blank=False, default=10000,
verbose_name=_("epics order"))
created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"),
default=timezone.now)
modified_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("modified date"))
finish_date = models.DateTimeField(null=True, blank=True,
verbose_name=_("finish date"))
subject = models.TextField(null=False, blank=False,
verbose_name=_("subject"))
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
default=None, related_name="epics_assigned_to_me",
verbose_name=_("assigned to"))
client_requirement = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is client requirement"))
team_requirement = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is team requirement"))
user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics",
verbose_name=_("user stories"))
attachments = GenericRelation("attachments.Attachment")
_importing = None
class Meta:
verbose_name = "epic"
verbose_name_plural = "epics"
ordering = ["project", "ref"]
def save(self, *args, **kwargs):
if not self._importing or not self.modified_date:
self.modified_date = timezone.now()
if not self.status:
self.status = self.project.default_epic_status
super().save(*args, **kwargs)
def __str__(self):
return "({1}) {0}".format(self.ref, self.subject)
def __repr__(self):
return "<Epic %s>" % (self.id)

View File

@ -5,26 +5,28 @@
"fields": {
"name": "Scrum",
"slug": "scrum",
"order": 1,
"description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers",
"order": 1,
"created_date": "2014-04-22T14:48:43.596Z",
"modified_date": "2014-07-25T10:02:46.479Z",
"modified_date": "2016-06-29T14:52:11.273Z",
"default_owner_role": "product-owner",
"is_epics_activated": true,
"is_backlog_activated": true,
"is_kanban_activated": false,
"is_wiki_activated": true,
"is_issues_activated": true,
"videoconferences": null,
"videoconferences_extra_data": "",
"default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}",
"us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#669900\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]",
"default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}",
"epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]",
"us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]",
"points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]",
"task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#ff9900\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#ffcc00\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#669900\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#999999\", \"is_closed\": false}]",
"issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#8C2318\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#5E8C6A\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#88A65E\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#BFB35A\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#89BAB4\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#CC0000\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#666666\", \"is_closed\": false}]",
"task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#ffcc00\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#669900\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#999999\", \"slug\": \"needs-info\", \"is_closed\": false}]",
"issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#8C2318\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#5E8C6A\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#88A65E\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#BFB35A\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#89BAB4\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#CC0000\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#666666\", \"slug\": \"posponed\", \"is_closed\": false}]",
"issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#89BAB4\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#ba89a8\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#89a8ba\"}]",
"priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#666666\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#669933\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]",
"severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#666666\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#669933\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#0000FF\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#FFA500\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]",
"roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]"
"roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]"
}
},
{
@ -33,26 +35,28 @@
"fields": {
"name": "Kanban",
"slug": "kanban",
"order": 2,
"description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.",
"order": 2,
"created_date": "2014-04-22T14:50:19.738Z",
"modified_date": "2014-07-25T13:11:42.754Z",
"modified_date": "2016-06-29T14:52:15.232Z",
"default_owner_role": "product-owner",
"is_epics_activated": true,
"is_backlog_activated": false,
"is_kanban_activated": true,
"is_wiki_activated": false,
"is_issues_activated": false,
"videoconferences": null,
"videoconferences_extra_data": "",
"default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}",
"us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#f57900\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#729fcf\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#4e9a06\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#cc0000\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]",
"default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}",
"epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]",
"us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#f57900\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#4e9a06\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#cc0000\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]",
"points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]",
"task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}]",
"issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#d3d7cf\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#75507b\", \"is_closed\": false}]",
"task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}]",
"issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#d3d7cf\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#75507b\", \"slug\": \"posponed\", \"is_closed\": false}]",
"issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#cc0000\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#4e9a06\"}]",
"priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#999999\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]",
"severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#999999\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#f57900\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]",
"roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]"
"roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]"
}
}
]

View File

@ -34,6 +34,7 @@ from taiga.permissions.choices import ANON_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_STAFF
from taiga.external_apps.models import Application, ApplicationToken
from taiga.projects.models import *
from taiga.projects.epics.models import *
from taiga.projects.milestones.models import *
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.services.stats import get_stats_for_project
@ -109,6 +110,8 @@ NUM_PROJECTS =getattr(settings, "SAMPLE_DATA_NUM_PROJECTS", 4)
NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2)
NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1)
NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5))
NUM_EPICS = getattr(settings, "SAMPLE_DATA_NUM_EPICS", (4, 8))
NUM_USS_EPICS = getattr(settings, "SAMPLE_DATA_NUM_USS_EPICS", (2, 6))
NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7))
NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5))
NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4))
@ -255,6 +258,11 @@ class Command(BaseCommand):
if self.sd.boolean():
self.create_wiki_page(project, wiki_link.href)
# create epics
for y in range(self.sd.int(*NUM_EPICS)):
epic = self.create_epic(project)
project.refresh_from_db()
@ -494,6 +502,59 @@ class Command(BaseCommand):
return milestone
def create_epic(self, project):
epic = Epic.objects.create(subject=self.sd.choice(SUBJECT_CHOICES),
project=project,
owner=self.sd.db_object_from_queryset(
project.memberships.filter(user__isnull=False)).user,
description=self.sd.paragraph(),
status=self.sd.db_object_from_queryset(project.epic_statuses.filter(
is_closed=False)),
tags=self.sd.words(1, 3).split(" "))
# 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()
for i in range(self.sd.int(*NUM_ATTACHMENTS)):
attachment = self.create_attachment(epic, i+1)
if self.sd.choice([True, True, False, True, True]):
epic.assigned_to = self.sd.db_object_from_queryset(project.memberships.filter(
user__isnull=False)).user
epic.save()
# TODO: Epic history
#take_snapshot(epic,
# comment=self.sd.paragraph(),
# user=epic.owner)
#
# Add history entry
#epic.status=self.sd.db_object_from_queryset(project.epic_statuses.filter(is_closed=False))
#epic.save()
#take_snapshot(epic,
# comment=self.sd.paragraph(),
# user=epic.owner)
# TODO: Epic voters
#self.create_votes(epic)
# TODO: Epic watchers
#self.create_watchers(epic)
if self.sd.choice([True, True, False, True, True]):
filters = {}
if self.sd.choice([True, True, False, True, True]):
filters = {"project": epic.project}
n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS))))
epic.user_stories.add(*UserStory.objects.filter(**filters).order_by("?")[:n])
return epic
def create_project(self, counter, is_private=None, blocked_code=None):
if is_private is None:
is_private=self.sd.boolean()

View File

@ -110,6 +110,7 @@ class Migration(migrations.Migration):
dependencies = [
('projects', '0029_project_is_looking_for_people'),
('likes', '0001_initial'),
('timeline', '0004_auto_20150603_1312'),
('likes', '0001_initial'),
]

View File

@ -9,6 +9,9 @@ class Migration(migrations.Migration):
dependencies = [
('projects', '0045_merge'),
('userstories', '0011_userstory_tribe_gig'),
('tasks', '0009_auto_20151104_1131'),
('issues', '0006_remove_issue_watchers'),
]
operations = [

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-06-29 14:43
from __future__ import unicode_literals
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import django_pgjson.fields
class Migration(migrations.Migration):
dependencies = [
('projects', '0048_auto_20160615_1508'),
]
operations = [
migrations.CreateModel(
name='EpicStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('slug', models.SlugField(blank=True, max_length=255, verbose_name='slug')),
('order', models.IntegerField(default=10, verbose_name='order')),
('is_closed', models.BooleanField(default=False, verbose_name='is closed')),
('color', models.CharField(default='#999999', max_length=20, verbose_name='color')),
],
options={
'verbose_name_plural': 'epic statuses',
'ordering': ['project', 'order', 'name'],
'verbose_name': 'epic status',
},
),
migrations.AlterModelOptions(
name='issuestatus',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue status', 'verbose_name_plural': 'issue statuses'},
),
migrations.AlterModelOptions(
name='issuetype',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue type', 'verbose_name_plural': 'issue types'},
),
migrations.AlterModelOptions(
name='membership',
options={'ordering': ['project', 'user__full_name', 'user__username', 'user__email', 'email'], 'verbose_name': 'membership', 'verbose_name_plural': 'memberships'},
),
migrations.AlterModelOptions(
name='points',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'points', 'verbose_name_plural': 'points'},
),
migrations.AlterModelOptions(
name='priority',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'priority', 'verbose_name_plural': 'priorities'},
),
migrations.AlterModelOptions(
name='severity',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'severity', 'verbose_name_plural': 'severities'},
),
migrations.AlterModelOptions(
name='taskstatus',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'task status', 'verbose_name_plural': 'task statuses'},
),
migrations.AlterModelOptions(
name='userstorystatus',
options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'user story status', 'verbose_name_plural': 'user story statuses'},
),
migrations.AddField(
model_name='project',
name='is_epics_activated',
field=models.BooleanField(default=True, verbose_name='active epics panel'),
),
migrations.AddField(
model_name='projecttemplate',
name='epic_statuses',
field=django_pgjson.fields.JsonField(blank=True, null=True, verbose_name='epic statuses'),
),
migrations.AddField(
model_name='projecttemplate',
name='is_epics_activated',
field=models.BooleanField(default=True, verbose_name='active epics panel'),
),
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_epic', 'View epic'), ('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_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('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.AddField(
model_name='epicstatus',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epic_statuses', to='projects.Project', verbose_name='project'),
),
migrations.AddField(
model_name='project',
name='default_epic_status',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='projects.EpicStatus', verbose_name='default epic status'),
),
migrations.AlterUniqueTogether(
name='epicstatus',
unique_together=set([('project', 'slug'), ('project', 'name')]),
),
]

View File

@ -102,19 +102,20 @@ class Membership(models.Model):
verbose_name_plural = "memberships"
unique_together = ("user", "project",)
ordering = ["project", "user__full_name", "user__username", "user__email", "email"]
permissions = (
("view_membership", "Can view membership"),
)
class ProjectDefaults(models.Model):
default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL,
related_name="+", null=True, blank=True,
verbose_name=_("default points"))
default_epic_status = models.OneToOneField("projects.EpicStatus",
on_delete=models.SET_NULL, related_name="+",
null=True, blank=True,
verbose_name=_("default epic status"))
default_us_status = models.OneToOneField("projects.UserStoryStatus",
on_delete=models.SET_NULL, related_name="+",
null=True, blank=True,
verbose_name=_("default US status"))
default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL,
related_name="+", null=True, blank=True,
verbose_name=_("default points"))
default_task_status = models.OneToOneField("projects.TaskStatus",
on_delete=models.SET_NULL, related_name="+",
null=True, blank=True,
@ -164,6 +165,8 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
verbose_name=_("total of milestones"))
total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points"))
is_epics_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active backlog panel"))
is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
@ -504,6 +507,39 @@ class ProjectModulesConfig(models.Model):
ordering = ["project"]
# Epic common Models
class EpicStatus(models.Model):
name = models.CharField(max_length=255, null=False, blank=False,
verbose_name=_("name"))
slug = models.SlugField(max_length=255, null=False, blank=True,
verbose_name=_("slug"))
order = models.IntegerField(default=10, null=False, blank=False,
verbose_name=_("order"))
is_closed = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is closed"))
color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
verbose_name=_("color"))
project = models.ForeignKey("Project", null=False, blank=False,
related_name="epic_statuses", verbose_name=_("project"))
class Meta:
verbose_name = "epic status"
verbose_name_plural = "epic statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
def __str__(self):
return self.name
def save(self, *args, **kwargs):
qs = self.project.epic_statuses
if self.id:
qs = qs.exclude(id=self.id)
self.slug = slugify_uniquely_for_queryset(self.name, qs)
return super().save(*args, **kwargs)
# User Stories common Models
class UserStoryStatus(models.Model):
name = models.CharField(max_length=255, null=False, blank=False,
@ -528,9 +564,6 @@ class UserStoryStatus(models.Model):
verbose_name_plural = "user story statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
permissions = (
("view_userstorystatus", "Can view user story status"),
)
def __str__(self):
return self.name
@ -559,9 +592,6 @@ class Points(models.Model):
verbose_name_plural = "points"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
permissions = (
("view_points", "Can view points"),
)
def __str__(self):
return self.name
@ -588,9 +618,6 @@ class TaskStatus(models.Model):
verbose_name_plural = "task statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
permissions = (
("view_taskstatus", "Can view task status"),
)
def __str__(self):
return self.name
@ -621,9 +648,6 @@ class Priority(models.Model):
verbose_name_plural = "priorities"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
permissions = (
("view_priority", "Can view priority"),
)
def __str__(self):
return self.name
@ -644,9 +668,6 @@ class Severity(models.Model):
verbose_name_plural = "severities"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
permissions = (
("view_severity", "Can view severity"),
)
def __str__(self):
return self.name
@ -671,9 +692,6 @@ class IssueStatus(models.Model):
verbose_name_plural = "issue statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
permissions = (
("view_issuestatus", "Can view issue status"),
)
def __str__(self):
return self.name
@ -702,9 +720,6 @@ class IssueType(models.Model):
verbose_name_plural = "issue types"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
permissions = (
("view_issuetype", "Can view issue type"),
)
def __str__(self):
return self.name
@ -728,6 +743,8 @@ class ProjectTemplate(models.Model):
blank=False,
verbose_name=_("default owner's role"))
is_epics_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active backlog panel"))
is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
@ -743,6 +760,7 @@ class ProjectTemplate(models.Model):
verbose_name=_("videoconference extra data"))
default_options = JsonField(null=True, blank=True, verbose_name=_("default options"))
epic_statuses = JsonField(null=True, blank=True, verbose_name=_("epic statuses"))
us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses"))
points = JsonField(null=True, blank=True, verbose_name=_("points"))
task_statuses = JsonField(null=True, blank=True, verbose_name=_("task statuses"))
@ -770,6 +788,7 @@ class ProjectTemplate(models.Model):
super().save(*args, **kwargs)
def load_data_from_project(self, project):
self.is_epics_activated = project.is_epics_activated
self.is_backlog_activated = project.is_backlog_activated
self.is_kanban_activated = project.is_kanban_activated
self.is_wiki_activated = project.is_wiki_activated
@ -779,6 +798,7 @@ class ProjectTemplate(models.Model):
self.default_options = {
"points": getattr(project.default_points, "name", None),
"epic_status": getattr(project.default_epic_status, "name", None),
"us_status": getattr(project.default_us_status, "name", None),
"task_status": getattr(project.default_task_status, "name", None),
"issue_status": getattr(project.default_issue_status, "name", None),
@ -787,6 +807,16 @@ class ProjectTemplate(models.Model):
"severity": getattr(project.default_severity, "name", None)
}
self.epic_statuses = []
for epic_status in project.epic_statuses.all():
self.epic_statuses.append({
"name": epic_status.name,
"slug": epic_status.slug,
"is_closed": epic_status.is_closed,
"color": epic_status.color,
"order": epic_status.order,
})
self.us_statuses = []
for us_status in project.us_statuses.all():
self.us_statuses.append({
@ -874,6 +904,7 @@ class ProjectTemplate(models.Model):
raise Exception("Project need an id (must be a saved project)")
project.creation_template = self
project.is_epics_activated = self.is_epics_activated
project.is_backlog_activated = self.is_backlog_activated
project.is_kanban_activated = self.is_kanban_activated
project.is_wiki_activated = self.is_wiki_activated
@ -881,6 +912,16 @@ class ProjectTemplate(models.Model):
project.videoconferences = self.videoconferences
project.videoconferences_extra_data = self.videoconferences_extra_data
for epic_status in self.epic_statuses:
EpicStatus.objects.create(
name=epic_status["name"],
slug=epic_status["slug"],
is_closed=epic_status["is_closed"],
color=epic_status["color"],
order=epic_status["order"],
project=project
)
for us_status in self.us_statuses:
UserStoryStatus.objects.create(
name=us_status["name"],
@ -955,12 +996,16 @@ class ProjectTemplate(models.Model):
permissions=role['permissions']
)
if self.points:
project.default_points = Points.objects.get(name=self.default_options["points"],
if self.epic_statuses:
project.default_epic_status = EpicStatus.objects.get(name=self.default_options["epic_status"],
project=project)
if self.us_statuses:
project.default_us_status = UserStoryStatus.objects.get(name=self.default_options["us_status"],
project=project)
if self.points:
project.default_points = Points.objects.get(name=self.default_options["points"],
project=project)
if self.task_statuses:
project.default_task_status = TaskStatus.objects.get(name=self.default_options["task_status"],

View File

@ -109,6 +109,18 @@ class MembershipPermission(TaigaResourcePermission):
resend_invitation_perms = IsProjectAdmin()
# Epics
class EpicStatusPermission(TaigaResourcePermission):
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()
# User Stories
class PointsPermission(TaigaResourcePermission):

View File

@ -37,11 +37,13 @@ from .notifications.choices import NotifyLevel
# Custom values for selectors
######################################################
class PointsSerializer(serializers.LightSerializer):
class EpicStatusSerializer(serializers.LightSerializer):
id = Field()
name = I18NField()
slug = Field()
order = Field()
value = Field()
is_closed = Field()
color = Field()
project = Field(attr="project_id")
@ -57,6 +59,14 @@ class UserStoryStatusSerializer(serializers.LightSerializer):
project = Field(attr="project_id")
class PointsSerializer(serializers.LightSerializer):
id = Field()
name = I18NField()
order = Field()
value = Field()
project = Field(attr="project_id")
class TaskStatusSerializer(serializers.LightSerializer):
id = Field()
name = I18NField()

View File

@ -19,7 +19,7 @@
# This makes all code that import services works and
# is not the baddest practice ;)
from .bulk_update_order import update_projects_order_in_bulk
from .bulk_update_order import apply_order_updates
from .bulk_update_order import bulk_update_severity_order
from .bulk_update_order import bulk_update_priority_order
from .bulk_update_order import bulk_update_issue_type_order
@ -27,7 +27,8 @@ from .bulk_update_order import bulk_update_issue_status_order
from .bulk_update_order import bulk_update_task_status_order
from .bulk_update_order import bulk_update_points_order
from .bulk_update_order import bulk_update_userstory_status_order
from .bulk_update_order import apply_order_updates
from .bulk_update_order import bulk_update_epic_status_order
from .bulk_update_order import update_projects_order_in_bulk
from .filters import get_all_tags

View File

@ -86,6 +86,23 @@ def update_projects_order_in_bulk(bulk_data: list, field: str, user):
db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership)
@transaction.atomic
def bulk_update_epic_status_order(project, user, data):
cursor = connection.cursor()
sql = """
prepare bulk_update_order as update projects_epicstatus set "order" = $1
where projects_epicstatus.id = $2 and
projects_epicstatus.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_status_order(project, user, data):
cursor = connection.cursor()

View File

@ -117,6 +117,26 @@ def attach_notify_policies(queryset, as_field="notify_policies_attr"):
return queryset
def attach_epic_statuses(queryset, as_field="epic_statuses_attr"):
"""Attach a json epic statuses representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the epic statuses 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(projects_epicstatus))
FROM projects_epicstatus
WHERE
projects_epicstatus.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"):
"""Attach a json userstory statuses representation to each object of the queryset.
@ -443,6 +463,7 @@ def attach_extra_info(queryset, user=None):
queryset = attach_members(queryset)
queryset = attach_closed_milestones(queryset)
queryset = attach_notify_policies(queryset)
queryset = attach_epic_statuses(queryset)
queryset = attach_userstory_statuses(queryset)
queryset = attach_points(queryset)
queryset = attach_task_statuses(queryset)

View File

@ -86,9 +86,9 @@ class TaskStatusExistsValidator:
# Custom values for selectors
######################################################
class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class EpicStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Points
model = models.EpicStatus
class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
@ -96,6 +96,11 @@ class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.Mode
model = models.UserStoryStatus
class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Points
class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.TaskStatus

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-06-29 14:43
from __future__ import unicode_literals
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0021_auto_20160614_1201'),
]
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_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('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'),
),
]

View File

@ -229,22 +229,17 @@ class StorageEntryFactory(Factory):
value = factory.Sequence(lambda n: {"value": "value-{}".format(n)})
class UserStoryStatusFactory(Factory):
class EpicFactory(Factory):
class Meta:
model = "projects.UserStoryStatus"
model = "epics.Epic"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "User Story status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class TaskStatusFactory(Factory):
class Meta:
model = "projects.TaskStatus"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Task status {}".format(n))
ref = factory.Sequence(lambda n: n)
project = factory.SubFactory("tests.factories.ProjectFactory")
owner = factory.SubFactory("tests.factories.UserFactory")
subject = factory.Sequence(lambda n: "User Story {}".format(n))
description = factory.Sequence(lambda n: "User Story {} description".format(n))
status = factory.SubFactory("tests.factories.EpicStatusFactory")
class MilestoneFactory(Factory):
@ -330,6 +325,33 @@ class WikiLinkFactory(Factory):
order = factory.Sequence(lambda n: n)
class EpicStatusFactory(Factory):
class Meta:
model = "projects.EpicStatus"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Epic status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class UserStoryStatusFactory(Factory):
class Meta:
model = "projects.UserStoryStatus"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "User Story status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class TaskStatusFactory(Factory):
class Meta:
model = "projects.TaskStatus"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Task status {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueStatusFactory(Factory):
class Meta:
model = "projects.IssueStatus"

View File

@ -115,6 +115,11 @@ def data():
user=m.project_owner,
is_admin=True)
m.public_epic_status = f.EpicStatusFactory(project=m.public_project)
m.private_epic_status1 = f.EpicStatusFactory(project=m.private_project1)
m.private_epic_status2 = f.EpicStatusFactory(project=m.private_project2)
m.blocked_epic_status = f.EpicStatusFactory(project=m.blocked_project)
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
m.private_points2 = f.PointsFactory(project=m.private_project2)
@ -155,6 +160,10 @@ def data():
return m
#####################################################
# Roles
#####################################################
def test_roles_retrieve(client, data):
public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk})
private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk})
@ -299,6 +308,198 @@ def test_roles_patch(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Epic Status
#####################################################
def test_epic_status_retrieve(client, data):
public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk})
private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk})
private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk})
blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'get', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
results = helper_test_http_method(client, 'get', blocked_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_epic_status_update(client, data):
public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk})
private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk})
private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk})
blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
epic_status_data = serializers.EpicStatusSerializer(data.public_epic_status).data
epic_status_data["name"] = "test"
epic_status_data = json.dumps(epic_status_data)
results = helper_test_http_method(client, 'put', public_url, epic_status_data, users)
assert results == [401, 403, 403, 403, 200]
epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status1).data
epic_status_data["name"] = "test"
epic_status_data = json.dumps(epic_status_data)
results = helper_test_http_method(client, 'put', private1_url, epic_status_data, users)
assert results == [401, 403, 403, 403, 200]
epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status2).data
epic_status_data["name"] = "test"
epic_status_data = json.dumps(epic_status_data)
results = helper_test_http_method(client, 'put', private2_url, epic_status_data, users)
assert results == [401, 403, 403, 403, 200]
epic_status_data = serializers.EpicStatusSerializer(data.blocked_epic_status).data
epic_status_data["name"] = "test"
epic_status_data = json.dumps(epic_status_data)
results = helper_test_http_method(client, 'put', blocked_url, epic_status_data, users)
assert results == [401, 403, 403, 403, 451]
def test_epic_status_delete(client, data):
public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk})
private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk})
private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk})
blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'delete', public_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private1_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', blocked_url, None, users)
assert results == [401, 403, 403, 403, 451]
def test_epic_status_list(client, data):
url = reverse('epic-statuses-list')
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
assert len(projects_data) == 2
assert response.status_code == 200
client.login(data.registered_user)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
assert len(projects_data) == 2
assert response.status_code == 200
client.login(data.project_member_without_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
assert len(projects_data) == 2
assert response.status_code == 200
client.login(data.project_member_with_perms)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
assert len(projects_data) == 4
assert response.status_code == 200
client.login(data.project_owner)
response = client.get(url)
projects_data = json.loads(response.content.decode('utf-8'))
assert len(projects_data) == 4
assert response.status_code == 200
def test_epic_status_patch(client, data):
public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk})
private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk})
private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk})
blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 451]
def test_epic_status_action_bulk_update_order(client, data):
url = reverse('epic-statuses-bulk-update-order')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
post_data = json.dumps({
"bulk_epic_statuses": [(1, 2)],
"project": data.public_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_epic_statuses": [(1, 2)],
"project": data.private_project1.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_epic_statuses": [(1, 2)],
"project": data.private_project2.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_epic_statuses": [(1, 2)],
"project": data.blocked_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 451]
#####################################################
# Points
#####################################################
def test_points_retrieve(client, data):
public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk})
private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk})
@ -483,6 +684,10 @@ def test_points_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# User Story Status
#####################################################
def test_user_story_status_retrieve(client, data):
public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk})
private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk})
@ -570,7 +775,6 @@ def test_user_story_status_delete(client, data):
assert results == [401, 403, 403, 403, 451]
def test_user_story_status_list(client, data):
url = reverse('userstory-statuses-list')
@ -668,6 +872,10 @@ def test_user_story_status_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Task Status
#####################################################
def test_task_status_retrieve(client, data):
public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk})
private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk})
@ -755,7 +963,6 @@ def test_task_status_delete(client, data):
assert results == [401, 403, 403, 403, 451]
def test_task_status_list(client, data):
url = reverse('task-statuses-list')
@ -853,6 +1060,10 @@ def test_task_status_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Issue Status
#####################################################
def test_issue_status_retrieve(client, data):
public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk})
private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk})
@ -1037,6 +1248,10 @@ def test_issue_status_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Issue Type
#####################################################
def test_issue_type_retrieve(client, data):
public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk})
private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk})
@ -1221,6 +1436,10 @@ def test_issue_type_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Priority
#####################################################
def test_priority_retrieve(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
@ -1283,6 +1502,7 @@ def test_priority_update(client, data):
results = helper_test_http_method(client, 'put', blocked_url, priority_data, users)
assert results == [401, 403, 403, 403, 451]
def test_priority_delete(client, data):
public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk})
private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk})
@ -1404,6 +1624,10 @@ def test_priority_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Severity
#####################################################
def test_severity_retrieve(client, data):
public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk})
private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk})
@ -1588,6 +1812,10 @@ def test_severity_action_bulk_update_order(client, data):
assert results == [401, 403, 403, 403, 451]
#####################################################
# Memberships
#####################################################
def test_membership_retrieve(client, data):
public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk})
private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk})
@ -1859,6 +2087,10 @@ def test_membership_action_resend_invitation(client, data):
assert results == [404, 404, 404, 403, 451]
#####################################################
# Project Templates
#####################################################
def test_project_template_retrieve(client, data):
url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk})
@ -1935,6 +2167,10 @@ def test_project_template_patch(client, data):
assert results == [401, 403, 200]
#####################################################
# Tags
#####################################################
def test_create_tag(client, data):
users = [
None,