From ebc3388d03b4fb63902f9c493b502c390e416f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 5 Feb 2015 12:30:13 +0100 Subject: [PATCH] US #55: Custom fields - Import/Export custom attributes --- taiga/export_import/api.py | 15 +++++++ taiga/export_import/dump_service.py | 10 +++++ taiga/export_import/serializers.py | 53 ++++++++++++++++++++----- taiga/export_import/service.py | 25 ++++++++++-- tests/integration/test_importer_api.py | 55 ++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 12 deletions(-) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index f05993df..8ddb4c15 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -127,6 +127,21 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi "severities" in data): service.store_default_choices(project_serialized.object, data) + if "userstorycustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "userstorycustomattributes", + serializers.UserStoryCustomAttributeExportSerializer) + + if "taskcustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "taskcustomattributes", + serializers.TaskCustomAttributeExportSerializer) + + if "issuecustomattributes" in data: + service.store_custom_attributes(project_serialized.object, data, + "issuecustomattributes", + serializers.IssueCustomAttributeExportSerializer) + if "roles" in data: service.store_roles(project_serialized.object, data) diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 09a9f0d9..2ef615a1 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -103,6 +103,16 @@ def dict_to_project(data, owner=None): if service.get_errors(clear=False): raise TaigaImportError('error importing default choices') + service.store_custom_attributes(proj, data, "userstorycustomattributes", + serializers.UserStoryCustomAttributeExportSerializer) + service.store_custom_attributes(proj, data, "taskcustomattributes", + serializers.TaskCustomAttributeExportSerializer) + service.store_custom_attributes(proj, data, "issuecustomattributes", + serializers.IssueCustomAttributeExportSerializer) + + if service.get_errors(clear=False): + raise TaigaImportError('error importing custom attributes') + service.store_roles(proj, data) if service.get_errors(clear=False): diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 8f0e0534..2893537b 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -20,11 +20,13 @@ from collections import OrderedDict from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError from rest_framework import serializers from taiga.projects import models as projects_models +from taiga.projects.custom_attributes import models as custom_attributes_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 @@ -81,14 +83,15 @@ class RelatedNoneSafeField(serializers.RelatedField): return value = self.get_default_value() + key = self.source or field_name if value in self.null_values: if self.required: raise ValidationError(self.error_messages['required']) - into[(self.source or field_name)] = None + into[key] = None elif self.many: - into[(self.source or field_name)] = [self.from_native(item) for item in value if self.from_native(item) is not None] + into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] else: - into[(self.source or field_name)] = self.from_native(value) + into[key] = self.from_native(value) class UserRelatedField(RelatedNoneSafeField): @@ -251,7 +254,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer): def get_attachments(self, obj): content_type = ContentType.objects.get_for_model(obj.__class__) - attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, content_type=content_type) + attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, + content_type=content_type) return AttachmentExportSerializer(attachments_qs, many=True).data @@ -305,6 +309,30 @@ class RoleExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') +class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.UserStoryCustomAttribute + exclude = ('id', 'project') + + +class TaskCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.TaskCustomAttribute + exclude = ('id', 'project') + + +class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.IssueCustomAttribute + exclude = ('id', 'project') + + class MembershipExportSerializer(serializers.ModelSerializer): user = UserRelatedField(required=False) role = ProjectRelatedField(slug_field="name") @@ -354,7 +382,8 @@ class MilestoneExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) @@ -368,7 +397,8 @@ class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSeriali exclude = ('id', 'project') -class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) @@ -383,7 +413,8 @@ class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSe exclude = ('id', 'project', 'points', 'tasks') -class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -403,7 +434,8 @@ class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerial exclude = ('id', 'project') -class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer): +class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, + serializers.ModelSerializer): owner = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False) watchers = UserRelatedField(many=True, required=False) @@ -437,6 +469,9 @@ class ProjectExportSerializer(serializers.ModelSerializer): priorities = PriorityExportSerializer(many=True, required=False) severities = SeverityExportSerializer(many=True, required=False) issue_types = IssueTypeExportSerializer(many=True, required=False) + userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) + taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) + issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) roles = RoleExportSerializer(many=True, required=False) milestones = MilestoneExportSerializer(many=True, required=False) wiki_pages = WikiPageExportSerializer(many=True, required=False) diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 797ae81f..0d8aff03 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -57,7 +57,8 @@ def store_project(data): "default_priority", "default_severity", "default_issue_status", "default_issue_type", "memberships", "points", "us_statuses", "task_statuses", "issue_statuses", "priorities", "severities", - "issue_types", "roles", "milestones", "wiki_pages", + "issue_types", "userstorycustomattributes", "taskcustomattributes", + "issuecustomattributes", "roles", "milestones", "wiki_pages", "wiki_links", "notify_policies", "user_stories", "issues", "tasks", ] if key not in excluded_fields: @@ -72,7 +73,7 @@ def store_project(data): return None -def store_choice(project, data, field, serializer): +def _store_choice(project, data, field, serializer): serialized = serializer(data=data) if serialized.is_valid(): serialized.object.project = project @@ -86,7 +87,25 @@ def store_choice(project, data, field, serializer): def store_choices(project, data, field, serializer): result = [] for choice_data in data.get(field, []): - result.append(store_choice(project, choice_data, field, serializer)) + result.append(_store_choice(project, choice_data, field, serializer)) + return result + + +def _store_custom_attribute(project, data, field, serializer): + serialized = serializer(data=data) + if serialized.is_valid(): + serialized.object.project = project + serialized.object._importing = True + serialized.save() + return serialized.object + add_errors(field, serialized.errors) + return None + + +def store_custom_attributes(project, data, field, serializer): + result = [] + for custom_attribute_data in data.get(field, []): + result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer)) return result diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 6236f80f..6231d4a9 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -167,6 +167,61 @@ def test_invalid_project_import_with_extra_data(client): assert Project.objects.filter(slug="imported-project").count() == 0 +def test_valid_project_import_with_custom_attributes(client): + user = f.UserFactory.create() + + url = reverse("importer-list") + data = { + "name": "Imported project", + "description": "Imported project", + "userstorycustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }], + "taskcustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }], + "issuecustomattributes": [{ + "name": "custom attribute example 1", + "description": "short description 1", + "order": 1 + }] + } + + must_empty_children = ["issues", "user_stories", "wiki_pages", "milestones", "wiki_links"] + must_one_instance_children = ["userstorycustomattributes", "taskcustomattributes", "issuecustomattributes"] + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert all(map(lambda x: len(response.data[x]) == 0, must_empty_children)) + # Allwais is created at least the owner membership + assert all(map(lambda x: len(response.data[x]) == 1, must_one_instance_children)) + assert response.data["owner"] == user.email + + +def test_invalid_project_import_with_custom_attributes(client): + user = f.UserFactory.create() + + url = reverse("importer-list") + data = { + "name": "Imported project", + "description": "Imported project", + "userstorycustomattributes": [{ }], + "taskcustomattributes": [{ }], + "issuecustomattributes": [{ }] + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert len(response.data) == 3 + assert Project.objects.filter(slug="imported-project").count() == 0 + + def test_invalid_issue_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user)