Import export including epics

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-08-01 15:10:11 +02:00 committed by David Barragán Merino
parent 0ff7ce8975
commit 9d0e0180ef
9 changed files with 319 additions and 23 deletions

View File

@ -23,7 +23,7 @@ _cache_user_by_email = {}
_custom_tasks_attributes_cache = {}
_custom_issues_attributes_cache = {}
_custom_userstories_attributes_cache = {}
_custom_epics_attributes_cache = {}
def cached_get_user_by_pk(pk):
if pk not in _cache_user_by_pk:

View File

@ -29,6 +29,7 @@ from .mixins import (HistoryExportSerializerMixin,
WatcheableObjectLightSerializerMixin)
from .cache import (_custom_tasks_attributes_cache,
_custom_userstories_attributes_cache,
_custom_epics_attributes_cache,
_custom_issues_attributes_cache)
@ -55,6 +56,14 @@ class UserStoryStatusExportSerializer(RelatedExportSerializer):
wip_limit = Field()
class EpicStatusExportSerializer(RelatedExportSerializer):
name = Field()
slug = Field()
order = Field()
is_closed = Field()
color = Field()
class TaskStatusExportSerializer(RelatedExportSerializer):
name = Field()
slug = Field()
@ -97,6 +106,15 @@ class RoleExportSerializer(RelatedExportSerializer):
permissions = Field()
class EpicCustomAttributesExportSerializer(RelatedExportSerializer):
name = Field()
description = Field()
type = Field()
order = Field()
created_date = DateTimeField()
modified_date = DateTimeField()
class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer):
name = Field()
description = Field()
@ -238,6 +256,45 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
return _custom_userstories_attributes_cache[project.id]
class EpicRelatedUserStoryExportSerializer(RelatedExportSerializer):
user_story = SlugRelatedField(slug_field="ref")
order = Field()
class EpicExportSerializer(CustomAttributesValuesExportSerializerMixin,
HistoryExportSerializerMixin,
AttachmentExportSerializerMixin,
WatcheableObjectLightSerializerMixin,
RelatedExportSerializer):
ref = Field()
owner = UserRelatedField()
status = SlugRelatedField(slug_field="name")
epics_order = Field()
created_date = DateTimeField()
modified_date = DateTimeField()
subject = Field()
description = Field()
color = Field()
assigned_to = UserRelatedField()
client_requirement = Field()
team_requirement = Field()
version = Field()
blocked_note = Field()
is_blocked = Field()
tags = Field()
related_user_stories = MethodField()
def get_related_user_stories(self, obj):
return EpicRelatedUserStoryExportSerializer(obj.relateduserstory_set.all(), many=True).data
def custom_attributes_queryset(self, project):
if project.id not in _custom_epics_attributes_cache:
_custom_epics_attributes_cache[project.id] = list(
project.userstorycustomattributes.all().values('id', 'name')
)
return _custom_epics_attributes_cache[project.id]
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
HistoryExportSerializerMixin,
AttachmentExportSerializerMixin,
@ -307,6 +364,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
logo = FileField()
total_milestones = Field()
total_story_points = Field()
is_epics_activated = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()
is_wiki_activated = Field()
@ -318,6 +376,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
is_featured = Field()
is_looking_for_people = Field()
looking_for_people_note = Field()
epics_csv_uuid = Field()
userstories_csv_uuid = Field()
tasks_csv_uuid = Field()
issues_csv_uuid = Field()
@ -339,6 +398,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
owner = UserRelatedField()
memberships = MembershipExportSerializer(many=True)
points = PointsExportSerializer(many=True)
epic_statuses = EpicStatusExportSerializer(many=True)
us_statuses = UserStoryStatusExportSerializer(many=True)
task_statuses = TaskStatusExportSerializer(many=True)
issue_types = IssueTypeExportSerializer(many=True)
@ -347,15 +407,18 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
severities = SeverityExportSerializer(many=True)
tags_colors = Field()
default_points = SlugRelatedField(slug_field="name")
default_epic_status = SlugRelatedField(slug_field="name")
default_us_status = SlugRelatedField(slug_field="name")
default_task_status = SlugRelatedField(slug_field="name")
default_priority = SlugRelatedField(slug_field="name")
default_severity = SlugRelatedField(slug_field="name")
default_issue_status = SlugRelatedField(slug_field="name")
default_issue_type = SlugRelatedField(slug_field="name")
epiccustomattributes = EpicCustomAttributesExportSerializer(many=True)
userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True)
taskcustomattributes = TaskCustomAttributeExportSerializer(many=True)
issuecustomattributes = IssueCustomAttributeExportSerializer(many=True)
epics = EpicExportSerializer(many=True)
user_stories = UserStoryExportSerializer(many=True)
tasks = TaskExportSerializer(many=True)
milestones = MilestoneExportSerializer(many=True)

