From 1b6077c105cb769f6ee2b9f11012f0f1b1cbd442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 7 Jul 2016 09:58:26 +0200 Subject: [PATCH] Migrating export_import api to new serializers/validators --- taiga/base/fields.py | 22 +- taiga/export_import/api.py | 59 +- taiga/export_import/serializers/fields.py | 194 +------ taiga/export_import/serializers/mixins.py | 111 ++-- .../export_import/serializers/serializers.py | 512 +++++++++--------- taiga/export_import/services/render.py | 67 +-- taiga/export_import/services/store.py | 383 ++++++------- taiga/export_import/validators/__init__.py | 27 + taiga/export_import/validators/cache.py | 42 ++ taiga/export_import/validators/fields.py | 196 +++++++ taiga/export_import/validators/mixins.py | 97 ++++ taiga/export_import/validators/validators.py | 349 ++++++++++++ 12 files changed, 1294 insertions(+), 765 deletions(-) create mode 100644 taiga/export_import/validators/__init__.py create mode 100644 taiga/export_import/validators/cache.py create mode 100644 taiga/export_import/validators/fields.py create mode 100644 taiga/export_import/validators/mixins.py create mode 100644 taiga/export_import/validators/validators.py diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 30be6b60..3b19f15f 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -18,7 +18,8 @@ from django.forms import widgets from django.utils.translation import ugettext as _ -from taiga.base.api import serializers +from taiga.base.api import serializers, ISO_8601 +from taiga.base.api.settings import api_settings import serpy @@ -128,4 +129,21 @@ class I18NJsonField(Field): class FileField(Field): def to_value(self, value): - return value.name + if value: + return value.name + return None + + +class DateTimeField(Field): + format = api_settings.DATETIME_FORMAT + + def to_value(self, value): + if value is None or self.format is None: + return value + + if self.format.lower() == ISO_8601: + ret = value.isoformat() + if ret.endswith("+00:00"): + ret = ret[:-6] + "Z" + return ret + return value.strftime(self.format) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index da2af132..75644365 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -44,11 +44,11 @@ from taiga.users import services as users_services from . import exceptions as err from . import mixins from . import permissions +from . import validators from . import serializers from . import services from . import tasks from . import throttling -from .renderers import ExportRenderer from taiga.base.api.utils import get_object_or_404 @@ -102,9 +102,8 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Validate if the project can be imported 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 + 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 (enough_slots, error_message) = users_services.has_available_slot_for_import_new_project( self.request.user, is_private, @@ -147,31 +146,31 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Create project values choicess if "points" in data: services.store.store_project_attributes_values(project_serialized.object, data, - "points", serializers.PointsExportSerializer) + "points", validators.PointsExportValidator) if "issue_types" in data: services.store.store_project_attributes_values(project_serialized.object, data, "issue_types", - serializers.IssueTypeExportSerializer) + validators.IssueTypeExportValidator) if "issue_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "issue_statuses", - serializers.IssueStatusExportSerializer,) + validators.IssueStatusExportValidator,) if "us_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "us_statuses", - serializers.UserStoryStatusExportSerializer,) + validators.UserStoryStatusExportValidator,) if "task_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "task_statuses", - serializers.TaskStatusExportSerializer) + validators.TaskStatusExportValidator) if "priorities" in data: services.store.store_project_attributes_values(project_serialized.object, data, "priorities", - serializers.PriorityExportSerializer) + validators.PriorityExportValidator) if "severities" in data: services.store.store_project_attributes_values(project_serialized.object, data, "severities", - serializers.SeverityExportSerializer) + validators.SeverityExportValidator) if ("points" in data or "issues_types" in data or "issues_statuses" in data or "us_statuses" in data or @@ -183,17 +182,17 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if "userstorycustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "userstorycustomattributes", - serializers.UserStoryCustomAttributeExportSerializer) + validators.UserStoryCustomAttributeExportValidator) if "taskcustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "taskcustomattributes", - serializers.TaskCustomAttributeExportSerializer) + validators.TaskCustomAttributeExportValidator) if "issuecustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "issuecustomattributes", - serializers.IssueCustomAttributeExportSerializer) + validators.IssueCustomAttributeExportValidator) # Is there any error? errors = services.store.get_errors() @@ -201,7 +200,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) # Importer process is OK - response_data = project_serialized.data + response_data = serializers.ProjectExportSerializer(project_serialized.object).data response_data['id'] = project_serialized.object.id headers = self.get_success_headers(response_data) return response.Created(response_data, headers=headers) @@ -218,8 +217,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(milestone.data) - return response.Created(milestone.data, headers=headers) + data = serializers.MilestoneExportSerializer(milestone.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -233,8 +233,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(us.data) - return response.Created(us.data, headers=headers) + data = serializers.UserStoryExportSerializer(us.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -251,8 +252,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(task.data) - return response.Created(task.data, headers=headers) + data = serializers.TaskExportSerializer(task.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -269,8 +271,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(issue.data) - return response.Created(issue.data, headers=headers) + data = serializers.IssueExportSerializer(issue.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -284,8 +287,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(wiki_page.data) - return response.Created(wiki_page.data, headers=headers) + data = serializers.WikiPageExportSerializer(wiki_page.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -299,8 +303,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(wiki_link.data) - return response.Created(wiki_link.data, headers=headers) + data = serializers.WikiLinkExportSerializer(wiki_link.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @list_route(methods=["POST"]) @method_decorator(atomic) diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py index 9ed21a19..29ec85aa 100644 --- a/taiga/export_import/serializers/fields.py +++ b/taiga/export_import/serializers/fields.py @@ -21,24 +21,15 @@ import os import copy from collections import OrderedDict -from django.core.files.base import ContentFile -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import ugettext as _ -from django.contrib.contenttypes.models import ContentType - from taiga.base.api import serializers -from taiga.base.exceptions import ValidationError -from taiga.base.fields import JsonField -from taiga.mdrender.service import render as mdrender +from taiga.base.fields import Field from taiga.users import models as users_models -from .cache import cached_get_user_by_email, cached_get_user_by_pk +from .cache import cached_get_user_by_pk -class FileField(serializers.WritableField): - read_only = False - - def to_native(self, obj): +class FileField(Field): + def to_value(self, obj): if not obj: return None @@ -49,202 +40,74 @@ class FileField(serializers.WritableField): ("name", os.path.basename(obj.name)), ]) - def from_native(self, data): - if not data: - return None - decoded_data = b'' - # The original file was encoded by chunks but we don't really know its - # length or if it was multiple of 3 so we must iterate over all those chunks - # decoding them one by one - for decoding_chunk in data['data'].split("="): - # When encoding to base64 3 bytes are transformed into 4 bytes and - # the extra space of the block is filled with = - # We must ensure that the decoding chunk has a length multiple of 4 so - # we restore the stripped '='s adding appending them until the chunk has - # a length multiple of 4 - decoding_chunk += "=" * (-len(decoding_chunk) % 4) - decoded_data += base64.b64decode(decoding_chunk+"=") - - return ContentFile(decoded_data, name=data['name']) - - -class ContentTypeField(serializers.RelatedField): - read_only = False - - def to_native(self, obj): +class ContentTypeField(Field): + def to_value(self, obj): if obj: return [obj.app_label, obj.model] return None - def from_native(self, data): - try: - return ContentType.objects.get_by_natural_key(*data) - except Exception: - return None - -class RelatedNoneSafeField(serializers.RelatedField): - def field_from_native(self, data, files, field_name, into): - if self.read_only: - return - - try: - if self.many: - try: - # Form data - value = data.getlist(field_name) - if value == [''] or value == []: - raise KeyError - except AttributeError: - # Non-form data - value = data[field_name] - else: - value = data[field_name] - except KeyError: - if self.partial: - 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[key] = None - elif self.many: - into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] - else: - into[key] = self.from_native(value) - - -class UserRelatedField(RelatedNoneSafeField): - read_only = False - - def to_native(self, obj): +class UserRelatedField(Field): + def to_value(self, obj): if obj: return obj.email return None - def from_native(self, data): - try: - return cached_get_user_by_email(data) - except users_models.User.DoesNotExist: - return None - -class UserPkField(serializers.RelatedField): - read_only = False - - def to_native(self, obj): +class UserPkField(Field): + def to_value(self, obj): try: user = cached_get_user_by_pk(obj) return user.email except users_models.User.DoesNotExist: return None - def from_native(self, data): - try: - user = cached_get_user_by_email(data) - return user.pk - except users_models.User.DoesNotExist: - return None - - -class CommentField(serializers.WritableField): - read_only = False - - def field_from_native(self, data, files, field_name, into): - super().field_from_native(data, files, field_name, into) - into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) - - -class ProjectRelatedField(serializers.RelatedField): - read_only = False - null_values = (None, "") +class SlugRelatedField(Field): def __init__(self, slug_field, *args, **kwargs): self.slug_field = slug_field super().__init__(*args, **kwargs) - def to_native(self, obj): + def to_value(self, obj): if obj: return getattr(obj, self.slug_field) return None - def from_native(self, data): - try: - kwargs = {self.slug_field: data, "project": self.context['project']} - return self.queryset.get(**kwargs) - except ObjectDoesNotExist: - raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data))) - -class HistoryUserField(JsonField): - def to_native(self, obj): +class HistoryUserField(Field): + def to_value(self, obj): if obj is None or obj == {}: return [] try: user = cached_get_user_by_pk(obj['pk']) except users_models.User.DoesNotExist: user = None - return (UserRelatedField().to_native(user), obj['name']) - - def from_native(self, data): - if data is None: - return {} - - if len(data) < 2: - return {} - - user = UserRelatedField().from_native(data[0]) - - if user: - pk = user.pk - else: - pk = None - - return {"pk": pk, "name": data[1]} + return (UserRelatedField().to_value(user), obj['name']) -class HistoryValuesField(JsonField): - def to_native(self, obj): +class HistoryValuesField(Field): + def to_value(self, obj): if obj is None: return [] if "users" in obj: - obj['users'] = list(map(UserPkField().to_native, obj['users'])) + obj['users'] = list(map(UserPkField().to_value, obj['users'])) return obj - def from_native(self, data): - if data is None: - return [] - if "users" in data: - data['users'] = list(map(UserPkField().from_native, data['users'])) - return data - -class HistoryDiffField(JsonField): - def to_native(self, obj): +class HistoryDiffField(Field): + def to_value(self, obj): if obj is None: return [] if "assigned_to" in obj: - obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to'])) + obj['assigned_to'] = list(map(UserPkField().to_value, obj['assigned_to'])) return obj - def from_native(self, data): - if data is None: - return [] - if "assigned_to" in data: - data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) - return data - - -class TimelineDataField(serializers.WritableField): - read_only = False - - def to_native(self, data): +class TimelineDataField(Field): + def to_value(self, data): new_data = copy.deepcopy(data) try: user = cached_get_user_by_pk(new_data["user"]["id"]) @@ -253,14 +116,3 @@ class TimelineDataField(serializers.WritableField): except Exception: pass return new_data - - def from_native(self, data): - new_data = copy.deepcopy(data) - try: - user = cached_get_user_by_email(new_data["user"]["email"]) - new_data["user"]["id"] = user.id - del new_data["user"]["email"] - except users_models.User.DoesNotExist: - pass - - return new_data diff --git a/taiga/export_import/serializers/mixins.py b/taiga/export_import/serializers/mixins.py index 007649a2..3006500f 100644 --- a/taiga/export_import/serializers/mixins.py +++ b/taiga/export_import/serializers/mixins.py @@ -16,56 +16,62 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField, DateTimeField from taiga.projects.history import models as history_models from taiga.projects.attachments import models as attachments_models -from taiga.projects.notifications import services as notifications_services from taiga.projects.history import services as history_service from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, - JsonField, HistoryValuesField, CommentField, FileField) + HistoryValuesField, FileField) -class HistoryExportSerializer(serializers.ModelSerializer): +class HistoryExportSerializer(serializers.LightSerializer): user = HistoryUserField() - diff = HistoryDiffField(required=False) - snapshot = JsonField(required=False) - values = HistoryValuesField(required=False) - comment = CommentField(required=False) - delete_comment_date = serializers.DateTimeField(required=False) - delete_comment_user = HistoryUserField(required=False) - - class Meta: - model = history_models.HistoryEntry - exclude = ("id", "comment_html", "key") + diff = HistoryDiffField() + snapshot = Field() + values = HistoryValuesField() + comment = Field() + delete_comment_date = DateTimeField() + delete_comment_user = HistoryUserField() + comment_versions = Field() + created_at = DateTimeField() + edit_comment_date = DateTimeField() + is_hidden = Field() + is_snapshot = Field() + type = Field() -class HistoryExportSerializerMixin(serializers.ModelSerializer): - history = serializers.SerializerMethodField("get_history") +class HistoryExportSerializerMixin(serializers.LightSerializer): + history = MethodField("get_history") def get_history(self, obj): - history_qs = history_service.get_history_queryset_by_model_instance(obj, - types=(history_models.HistoryType.change, history_models.HistoryType.create,)) + history_qs = history_service.get_history_queryset_by_model_instance( + obj, + types=(history_models.HistoryType.change, history_models.HistoryType.create,) + ) return HistoryExportSerializer(history_qs, many=True).data -class AttachmentExportSerializer(serializers.ModelSerializer): - owner = UserRelatedField(required=False) +class AttachmentExportSerializer(serializers.LightSerializer): + owner = UserRelatedField() attached_file = FileField() - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = attachments_models.Attachment - exclude = ('id', 'content_type', 'object_id', 'project') + created_date = DateTimeField() + modified_date = DateTimeField() + description = Field() + is_deprecated = Field() + name = Field() + order = Field() + sha1 = Field() + size = Field() -class AttachmentExportSerializerMixin(serializers.ModelSerializer): - attachments = serializers.SerializerMethodField("get_attachments") +class AttachmentExportSerializerMixin(serializers.LightSerializer): + attachments = MethodField() def get_attachments(self, obj): content_type = ContentType.objects.get_for_model(obj.__class__) @@ -74,8 +80,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer): return AttachmentExportSerializer(attachments_qs, many=True).data -class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") +class CustomAttributesValuesExportSerializerMixin(serializers.LightSerializer): + custom_attributes_values = MethodField("get_custom_attributes_values") def custom_attributes_queryset(self, project): raise NotImplementedError() @@ -85,13 +91,13 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): ret = {} for attr in custom_attributes: value = values.get(str(attr["id"]), None) - if value is not None: + if value is not None: ret[attr["name"]] = value return ret try: - values = obj.custom_attributes_values.attributes_values + values = obj.custom_attributes_values.attributes_values custom_attributes = self.custom_attributes_queryset(obj.project) return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) @@ -99,43 +105,8 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): return None -class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer): - watchers = UserRelatedField(many=True, required=False) +class WatcheableObjectLightSerializerMixin(serializers.LightSerializer): + watchers = MethodField() - def __init__(self, *args, **kwargs): - self._watchers_field = self.base_fields.pop("watchers", None) - super(WatcheableObjectModelSerializerMixin, self).__init__(*args, **kwargs) - - """ - watchers is not a field from the model so we need to do some magic to make it work like a normal field - It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances - """ - - def restore_object(self, attrs, instance=None): - watcher_field = self.fields.pop("watchers", None) - instance = super(WatcheableObjectModelSerializerMixin, self).restore_object(attrs, instance) - self._watchers = self.init_data.get("watchers", []) - return instance - - def save_watchers(self): - new_watcher_emails = set(self._watchers) - old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) - adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) - removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) - - User = get_user_model() - adding_users = User.objects.filter(email__in=adding_watcher_emails) - removing_users = User.objects.filter(email__in=removing_watcher_emails) - - for user in adding_users: - notifications_services.add_watcher(self.object, user) - - for user in removing_users: - notifications_services.remove_watcher(self.object, user) - - self.object.watchers = [user.email for user in self.object.get_watchers()] - - def to_native(self, obj): - ret = super(WatcheableObjectModelSerializerMixin, self).to_native(obj) - ret["watchers"] = [user.email for user in obj.get_watchers()] - return ret + def get_watchers(self, obj): + return [user.email for user in obj.get_watchers()] diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index 6a316b68..ff7e791c 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -16,231 +16,183 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext as _ - from taiga.base.api import serializers -from taiga.base.fields import JsonField, PgArrayField -from taiga.base.exceptions import ValidationError +from taiga.base.fields import Field, DateTimeField, MethodField -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 -from taiga.projects.milestones import models as milestones_models -from taiga.projects.wiki import models as wiki_models -from taiga.timeline import models as timeline_models -from taiga.users import models as users_models from taiga.projects.votes import services as votes_service -from .fields import (FileField, UserRelatedField, - ProjectRelatedField, - TimelineDataField, ContentTypeField) +from .fields import (FileField, UserRelatedField, TimelineDataField, + ContentTypeField, SlugRelatedField) from .mixins import (HistoryExportSerializerMixin, AttachmentExportSerializerMixin, CustomAttributesValuesExportSerializerMixin, - WatcheableObjectModelSerializerMixin) + WatcheableObjectLightSerializerMixin) from .cache import (_custom_tasks_attributes_cache, _custom_userstories_attributes_cache, _custom_issues_attributes_cache) -class PointsExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Points - exclude = ('id', 'project') +class RelatedExportSerializer(serializers.LightSerializer): + def to_value(self, value): + if hasattr(value, 'all'): + return super().to_value(value.all()) + return super().to_value(value) -class UserStoryStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.UserStoryStatus - exclude = ('id', 'project') +class PointsExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + value = Field() -class TaskStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.TaskStatus - exclude = ('id', 'project') +class UserStoryStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + is_archived = Field() + color = Field() + wip_limit = Field() -class IssueStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.IssueStatus - exclude = ('id', 'project') +class TaskStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + color = Field() -class PriorityExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Priority - exclude = ('id', 'project') +class IssueStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + color = Field() -class SeverityExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Severity - exclude = ('id', 'project') +class PriorityExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class IssueTypeExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.IssueType - exclude = ('id', 'project') +class SeverityExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class RoleExportSerializer(serializers.ModelSerializer): - permissions = PgArrayField(required=False) - - class Meta: - model = users_models.Role - exclude = ('id', 'project') +class IssueTypeExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.UserStoryCustomAttribute - exclude = ('id', 'project') +class RoleExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + computable = Field() + permissions = Field() -class TaskCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.TaskCustomAttribute - exclude = ('id', 'project') +class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() -class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.IssueCustomAttribute - exclude = ('id', 'project') +class TaskCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() -class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): - attributes_values = JsonField(source="attributes_values", required=True) - _custom_attribute_model = None - _container_field = None +class IssueCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() - class Meta: - exclude = ("id",) - def validate_attributes_values(self, attrs, source): - # values must be a dict - data_values = attrs.get("attributes_values", None) - if self.object: - data_values = (data_values or self.object.attributes_values) - - if type(data_values) is not dict: - raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) - - # Values keys must be in the container object project - data_container = attrs.get(self._container_field, None) - if data_container: - project_id = data_container.project_id - elif self.object: - project_id = getattr(self.object, self._container_field).project_id - else: - project_id = None - - values_ids = list(data_values.keys()) - qs = self._custom_attribute_model.objects.filter(project=project_id, - id__in=values_ids) - if qs.count() != len(values_ids): - raise ValidationError(_("It contain invalid custom fields.")) - - return attrs +class BaseCustomAttributesValuesExportSerializer(RelatedExportSerializer): + attributes_values = Field(required=True) class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute - _container_model = "userstories.UserStory" - _container_field = "user_story" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.UserStoryCustomAttributesValues + user_story = Field(attr="user_story.id") class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.TaskCustomAttribute - _container_field = "task" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.TaskCustomAttributesValues + task = Field(attr="task.id") class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.IssueCustomAttribute - _container_field = "issue" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.IssueCustomAttributesValues + issue = Field(attr="issue.id") -class MembershipExportSerializer(serializers.ModelSerializer): - user = UserRelatedField(required=False) - role = ProjectRelatedField(slug_field="name") - invited_by = UserRelatedField(required=False) - - class Meta: - model = projects_models.Membership - exclude = ('id', 'project', 'token') - - def full_clean(self, instance): - return instance +class MembershipExportSerializer(RelatedExportSerializer): + user = UserRelatedField() + role = SlugRelatedField(slug_field="name") + invited_by = UserRelatedField() + is_admin = Field() + email = Field() + created_at = DateTimeField() + invitation_extra_text = Field() + user_order = Field() -class RolePointsExportSerializer(serializers.ModelSerializer): - role = ProjectRelatedField(slug_field="name") - points = ProjectRelatedField(slug_field="name") - - class Meta: - model = userstories_models.RolePoints - exclude = ('id', 'user_story') +class RolePointsExportSerializer(RelatedExportSerializer): + role = SlugRelatedField(slug_field="name") + points = SlugRelatedField(slug_field="name") -class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - estimated_start = serializers.DateField(required=False) - estimated_finish = serializers.DateField(required=False) - - def __init__(self, *args, **kwargs): - project = kwargs.pop('project', None) - super(MilestoneExportSerializer, self).__init__(*args, **kwargs) - if project: - self.project = project - - def validate_name(self, attrs, source): - """ - Check the milestone name is not duplicated in the project - """ - name = attrs[source] - qs = self.project.milestones.filter(name=name) - if qs.exists(): - raise ValidationError(_("Name duplicated for the project")) - - return attrs - - class Meta: - model = milestones_models.Milestone - exclude = ('id', 'project') +class MilestoneExportSerializer(WatcheableObjectLightSerializerMixin, RelatedExportSerializer): + name = Field() + owner = UserRelatedField() + created_date = DateTimeField() + modified_date = DateTimeField() + estimated_start = Field() + estimated_finish = Field() + slug = Field() + closed = Field() + disponibility = Field() + order = Field() -class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - user_story = ProjectRelatedField(slug_field="ref", required=False) - milestone = ProjectRelatedField(slug_field="name", required=False) - assigned_to = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = tasks_models.Task - exclude = ('id', 'project') +class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + owner = UserRelatedField() + status = SlugRelatedField(slug_field="name") + user_story = SlugRelatedField(slug_field="ref") + milestone = SlugRelatedField(slug_field="name") + assigned_to = UserRelatedField() + modified_date = DateTimeField() + created_date = DateTimeField() + finished_date = DateTimeField() + ref = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + description = Field() + is_iocaine = Field() + external_reference = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_tasks_attributes_cache: @@ -248,19 +200,35 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE return _custom_tasks_attributes_cache[project.id] -class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - role_points = RolePointsExportSerializer(many=True, required=False) - owner = UserRelatedField(required=False) - assigned_to = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - milestone = ProjectRelatedField(slug_field="name", required=False) - modified_date = serializers.DateTimeField(required=False) - generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) - - class Meta: - model = userstories_models.UserStory - exclude = ('id', 'project', 'points', 'tasks') +class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + role_points = RolePointsExportSerializer(many=True) + owner = UserRelatedField() + assigned_to = UserRelatedField() + status = SlugRelatedField(slug_field="name") + milestone = SlugRelatedField(slug_field="name") + modified_date = DateTimeField() + created_date = DateTimeField() + finish_date = DateTimeField() + generated_from_issue = SlugRelatedField(slug_field="ref") + ref = Field() + is_closed = Field() + backlog_order = Field() + sprint_order = Field() + kanban_order = Field() + subject = Field() + description = Field() + client_requirement = Field() + team_requirement = Field() + external_reference = Field() + tribe_gig = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_userstories_attributes_cache: @@ -270,21 +238,31 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His return _custom_userstories_attributes_cache[project.id] -class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - assigned_to = UserRelatedField(required=False) - priority = ProjectRelatedField(slug_field="name") - severity = ProjectRelatedField(slug_field="name") - type = ProjectRelatedField(slug_field="name") - milestone = ProjectRelatedField(slug_field="name", required=False) - votes = serializers.SerializerMethodField("get_votes") - modified_date = serializers.DateTimeField(required=False) +class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + owner = UserRelatedField() + status = SlugRelatedField(slug_field="name") + assigned_to = UserRelatedField() + priority = SlugRelatedField(slug_field="name") + severity = SlugRelatedField(slug_field="name") + type = SlugRelatedField(slug_field="name") + milestone = SlugRelatedField(slug_field="name") + votes = MethodField("get_votes") + modified_date = DateTimeField() + created_date = DateTimeField() + finished_date = DateTimeField() - class Meta: - model = issues_models.Issue - exclude = ('id', 'project') + ref = Field() + subject = Field() + description = Field() + external_reference = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def get_votes(self, obj): return [x.email for x in votes_service.get_voters(obj)] @@ -295,65 +273,93 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History return _custom_issues_attributes_cache[project.id] -class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - last_modifier = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = wiki_models.WikiPage - exclude = ('id', 'project') +class WikiPageExportSerializer(HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + slug = Field() + owner = UserRelatedField() + last_modifier = UserRelatedField() + modified_date = DateTimeField() + created_date = DateTimeField() + content = Field() + version = Field() -class WikiLinkExportSerializer(serializers.ModelSerializer): - class Meta: - model = wiki_models.WikiLink - exclude = ('id', 'project') +class WikiLinkExportSerializer(RelatedExportSerializer): + title = Field() + href = Field() + order = Field() -class TimelineExportSerializer(serializers.ModelSerializer): +class TimelineExportSerializer(RelatedExportSerializer): data = TimelineDataField() data_content_type = ContentTypeField() - - class Meta: - model = timeline_models.Timeline - exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') + event_type = Field() + created = DateTimeField() -class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): - logo = FileField(required=False) - anon_permissions = PgArrayField(required=False) - public_permissions = PgArrayField(required=False) - modified_date = serializers.DateTimeField(required=False) - roles = RoleExportSerializer(many=True, required=False) - owner = UserRelatedField(required=False) - memberships = MembershipExportSerializer(many=True, required=False) - points = PointsExportSerializer(many=True, required=False) - us_statuses = UserStoryStatusExportSerializer(many=True, required=False) - task_statuses = TaskStatusExportSerializer(many=True, required=False) - issue_types = IssueTypeExportSerializer(many=True, required=False) - issue_statuses = IssueStatusExportSerializer(many=True, required=False) - priorities = PriorityExportSerializer(many=True, required=False) - severities = SeverityExportSerializer(many=True, required=False) - tags_colors = JsonField(required=False) - default_points = serializers.SlugRelatedField(slug_field="name", required=False) - default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_priority = serializers.SlugRelatedField(slug_field="name", required=False) - default_severity = serializers.SlugRelatedField(slug_field="name", required=False) - default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) - userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) - taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) - issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) - user_stories = UserStoryExportSerializer(many=True, required=False) - tasks = TaskExportSerializer(many=True, required=False) - milestones = MilestoneExportSerializer(many=True, required=False) - issues = IssueExportSerializer(many=True, required=False) - wiki_links = WikiLinkExportSerializer(many=True, required=False) - wiki_pages = WikiPageExportSerializer(many=True, required=False) - - class Meta: - model = projects_models.Project - exclude = ('id', 'creation_template', 'members') +class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): + name = Field() + slug = Field() + description = Field() + created_date = DateTimeField() + logo = FileField() + total_milestones = Field() + total_story_points = Field() + is_backlog_activated = Field() + is_kanban_activated = Field() + is_wiki_activated = Field() + is_issues_activated = Field() + videoconferences = Field() + videoconferences_extra_data = Field() + creation_template = SlugRelatedField(slug_field="slug") + is_private = Field() + is_featured = Field() + is_looking_for_people = Field() + looking_for_people_note = Field() + userstories_csv_uuid = Field() + tasks_csv_uuid = Field() + issues_csv_uuid = Field() + transfer_token = Field() + blocked_code = Field() + totals_updated_datetime = DateTimeField() + total_fans = Field() + total_fans_last_week = Field() + total_fans_last_month = Field() + total_fans_last_year = Field() + total_activity = Field() + total_activity_last_week = Field() + total_activity_last_month = Field() + total_activity_last_year = Field() + anon_permissions = Field() + public_permissions = Field() + modified_date = DateTimeField() + roles = RoleExportSerializer(many=True) + owner = UserRelatedField() + memberships = MembershipExportSerializer(many=True) + points = PointsExportSerializer(many=True) + us_statuses = UserStoryStatusExportSerializer(many=True) + task_statuses = TaskStatusExportSerializer(many=True) + issue_types = IssueTypeExportSerializer(many=True) + issue_statuses = IssueStatusExportSerializer(many=True) + priorities = PriorityExportSerializer(many=True) + severities = SeverityExportSerializer(many=True) + tags_colors = Field() + default_points = 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") + userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True) + taskcustomattributes = TaskCustomAttributeExportSerializer(many=True) + issuecustomattributes = IssueCustomAttributeExportSerializer(many=True) + user_stories = UserStoryExportSerializer(many=True) + tasks = TaskExportSerializer(many=True) + milestones = MilestoneExportSerializer(many=True) + issues = IssueExportSerializer(many=True) + wiki_links = WikiLinkExportSerializer(many=True) + wiki_pages = WikiPageExportSerializer(many=True) + tags = Field() diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index 923647a7..0b56f3f5 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -19,49 +19,44 @@ # This makes all code that import services works and # is not the baddest practice ;) -import base64 import gc -import os - -from django.core.files.storage import default_storage from taiga.base.utils import json +from taiga.base.fields import MethodField from taiga.timeline.service import get_project_timeline from taiga.base.api.fields import get_component from .. import serializers -def render_project(project, outfile, chunk_size = 8190): +def render_project(project, outfile, chunk_size=8190): serializer = serializers.ProjectExportSerializer(project) outfile.write(b'{\n') first_field = True - for field_name in serializer.fields.keys(): + for field_name in serializer._field_map.keys(): # Avoid writing "," in the last element if not first_field: outfile.write(b",\n") else: first_field = False - field = serializer.fields.get(field_name) - field.initialize(parent=serializer, field_name=field_name) + field = serializer._field_map.get(field_name) + # 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"]: value = get_component(project, field_name) if field_name != "wiki_pages": - value = value.select_related('owner', 'status', 'milestone', 'project', 'assigned_to', 'custom_attributes_values') + value = value.select_related('owner', 'status', 'milestone', + 'project', 'assigned_to', + 'custom_attributes_values') if field_name == "issues": value = value.select_related('severity', 'priority', 'type') value = value.prefetch_related('history_entry', 'attachments') outfile.write('"{}": [\n'.format(field_name).encode()) - attachments_field = field.fields.pop("attachments", None) - if attachments_field: - attachments_field.initialize(parent=field, field_name="attachments") - first_item = True for item in value.iterator(): # Avoid writing "," in the last element @@ -70,47 +65,18 @@ def render_project(project, outfile, chunk_size = 8190): else: first_item = False - - dumped_value = json.dumps(field.to_native(item)) - writing_value = dumped_value[:-1]+ ',\n "attachments": [\n' - outfile.write(writing_value.encode()) - - first_attachment = True - for attachment in item.attachments.iterator(): - # Avoid writing "," in the last element - if not first_attachment: - outfile.write(b",\n") - else: - first_attachment = False - - # Write all the data expect the serialized file - attachment_serializer = serializers.AttachmentExportSerializer(instance=attachment) - attached_file_serializer = attachment_serializer.fields.pop("attached_file") - dumped_value = json.dumps(attachment_serializer.data) - dumped_value = dumped_value[:-1] + ',\n "attached_file":{\n "data":"' - outfile.write(dumped_value.encode()) - - # We write the attached_files by chunks so the memory used is not increased - attachment_file = attachment.attached_file - if default_storage.exists(attachment_file.name): - with default_storage.open(attachment_file.name) as f: - while True: - bin_data = f.read(chunk_size) - if not bin_data: - break - - b64_data = base64.b64encode(bin_data) - outfile.write(b64_data) - - outfile.write('", \n "name":"{}"}}\n}}'.format( - os.path.basename(attachment_file.name)).encode()) - - outfile.write(b']}') + field.many = False + dumped_value = json.dumps(field.to_value(item)) + outfile.write(dumped_value.encode()) outfile.flush() gc.collect() outfile.write(b']') else: - value = field.field_to_native(project, field_name) + if isinstance(field, MethodField): + value = field.as_getter(field_name, serializers.ProjectExportSerializer)(serializer, project) + else: + attr = getattr(project, field_name) + value = field.to_value(attr) outfile.write('"{}": {}'.format(field_name, json.dumps(value)).encode()) # Generate the timeline @@ -127,4 +93,3 @@ def render_project(project, outfile, chunk_size = 8190): outfile.write(dumped_value.encode()) outfile.write(b']}\n') - diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index 5d71c445..9739bb1e 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -39,7 +39,7 @@ from taiga.timeline.service import build_project_namespace from taiga.users import services as users_service from .. import exceptions as err -from .. import serializers +from .. import validators ######################################################################## @@ -90,13 +90,13 @@ def store_project(data): if key not in excluded_fields: project_data[key] = value - serialized = serializers.ProjectExportSerializer(data=project_data) - if serialized.is_valid(): - serialized.object._importing = True - serialized.object.save() - serialized.save_watchers() - return serialized - add_errors("project", serialized.errors) + validator = validators.ProjectExportValidator(data=project_data) + if validator.is_valid(): + validator.object._importing = True + validator.object.save() + validator.save_watchers() + return validator + add_errors("project", validator.errors) return None @@ -133,54 +133,55 @@ def _store_custom_attributes_values(obj, data_values, obj_field, serializer_clas def _store_attachment(project, obj, attachment): - serialized = serializers.AttachmentExportSerializer(data=attachment) - if serialized.is_valid(): - serialized.object.content_type = ContentType.objects.get_for_model(obj.__class__) - serialized.object.object_id = obj.id - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object.size = serialized.object.attached_file.size - serialized.object.name = os.path.basename(serialized.object.attached_file.name) - serialized.save() - return serialized - add_errors("attachments", serialized.errors) - return serialized + validator = validators.AttachmentExportValidator(data=attachment) + if validator.is_valid(): + validator.object.content_type = ContentType.objects.get_for_model(obj.__class__) + validator.object.object_id = obj.id + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object.size = validator.object.attached_file.size + validator.object.name = os.path.basename(validator.object.attached_file.name) + validator.save() + return validator + add_errors("attachments", validator.errors) + return validator def _store_history(project, obj, history): - serialized = serializers.HistoryExportSerializer(data=history, context={"project": project}) - if serialized.is_valid(): - serialized.object.key = make_key_from_model_object(obj) - if serialized.object.diff is None: - serialized.object.diff = [] - serialized.object._importing = True - serialized.save() - return serialized - add_errors("history", serialized.errors) - return serialized + validator = validators.HistoryExportValidator(data=history, context={"project": project}) + if validator.is_valid(): + validator.object.key = make_key_from_model_object(obj) + if validator.object.diff is None: + validator.object.diff = [] + validator.object.project_id = project.id + validator.object._importing = True + validator.save() + return validator + add_errors("history", validator.errors) + return validator ## ROLES def _store_role(project, role): - serialized = serializers.RoleExportSerializer(data=role) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized - add_errors("roles", serialized.errors) + validator = validators.RoleExportValidator(data=role) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator + add_errors("roles", validator.errors) return None def store_roles(project, data): results = [] for role in data.get("roles", []): - serialized = _store_role(project, role) - if serialized: - results.append(serialized) + validator = _store_role(project, role) + if validator: + results.append(validator) return results @@ -188,17 +189,17 @@ def store_roles(project, data): ## MEMGERSHIPS def _store_membership(project, membership): - serialized = serializers.MembershipExportSerializer(data=membership, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.object.token = str(uuid.uuid1()) - serialized.object.user = find_invited_user(serialized.object.email, - default=serialized.object.user) - serialized.save() - return serialized + validator = validators.MembershipExportValidator(data=membership, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.object.token = str(uuid.uuid1()) + validator.object.user = find_invited_user(validator.object.email, + default=validator.object.user) + validator.save() + return validator - add_errors("memberships", serialized.errors) + add_errors("memberships", validator.errors) return None @@ -212,13 +213,13 @@ def store_memberships(project, data): ## PROJECT ATTRIBUTES def _store_project_attribute_value(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) + validator = serializer(data=data) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator.object + add_errors(field, validator.errors) return None @@ -253,13 +254,13 @@ def store_default_project_attributes_values(project, data): ## CUSTOM ATTRIBUTES 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) + validator = serializer(data=data) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator.object + add_errors(field, validator.errors) return None @@ -273,19 +274,19 @@ def store_custom_attributes(project, data, field, serializer): ## MILESTONE def store_milestone(project, milestone): - serialized = serializers.MilestoneExportSerializer(data=milestone, project=project) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - serialized.save_watchers() + validator = validators.MilestoneExportValidator(data=milestone, project=project) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + validator.save_watchers() for task_without_us in milestone.get("tasks_without_us", []): task_without_us["user_story"] = None store_task(project, task_without_us) - return serialized + return validator - add_errors("milestones", serialized.errors) + add_errors("milestones", validator.errors) return None @@ -300,20 +301,20 @@ def store_milestones(project, data): ## USER STORIES def _store_role_point(project, us, role_point): - serialized = serializers.RolePointsExportSerializer(data=role_point, context={"project": project}) - if serialized.is_valid(): + validator = validators.RolePointsExportValidator(data=role_point, context={"project": project}) + if validator.is_valid(): try: - existing_role_point = us.role_points.get(role=serialized.object.role) - existing_role_point.points = serialized.object.points + existing_role_point = us.role_points.get(role=validator.object.role) + existing_role_point.points = validator.object.points existing_role_point.save() return existing_role_point except RolePoints.DoesNotExist: - serialized.object.user_story = us - serialized.save() - return serialized.object + validator.object.user_story = us + validator.save() + return validator.object - add_errors("role_points", serialized.errors) + add_errors("role_points", validator.errors) return None def store_user_story(project, data): @@ -322,51 +323,51 @@ def store_user_story(project, data): us_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} - serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project}) + validator = validators.UserStoryExportValidator(data=us_data, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + 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 - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + 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, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for us_attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, us_attachment) + _store_attachment(project, validator.object, us_attachment) for role_point in data.get("role_points", []): - _store_role_point(project, serialized.object, role_point) + _store_role_point(project, validator.object, role_point) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name') + 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) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "user_story", serializers.UserStoryCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "user_story", validators.UserStoryCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("user_stories", serialized.errors) + add_errors("user_stories", validator.errors) return None @@ -384,47 +385,47 @@ def store_task(project, data): if "status" not in data and project.default_task_status: data["status"] = project.default_task_status.name - serialized = serializers.TaskExportSerializer(data=data, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + validator = validators.TaskExportValidator(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 - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + 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, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for task_attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, task_attachment) + _store_attachment(project, validator.object, task_attachment) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name') + 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) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "task", serializers.TaskCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "task", validators.TaskCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("tasks", serialized.errors) + add_errors("tasks", validator.errors) return None @@ -439,7 +440,7 @@ def store_tasks(project, data): ## ISSUES def store_issue(project, data): - serialized = serializers.IssueExportSerializer(data=data, context={"project": project}) + validator = validators.IssueExportValidator(data=data, context={"project": project}) if "type" not in data and project.default_issue_type: data["type"] = project.default_issue_type.name @@ -453,46 +454,46 @@ def store_issue(project, data): if "severity" not in data and project.default_severity: data["severity"] = project.default_severity.name - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + 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 - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + 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, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, attachment) + _store_attachment(project, validator.object, attachment) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name') + 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) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "issue", serializers.IssueCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "issue", validators.IssueCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("issues", serialized.errors) + add_errors("issues", validator.errors) return None @@ -507,29 +508,29 @@ def store_issues(project, data): def store_wiki_page(project, wiki_page): wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", ""))) - serialized = serializers.WikiPageExportSerializer(data=wiki_page) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True - serialized.save() - serialized.save_watchers() + validator = validators.WikiPageExportValidator(data=wiki_page) + 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() for attachment in wiki_page.get("attachments", []): - _store_attachment(project, serialized.object, attachment) + _store_attachment(project, validator.object, attachment) history_entries = wiki_page.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) - return serialized + return validator - add_errors("wiki_pages", serialized.errors) + add_errors("wiki_pages", validator.errors) return None @@ -543,14 +544,14 @@ def store_wiki_pages(project, data): ## WIKI LINKS def store_wiki_link(project, wiki_link): - serialized = serializers.WikiLinkExportSerializer(data=wiki_link) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized + validator = validators.WikiLinkExportValidator(data=wiki_link) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator - add_errors("wiki_links", serialized.errors) + add_errors("wiki_links", validator.errors) return None @@ -572,17 +573,17 @@ def store_tags_colors(project, data): ## TIMELINE def _store_timeline_entry(project, timeline): - serialized = serializers.TimelineExportSerializer(data=timeline, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - serialized.object.namespace = build_project_namespace(project) - serialized.object.object_id = project.id - serialized.object.content_type = ContentType.objects.get_for_model(project.__class__) - serialized.object._importing = True - serialized.save() - return serialized - add_errors("timeline", serialized.errors) - return serialized + validator = validators.TimelineExportValidator(data=timeline, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + validator.object.namespace = build_project_namespace(project) + validator.object.object_id = project.id + validator.object.content_type = ContentType.objects.get_for_model(project.__class__) + validator.object._importing = True + validator.save() + return validator + add_errors("timeline", validator.errors) + return validator def store_timeline_entries(project, data): @@ -617,13 +618,13 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data): def _create_project_object(data): # Create the project - project_serialized = store_project(data) + project_validator = store_project(data) - if not project_serialized: + if not project_validator: errors = get_errors(clear=True) raise err.TaigaImportError(_("error importing project data"), None, errors=errors) - return project_serialized.object if project_serialized else None + return project_validator.object if project_validator else None def _create_membership_for_project_owner(project): @@ -654,13 +655,13 @@ def _populate_project_object(project, data): check_if_there_is_some_error(_("error importing memberships"), project) # Create project attributes values - store_project_attributes_values(project, data, "us_statuses", serializers.UserStoryStatusExportSerializer) - store_project_attributes_values(project, data, "points", serializers.PointsExportSerializer) - store_project_attributes_values(project, data, "task_statuses", serializers.TaskStatusExportSerializer) - store_project_attributes_values(project, data, "issue_types", serializers.IssueTypeExportSerializer) - store_project_attributes_values(project, data, "issue_statuses", serializers.IssueStatusExportSerializer) - store_project_attributes_values(project, data, "priorities", serializers.PriorityExportSerializer) - store_project_attributes_values(project, data, "severities", serializers.SeverityExportSerializer) + 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) + store_project_attributes_values(project, data, "issue_types", validators.IssueTypeExportValidator) + store_project_attributes_values(project, data, "issue_statuses", validators.IssueStatusExportValidator) + store_project_attributes_values(project, data, "priorities", validators.PriorityExportValidator) + store_project_attributes_values(project, data, "severities", validators.SeverityExportValidator) check_if_there_is_some_error(_("error importing lists of project attributes"), project) # Create default values for project attributes @@ -669,11 +670,11 @@ def _populate_project_object(project, data): # Create custom attributes store_custom_attributes(project, data, "userstorycustomattributes", - serializers.UserStoryCustomAttributeExportSerializer) + validators.UserStoryCustomAttributeExportValidator) store_custom_attributes(project, data, "taskcustomattributes", - serializers.TaskCustomAttributeExportSerializer) + validators.TaskCustomAttributeExportValidator) store_custom_attributes(project, data, "issuecustomattributes", - serializers.IssueCustomAttributeExportSerializer) + validators.IssueCustomAttributeExportValidator) check_if_there_is_some_error(_("error importing custom attributes"), project) # Create milestones diff --git a/taiga/export_import/validators/__init__.py b/taiga/export_import/validators/__init__.py new file mode 100644 index 00000000..969a8d0c --- /dev/null +++ b/taiga/export_import/validators/__init__.py @@ -0,0 +1,27 @@ +from .validators import PointsExportValidator +from .validators import UserStoryStatusExportValidator +from .validators import TaskStatusExportValidator +from .validators import IssueStatusExportValidator +from .validators import PriorityExportValidator +from .validators import SeverityExportValidator +from .validators import IssueTypeExportValidator +from .validators import RoleExportValidator +from .validators import UserStoryCustomAttributeExportValidator +from .validators import TaskCustomAttributeExportValidator +from .validators import IssueCustomAttributeExportValidator +from .validators import BaseCustomAttributesValuesExportValidator +from .validators import UserStoryCustomAttributesValuesExportValidator +from .validators import TaskCustomAttributesValuesExportValidator +from .validators import IssueCustomAttributesValuesExportValidator +from .validators import MembershipExportValidator +from .validators import RolePointsExportValidator +from .validators import MilestoneExportValidator +from .validators import TaskExportValidator +from .validators import UserStoryExportValidator +from .validators import IssueExportValidator +from .validators import WikiPageExportValidator +from .validators import WikiLinkExportValidator +from .validators import TimelineExportValidator +from .validators import ProjectExportValidator +from .mixins import AttachmentExportValidator +from .mixins import HistoryExportValidator diff --git a/taiga/export_import/validators/cache.py b/taiga/export_import/validators/cache.py new file mode 100644 index 00000000..c4eb5bfa --- /dev/null +++ b/taiga/export_import/validators/cache.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +from taiga.users import models as users_models + +_cache_user_by_pk = {} +_cache_user_by_email = {} +_custom_tasks_attributes_cache = {} +_custom_issues_attributes_cache = {} +_custom_userstories_attributes_cache = {} + + +def cached_get_user_by_pk(pk): + if pk not in _cache_user_by_pk: + try: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + except Exception: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + return _cache_user_by_pk[pk] + +def cached_get_user_by_email(email): + if email not in _cache_user_by_email: + try: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + except Exception: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + return _cache_user_by_email[email] diff --git a/taiga/export_import/validators/fields.py b/taiga/export_import/validators/fields.py new file mode 100644 index 00000000..e3d33c7a --- /dev/null +++ b/taiga/export_import/validators/fields.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import base64 +import copy + +from django.core.files.base import ContentFile +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError +from taiga.base.fields import JsonField +from taiga.mdrender.service import render as mdrender +from taiga.users import models as users_models + +from .cache import cached_get_user_by_email + + +class FileField(serializers.WritableField): + read_only = False + + def from_native(self, data): + if not data: + return None + + decoded_data = b'' + # The original file was encoded by chunks but we don't really know its + # length or if it was multiple of 3 so we must iterate over all those chunks + # decoding them one by one + for decoding_chunk in data['data'].split("="): + # When encoding to base64 3 bytes are transformed into 4 bytes and + # the extra space of the block is filled with = + # We must ensure that the decoding chunk has a length multiple of 4 so + # we restore the stripped '='s adding appending them until the chunk has + # a length multiple of 4 + decoding_chunk += "=" * (-len(decoding_chunk) % 4) + decoded_data += base64.b64decode(decoding_chunk + "=") + + return ContentFile(decoded_data, name=data['name']) + + +class ContentTypeField(serializers.RelatedField): + read_only = False + + def from_native(self, data): + try: + return ContentType.objects.get_by_natural_key(*data) + except Exception: + return None + + +class RelatedNoneSafeField(serializers.RelatedField): + def field_from_native(self, data, files, field_name, into): + if self.read_only: + return + + try: + if self.many: + try: + # Form data + value = data.getlist(field_name) + if value == [''] or value == []: + raise KeyError + except AttributeError: + # Non-form data + value = data[field_name] + else: + value = data[field_name] + except KeyError: + if self.partial: + 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[key] = None + elif self.many: + into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] + else: + into[key] = self.from_native(value) + + +class UserRelatedField(RelatedNoneSafeField): + read_only = False + + def from_native(self, data): + try: + return cached_get_user_by_email(data) + except users_models.User.DoesNotExist: + return None + + +class UserPkField(serializers.RelatedField): + read_only = False + + def from_native(self, data): + try: + user = cached_get_user_by_email(data) + return user.pk + except users_models.User.DoesNotExist: + return None + + +class CommentField(serializers.WritableField): + read_only = False + + def field_from_native(self, data, files, field_name, into): + super().field_from_native(data, files, field_name, into) + into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) + + +class ProjectRelatedField(serializers.RelatedField): + read_only = False + null_values = (None, "") + + def __init__(self, slug_field, *args, **kwargs): + self.slug_field = slug_field + super().__init__(*args, **kwargs) + + def from_native(self, data): + try: + kwargs = {self.slug_field: data, "project": self.context['project']} + return self.queryset.get(**kwargs) + except ObjectDoesNotExist: + raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data))) + + +class HistoryUserField(JsonField): + def from_native(self, data): + if data is None: + return {} + + if len(data) < 2: + return {} + + user = UserRelatedField().from_native(data[0]) + + if user: + pk = user.pk + else: + pk = None + + return {"pk": pk, "name": data[1]} + + +class HistoryValuesField(JsonField): + def from_native(self, data): + if data is None: + return [] + if "users" in data: + data['users'] = list(map(UserPkField().from_native, data['users'])) + return data + + +class HistoryDiffField(JsonField): + def from_native(self, data): + if data is None: + return [] + + if "assigned_to" in data: + data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) + return data + + +class TimelineDataField(serializers.WritableField): + read_only = False + + def from_native(self, data): + new_data = copy.deepcopy(data) + try: + user = cached_get_user_by_email(new_data["user"]["email"]) + new_data["user"]["id"] = user.id + del new_data["user"]["email"] + except users_models.User.DoesNotExist: + pass + + return new_data diff --git a/taiga/export_import/validators/mixins.py b/taiga/export_import/validators/mixins.py new file mode 100644 index 00000000..d07334b6 --- /dev/null +++ b/taiga/export_import/validators/mixins.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.contenttypes.models import ContentType + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.projects.history import models as history_models +from taiga.projects.attachments import models as attachments_models +from taiga.projects.notifications import services as notifications_services +from taiga.projects.history import services as history_service + +from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, + JsonField, HistoryValuesField, CommentField, FileField) + + +class HistoryExportValidator(validators.ModelValidator): + user = HistoryUserField() + diff = HistoryDiffField(required=False) + snapshot = JsonField(required=False) + values = HistoryValuesField(required=False) + comment = CommentField(required=False) + delete_comment_date = serializers.DateTimeField(required=False) + delete_comment_user = HistoryUserField(required=False) + + class Meta: + model = history_models.HistoryEntry + exclude = ("id", "comment_html", "key", "project") + + +class AttachmentExportValidator(validators.ModelValidator): + owner = UserRelatedField(required=False) + attached_file = FileField() + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = attachments_models.Attachment + exclude = ('id', 'content_type', 'object_id', 'project') + + +class WatcheableObjectModelValidatorMixin(validators.ModelValidator): + watchers = UserRelatedField(many=True, required=False) + + def __init__(self, *args, **kwargs): + self._watchers_field = self.base_fields.pop("watchers", None) + super(WatcheableObjectModelValidatorMixin, self).__init__(*args, **kwargs) + + """ + watchers is not a field from the model so we need to do some magic to make it work like a normal field + It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances + """ + + def restore_object(self, attrs, instance=None): + self.fields.pop("watchers", None) + instance = super(WatcheableObjectModelValidatorMixin, self).restore_object(attrs, instance) + self._watchers = self.init_data.get("watchers", []) + return instance + + def save_watchers(self): + new_watcher_emails = set(self._watchers) + old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) + adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) + removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) + + User = get_user_model() + adding_users = User.objects.filter(email__in=adding_watcher_emails) + removing_users = User.objects.filter(email__in=removing_watcher_emails) + + for user in adding_users: + notifications_services.add_watcher(self.object, user) + + for user in removing_users: + notifications_services.remove_watcher(self.object, user) + + self.object.watchers = [user.email for user in self.object.get_watchers()] + + def to_native(self, obj): + ret = super(WatcheableObjectModelValidatorMixin, self).to_native(obj) + ret["watchers"] = [user.email for user in obj.get_watchers()] + return ret diff --git a/taiga/export_import/validators/validators.py b/taiga/export_import/validators/validators.py new file mode 100644 index 00000000..818df0c3 --- /dev/null +++ b/taiga/export_import/validators/validators.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +from django.utils.translation import ugettext as _ + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import JsonField, PgArrayField +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.userstories import models as userstories_models +from taiga.projects.tasks import models as tasks_models +from taiga.projects.issues import models as issues_models +from taiga.projects.milestones import models as milestones_models +from taiga.projects.wiki import models as wiki_models +from taiga.timeline import models as timeline_models +from taiga.users import models as users_models + +from .fields import (FileField, UserRelatedField, + ProjectRelatedField, + TimelineDataField, ContentTypeField) +from .mixins import WatcheableObjectModelValidatorMixin +from .cache import (_custom_tasks_attributes_cache, + _custom_userstories_attributes_cache, + _custom_issues_attributes_cache) + + +class PointsExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Points + exclude = ('id', 'project') + + +class UserStoryStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.UserStoryStatus + exclude = ('id', 'project') + + +class TaskStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.TaskStatus + exclude = ('id', 'project') + + +class IssueStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.IssueStatus + exclude = ('id', 'project') + + +class PriorityExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Priority + exclude = ('id', 'project') + + +class SeverityExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Severity + exclude = ('id', 'project') + + +class IssueTypeExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.IssueType + exclude = ('id', 'project') + + +class RoleExportValidator(validators.ModelValidator): + permissions = PgArrayField(required=False) + + class Meta: + model = users_models.Role + exclude = ('id', 'project') + + +class UserStoryCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.UserStoryCustomAttribute + exclude = ('id', 'project') + + +class TaskCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.TaskCustomAttribute + exclude = ('id', 'project') + + +class IssueCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.IssueCustomAttribute + exclude = ('id', 'project') + + +class BaseCustomAttributesValuesExportValidator(validators.ModelValidator): + attributes_values = JsonField(source="attributes_values", required=True) + _custom_attribute_model = None + _container_field = None + + class Meta: + exclude = ("id",) + + def validate_attributes_values(self, attrs, source): + # values must be a dict + data_values = attrs.get("attributes_values", None) + if self.object: + data_values = (data_values or self.object.attributes_values) + + if type(data_values) is not dict: + raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) + + # Values keys must be in the container object project + data_container = attrs.get(self._container_field, None) + if data_container: + project_id = data_container.project_id + elif self.object: + project_id = getattr(self.object, self._container_field).project_id + else: + project_id = None + + values_ids = list(data_values.keys()) + qs = self._custom_attribute_model.objects.filter(project=project_id, + id__in=values_ids) + if qs.count() != len(values_ids): + raise ValidationError(_("It contain invalid custom fields.")) + + return attrs + + +class UserStoryCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute + _container_model = "userstories.UserStory" + _container_field = "user_story" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.UserStoryCustomAttributesValues + + +class TaskCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.TaskCustomAttribute + _container_field = "task" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.TaskCustomAttributesValues + + +class IssueCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.IssueCustomAttribute + _container_field = "issue" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.IssueCustomAttributesValues + + +class MembershipExportValidator(validators.ModelValidator): + user = UserRelatedField(required=False) + role = ProjectRelatedField(slug_field="name") + invited_by = UserRelatedField(required=False) + + class Meta: + model = projects_models.Membership + exclude = ('id', 'project', 'token') + + def full_clean(self, instance): + return instance + + +class RolePointsExportValidator(validators.ModelValidator): + role = ProjectRelatedField(slug_field="name") + points = ProjectRelatedField(slug_field="name") + + class Meta: + model = userstories_models.RolePoints + exclude = ('id', 'user_story') + + +class MilestoneExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + estimated_start = serializers.DateField(required=False) + estimated_finish = serializers.DateField(required=False) + + def __init__(self, *args, **kwargs): + project = kwargs.pop('project', None) + super(MilestoneExportValidator, self).__init__(*args, **kwargs) + if project: + self.project = project + + def validate_name(self, attrs, source): + """ + Check the milestone name is not duplicated in the project + """ + name = attrs[source] + qs = self.project.milestones.filter(name=name) + if qs.exists(): + raise ValidationError(_("Name duplicated for the project")) + + return attrs + + class Meta: + model = milestones_models.Milestone + exclude = ('id', 'project') + + +class TaskExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + user_story = ProjectRelatedField(slug_field="ref", required=False) + milestone = ProjectRelatedField(slug_field="name", required=False) + assigned_to = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = tasks_models.Task + exclude = ('id', 'project') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_tasks_attributes_cache: + _custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name')) + return _custom_tasks_attributes_cache[project.id] + + +class UserStoryExportValidator(WatcheableObjectModelValidatorMixin): + role_points = RolePointsExportValidator(many=True, required=False) + owner = UserRelatedField(required=False) + assigned_to = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + milestone = ProjectRelatedField(slug_field="name", required=False) + modified_date = serializers.DateTimeField(required=False) + generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) + + class Meta: + model = userstories_models.UserStory + exclude = ('id', 'project', 'points', 'tasks') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_userstories_attributes_cache: + _custom_userstories_attributes_cache[project.id] = list( + project.userstorycustomattributes.all().values('id', 'name') + ) + return _custom_userstories_attributes_cache[project.id] + + +class IssueExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + assigned_to = UserRelatedField(required=False) + priority = ProjectRelatedField(slug_field="name") + severity = ProjectRelatedField(slug_field="name") + type = ProjectRelatedField(slug_field="name") + milestone = ProjectRelatedField(slug_field="name", required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = issues_models.Issue + exclude = ('id', 'project') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_issues_attributes_cache: + _custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name')) + return _custom_issues_attributes_cache[project.id] + + +class WikiPageExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + last_modifier = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = wiki_models.WikiPage + exclude = ('id', 'project') + + +class WikiLinkExportValidator(validators.ModelValidator): + class Meta: + model = wiki_models.WikiLink + exclude = ('id', 'project') + + +class TimelineExportValidator(validators.ModelValidator): + data = TimelineDataField() + data_content_type = ContentTypeField() + + class Meta: + model = timeline_models.Timeline + exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') + + +class ProjectExportValidator(WatcheableObjectModelValidatorMixin): + logo = FileField(required=False) + anon_permissions = PgArrayField(required=False) + public_permissions = PgArrayField(required=False) + modified_date = serializers.DateTimeField(required=False) + roles = RoleExportValidator(many=True, required=False) + owner = UserRelatedField(required=False) + memberships = MembershipExportValidator(many=True, required=False) + points = PointsExportValidator(many=True, required=False) + us_statuses = UserStoryStatusExportValidator(many=True, required=False) + task_statuses = TaskStatusExportValidator(many=True, required=False) + issue_types = IssueTypeExportValidator(many=True, required=False) + issue_statuses = IssueStatusExportValidator(many=True, required=False) + priorities = PriorityExportValidator(many=True, required=False) + severities = SeverityExportValidator(many=True, required=False) + tags_colors = JsonField(required=False) + creation_template = serializers.SlugRelatedField(slug_field="slug", required=False) + default_points = serializers.SlugRelatedField(slug_field="name", required=False) + default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_priority = serializers.SlugRelatedField(slug_field="name", required=False) + default_severity = serializers.SlugRelatedField(slug_field="name", required=False) + default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) + userstorycustomattributes = UserStoryCustomAttributeExportValidator(many=True, required=False) + taskcustomattributes = TaskCustomAttributeExportValidator(many=True, required=False) + issuecustomattributes = IssueCustomAttributeExportValidator(many=True, required=False) + user_stories = UserStoryExportValidator(many=True, required=False) + tasks = TaskExportValidator(many=True, required=False) + milestones = MilestoneExportValidator(many=True, required=False) + issues = IssueExportValidator(many=True, required=False) + wiki_links = WikiLinkExportValidator(many=True, required=False) + wiki_pages = WikiPageExportValidator(many=True, required=False) + + class Meta: + model = projects_models.Project + exclude = ('id', 'members')