View File

@ -45,12 +45,16 @@ def render_project(project, outfile, chunk_size=8190):
# field.initialize(parent=serializer, field_name=field_name)
# These four "special" fields hava attachments so we use them in a special way
if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]:
if field_name in ["wiki_pages", "user_stories", "tasks", "issues", "epics"]:
value = get_component(project, field_name)
if field_name != "wiki_pages":
value = value.select_related('owner', 'status', 'milestone',
value = value.select_related('owner', 'status',
'project', 'assigned_to',
'custom_attributes_values')
if field_name in ["user_stories", "tasks", "issues"]:
value = value.select_related('milestone')
if field_name == "issues":
value = value.select_related('severity', 'priority', 'type')
value = value.prefetch_related('history_entry', 'attachments')

View File

@ -80,11 +80,17 @@ def store_project(data):
excluded_fields = [
"default_points", "default_us_status", "default_task_status",
"default_priority", "default_severity", "default_issue_status",
"default_issue_type", "memberships", "points", "us_statuses",
"task_statuses", "issue_statuses", "priorities", "severities",
"issue_types", "userstorycustomattributes", "taskcustomattributes",
"issuecustomattributes", "roles", "milestones", "wiki_pages",
"wiki_links", "notify_policies", "user_stories", "issues", "tasks",
"default_issue_type", "default_epic_status",
"memberships", "points",
"epic_statuses", "us_statuses", "task_statuses", "issue_statuses",
"priorities", "severities",
"issue_types",
"epiccustomattributes", "userstorycustomattributes",
"taskcustomattributes", "issuecustomattributes",
"roles", "milestones",
"wiki_pages", "wiki_links",
"notify_policies",
"epics", "user_stories", "issues", "tasks",
"is_featured"
]
if key not in excluded_fields:
@ -195,7 +201,7 @@ def _store_membership(project, membership):
validator.object._importing = True
validator.object.token = str(uuid.uuid1())
validator.object.user = find_invited_user(validator.object.email,
default=validator.object.user)
default=validator.object.user)
validator.save()
return validator
@ -219,6 +225,7 @@ def _store_project_attribute_value(project, data, field, serializer):
validator.object._importing = True
validator.save()
return validator.object
add_errors(field, validator.errors)
return None
@ -239,10 +246,10 @@ def store_default_project_attributes_values(project, data):
else:
value = related.all().first()
setattr(project, field, value)
helper(project, "default_points", project.points, data)
helper(project, "default_issue_type", project.issue_types, data)
helper(project, "default_issue_status", project.issue_statuses, data)
helper(project, "default_epic_status", project.epic_statuses, data)
helper(project, "default_us_status", project.us_statuses, data)
helper(project, "default_task_status", project.task_statuses, data)
helper(project, "default_priority", project.priorities, data)
@ -317,12 +324,14 @@ def _store_role_point(project, us, role_point):
add_errors("role_points", validator.errors)
return None
def store_user_story(project, data):
if "status" not in data and project.default_us_status:
data["status"] = project.default_us_status.name
us_data = {key: value for key, value in data.items() if key not in
["role_points", "custom_attributes_values"]}
["role_points", "custom_attributes_values"]}
validator = validators.UserStoryExportValidator(data=us_data, context={"project": project})
if validator.is_valid():
@ -360,10 +369,13 @@ def store_user_story(project, data):
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = validator.object.project.userstorycustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values)
custom_attributes_values = \
_use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
_store_custom_attributes_values(validator.object, custom_attributes_values,
"user_story", validators.UserStoryCustomAttributesValuesExportValidator)
"user_story",
validators.UserStoryCustomAttributesValuesExportValidator)
return validator
@ -379,6 +391,81 @@ def store_user_stories(project, data):
return results
## EPICS
def _store_epic_related_user_story(project, epic, related_user_story):
validator = validators.EpicRelatedUserStoryExportValidator(data=related_user_story,
context={"project": project})
if validator.is_valid():
validator.object.epic = epic
validator.object.save()
return validator.object
add_errors("epic_related_user_stories", validator.errors)
return None
def store_epic(project, data):
if "status" not in data and project.default_epic_status:
data["status"] = project.default_epic_status.name
validator = validators.EpicExportValidator(data=data, context={"project": project})
if validator.is_valid():
validator.object.project = project
if validator.object.owner is None:
validator.object.owner = validator.object.project.owner
validator.object._importing = True
validator.object._not_notify = True
validator.save()
validator.save_watchers()
if validator.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
seq.set_max(sequence_name, validator.object.ref)
else:
validator.object.ref, _ = refs.make_reference(validator.object, project)
validator.object.save()
for epic_attachment in data.get("attachments", []):
_store_attachment(project, validator.object, epic_attachment)
for related_user_story in data.get("related_user_stories", []):
_store_epic_related_user_story(project, validator.object, related_user_story)
history_entries = data.get("history", [])
for history in history_entries:
_store_history(project, validator.object, history)
if not history_entries:
take_snapshot(validator.object, user=validator.object.owner)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = validator.object.project.epiccustomattributes.all().values('id', 'name')
custom_attributes_values = \
_use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
_store_custom_attributes_values(validator.object, custom_attributes_values,
"epic",
validators.EpicCustomAttributesValuesExportValidator)
return validator
add_errors("epics", validator.errors)
return None
def store_epics(project, data):
results = []
for epic in data.get("epics", []):
epic = store_epic(project, epic)
results.append(epic)
return results
## TASKS
def store_task(project, data):
@ -418,10 +505,13 @@ def store_task(project, data):
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = validator.object.project.taskcustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values)
custom_attributes_values = \
_use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
_store_custom_attributes_values(validator.object, custom_attributes_values,
"task", validators.TaskCustomAttributesValuesExportValidator)
"task",
validators.TaskCustomAttributesValuesExportValidator)
return validator
@ -486,10 +576,12 @@ def store_issue(project, data):
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = validator.object.project.issuecustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values)
custom_attributes_values = \
_use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
_store_custom_attributes_values(validator.object, custom_attributes_values,
"issue", validators.IssueCustomAttributesValuesExportValidator)
"issue",
validators.IssueCustomAttributesValuesExportValidator)
return validator
@ -605,8 +697,9 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data):
is_private = data.get("is_private", False)
total_memberships = len([m for m in data.get("memberships", [])
if m.get("email", None) != data["owner"]])
total_memberships = total_memberships + 1 # 1 is the owner
if m.get("email", None) != data["owner"]])
total_memberships = total_memberships + 1 # 1 is the owner
(enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
owner,
is_private,
@ -652,9 +745,10 @@ def _populate_project_object(project, data):
# Create memberships
store_memberships(project, data)
_create_membership_for_project_owner(project)
check_if_there_is_some_error(_("error importing memberships"), project)
check_if_there_is_some_error(_("error importing memberships"), project)
# Create project attributes values
store_project_attributes_values(project, data, "epic_statuses", validators.EpicStatusExportValidator)
store_project_attributes_values(project, data, "us_statuses", validators.UserStoryStatusExportValidator)
store_project_attributes_values(project, data, "points", validators.PointsExportValidator)
store_project_attributes_values(project, data, "task_statuses", validators.TaskStatusExportValidator)
@ -669,6 +763,8 @@ def _populate_project_object(project, data):
check_if_there_is_some_error(_("error importing default project attributes values"), project)
# Create custom attributes
store_custom_attributes(project, data, "epiccustomattributes",
validators.EpicCustomAttributeExportValidator)
store_custom_attributes(project, data, "userstorycustomattributes",
validators.UserStoryCustomAttributeExportValidator)
store_custom_attributes(project, data, "taskcustomattributes",
@ -689,6 +785,10 @@ def _populate_project_object(project, data):
store_user_stories(project, data)
check_if_there_is_some_error(_("error importing user stories"), project)
# Creat epics
store_epics(project, data)
check_if_there_is_some_error(_("error importing epics"), project)
# Createer tasks
store_tasks(project, data)
check_if_there_is_some_error(_("error importing tasks"), project)

View File

@ -1,4 +1,5 @@
from .validators import PointsExportValidator
from .validators import EpicStatusExportValidator
from .validators import UserStoryStatusExportValidator
from .validators import TaskStatusExportValidator
from .validators import IssueStatusExportValidator
@ -6,6 +7,7 @@ from .validators import PriorityExportValidator
from .validators import SeverityExportValidator
from .validators import IssueTypeExportValidator
from .validators import RoleExportValidator
from .validators import EpicCustomAttributeExportValidator
from .validators import UserStoryCustomAttributeExportValidator
from .validators import TaskCustomAttributeExportValidator
from .validators import IssueCustomAttributeExportValidator
@ -17,6 +19,8 @@ from .validators import MembershipExportValidator
from .validators import RolePointsExportValidator
from .validators import MilestoneExportValidator
from .validators import TaskExportValidator
from .validators import EpicRelatedUserStoryExportValidator
from .validators import EpicExportValidator
from .validators import UserStoryExportValidator
from .validators import IssueExportValidator
from .validators import WikiPageExportValidator

View File

@ -22,6 +22,7 @@ _cache_user_by_pk = {}
_cache_user_by_email = {}
_custom_tasks_attributes_cache = {}
_custom_issues_attributes_cache = {}
_custom_epics_attributes_cache = {}
_custom_userstories_attributes_cache = {}

View File

@ -25,6 +25,7 @@ from taiga.base.exceptions import ValidationError
from taiga.projects import models as projects_models
from taiga.projects.custom_attributes import models as custom_attributes_models
from taiga.projects.epics import models as epics_models
from taiga.projects.userstories import models as userstories_models
from taiga.projects.tasks import models as tasks_models
from taiga.projects.issues import models as issues_models
@ -38,6 +39,7 @@ from .fields import (FileField, UserRelatedField,
TimelineDataField, ContentTypeField)
from .mixins import WatcheableObjectModelValidatorMixin
from .cache import (_custom_tasks_attributes_cache,
_custom_epics_attributes_cache,
_custom_userstories_attributes_cache,
_custom_issues_attributes_cache)
@ -48,6 +50,12 @@ class PointsExportValidator(validators.ModelValidator):
exclude = ('id', 'project')
class EpicStatusExportValidator(validators.ModelValidator):
class Meta:
model = projects_models.EpicStatus
exclude = ('id', 'project')
class UserStoryStatusExportValidator(validators.ModelValidator):
class Meta:
model = projects_models.UserStoryStatus
@ -92,6 +100,14 @@ class RoleExportValidator(validators.ModelValidator):
exclude = ('id', 'project')
class EpicCustomAttributeExportValidator(validators.ModelValidator):
modified_date = serializers.DateTimeField(required=False)
class Meta:
model = custom_attributes_models.EpicCustomAttribute
exclude = ('id', 'project')
class UserStoryCustomAttributeExportValidator(validators.ModelValidator):
modified_date = serializers.DateTimeField(required=False)
@ -151,6 +167,15 @@ class BaseCustomAttributesValuesExportValidator(validators.ModelValidator):
return attrs
class EpicCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
_custom_attribute_model = custom_attributes_models.EpicCustomAttribute
_container_model = "epics.Epic"
_container_field = "epic"
class Meta(BaseCustomAttributesValuesExportValidator.Meta):
model = custom_attributes_models.EpicCustomAttributesValues
class UserStoryCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
_container_model = "userstories.UserStory"
@ -244,6 +269,34 @@ class TaskExportValidator(WatcheableObjectModelValidatorMixin):
return _custom_tasks_attributes_cache[project.id]
class EpicRelatedUserStoryExportValidator(validators.ModelValidator):
user_story = ProjectRelatedField(slug_field="ref")
order = serializers.IntegerField()
class Meta:
model = epics_models.RelatedUserStory
exclude = ('id', 'epic')
class EpicExportValidator(WatcheableObjectModelValidatorMixin):
owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
modified_date = serializers.DateTimeField(required=False)
user_stories = EpicRelatedUserStoryExportValidator(many=True, required=False)
class Meta:
model = epics_models.Epic
exclude = ('id', 'project')
def custom_attributes_queryset(self, project):
if project.id not in _custom_epics_attributes_cache:
_custom_epics_attributes_cache[project.id] = list(
project.epiccustomattributes.all().values('id', 'name')
)
return _custom_epics_attributes_cache[project.id]
class UserStoryExportValidator(WatcheableObjectModelValidatorMixin):
role_points = RolePointsExportValidator(many=True, required=False)
owner = UserRelatedField(required=False)

View File

@ -42,3 +42,17 @@ def test_export_user_story_finish_date(client):
project_data = json.loads(output.getvalue())
finish_date = project_data["user_stories"][0]["finish_date"]
assert finish_date == "2014-10-22T00:00:00+0000"
def test_export_epic_with_user_stories(client):
epic = f.EpicFactory.create(subject="test epic export")
user_story = f.UserStoryFactory.create(project=epic.project)
f.RelatedUserStory.create(epic=epic, user_story=user_story)
output = io.BytesIO()
render_project(user_story.project, output)
project_data = json.loads(output.getvalue())
assert project_data["epics"][0]["subject"] == "test epic export"
assert len(project_data["epics"]) == 1
assert project_data["epics"][0]["related_user_stories"][0]["user_story"] == user_story.ref
assert len(project_data["epics"][0]["related_user_stories"]) == 1

57
tests/unit/test_import.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
import io
from .. import factories as f
from taiga.base.utils import json
from taiga.export_import.services import render_project, store_project_from_dict
pytestmark = pytest.mark.django_db
def test_import_epic_with_user_stories(client):
project = f.ProjectFactory()
project.default_points = f.PointsFactory.create(project=project)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_epic_status = f.EpicStatusFactory.create(project=project)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.default_priority = f.PriorityFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
epic = f.EpicFactory.create(subject="test epic export", project=project, status=project.default_epic_status)
user_story = f.UserStoryFactory.create(project=project, status=project.default_us_status, milestone=None)
f.RelatedUserStory.create(epic=epic, user_story=user_story, order=55)
output = io.BytesIO()
render_project(user_story.project, output)
project_data = json.loads(output.getvalue())
epic.project.delete()
project = store_project_from_dict(project_data)
assert project.epics.count() == 1
assert project.epics.first().ref == epic.ref
assert project.epics.first().user_stories.count() == 1
related_userstory = project.epics.first().relateduserstory_set.first()
assert related_userstory.user_story.ref == user_story.ref
assert related_userstory.order == 55
assert related_userstory.epic.ref == epic.ref