From 1fa5a12d061346240e30fb59a4389797476cab3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 Jun 2016 09:33:12 +0200 Subject: [PATCH] Reestructure serializers module --- taiga/export_import/serializers/__init__.py | 45 ++ taiga/export_import/serializers/cache.py | 42 ++ taiga/export_import/serializers/fields.py | 250 ++++++++++++ taiga/export_import/serializers/mixins.py | 141 +++++++ .../{ => serializers}/serializers.py | 384 +----------------- 5 files changed, 495 insertions(+), 367 deletions(-) create mode 100644 taiga/export_import/serializers/__init__.py create mode 100644 taiga/export_import/serializers/cache.py create mode 100644 taiga/export_import/serializers/fields.py create mode 100644 taiga/export_import/serializers/mixins.py rename taiga/export_import/{ => serializers}/serializers.py (52%) diff --git a/taiga/export_import/serializers/__init__.py b/taiga/export_import/serializers/__init__.py new file mode 100644 index 00000000..5d793a87 --- /dev/null +++ b/taiga/export_import/serializers/__init__.py @@ -0,0 +1,45 @@ +# -*- 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 .serializers import PointsExportSerializer +from .serializers import UserStoryStatusExportSerializer +from .serializers import TaskStatusExportSerializer +from .serializers import IssueStatusExportSerializer +from .serializers import PriorityExportSerializer +from .serializers import SeverityExportSerializer +from .serializers import IssueTypeExportSerializer +from .serializers import RoleExportSerializer +from .serializers import UserStoryCustomAttributeExportSerializer +from .serializers import TaskCustomAttributeExportSerializer +from .serializers import IssueCustomAttributeExportSerializer +from .serializers import BaseCustomAttributesValuesExportSerializer +from .serializers import UserStoryCustomAttributesValuesExportSerializer +from .serializers import TaskCustomAttributesValuesExportSerializer +from .serializers import IssueCustomAttributesValuesExportSerializer +from .serializers import MembershipExportSerializer +from .serializers import RolePointsExportSerializer +from .serializers import MilestoneExportSerializer +from .serializers import TaskExportSerializer +from .serializers import UserStoryExportSerializer +from .serializers import IssueExportSerializer +from .serializers import WikiPageExportSerializer +from .serializers import WikiLinkExportSerializer +from .serializers import TimelineExportSerializer +from .serializers import ProjectExportSerializer +from .mixins import AttachmentExportSerializer +from .mixins import HistoryExportSerializer diff --git a/taiga/export_import/serializers/cache.py b/taiga/export_import/serializers/cache.py new file mode 100644 index 00000000..c4eb5bfa --- /dev/null +++ b/taiga/export_import/serializers/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/serializers/fields.py b/taiga/export_import/serializers/fields.py new file mode 100644 index 00000000..f2ca8841 --- /dev/null +++ b/taiga/export_import/serializers/fields.py @@ -0,0 +1,250 @@ +# -*- 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 os +import copy +from collections import OrderedDict + +from django.core.files.base import ContentFile +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext as _ + +from taiga.base.api import serializers +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, cached_get_user_by_pk + + +class FileField(serializers.WritableField): + read_only = False + + def to_native(self, obj): + if not obj: + return None + + data = base64.b64encode(obj.read()).decode('utf-8') + + return OrderedDict([ + ("data", data), + ("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 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): + 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): + 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, "") + + def __init__(self, slug_field, *args, **kwargs): + self.slug_field = slug_field + super().__init__(*args, **kwargs) + + def to_native(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): + 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]} + + +class HistoryValuesField(JsonField): + def to_native(self, obj): + if obj is None: + return [] + if "users" in obj: + obj['users'] = list(map(UserPkField().to_native, 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): + if obj is None: + return [] + + if "assigned_to" in obj: + obj['assigned_to'] = list(map(UserPkField().to_native, 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): + new_data = copy.deepcopy(data) + try: + user = cached_get_user_by_pk(new_data["user"]["id"]) + new_data["user"]["email"] = user.email + del new_data["user"]["id"] + 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 new file mode 100644 index 00000000..007649a2 --- /dev/null +++ b/taiga/export_import/serializers/mixins.py @@ -0,0 +1,141 @@ +# -*- 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.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 HistoryExportSerializer(serializers.ModelSerializer): + 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") + + +class HistoryExportSerializerMixin(serializers.ModelSerializer): + history = serializers.SerializerMethodField("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,)) + + return HistoryExportSerializer(history_qs, many=True).data + + +class AttachmentExportSerializer(serializers.ModelSerializer): + 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 AttachmentExportSerializerMixin(serializers.ModelSerializer): + attachments = serializers.SerializerMethodField("get_attachments") + + def get_attachments(self, obj): + content_type = ContentType.objects.get_for_model(obj.__class__) + attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, + content_type=content_type) + return AttachmentExportSerializer(attachments_qs, many=True).data + + +class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): + custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") + + def custom_attributes_queryset(self, project): + raise NotImplementedError() + + def get_custom_attributes_values(self, obj): + def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): + ret = {} + for attr in custom_attributes: + value = values.get(str(attr["id"]), None) + if value is not None: + ret[attr["name"]] = value + + return ret + + try: + 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) + except ObjectDoesNotExist: + return None + + +class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer): + watchers = UserRelatedField(many=True, required=False) + + 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 diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers/serializers.py similarity index 52% rename from taiga/export_import/serializers.py rename to taiga/export_import/serializers/serializers.py index 16877ea7..e7a2af76 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -16,25 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import base64 import copy -import os -from collections import OrderedDict -from django.apps import apps -from django.contrib.auth import get_user_model -from django.core.files.base import ContentFile -from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError -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.fields import JsonField, PgArrayField -from taiga.mdrender.service import render as mdrender 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 @@ -46,308 +35,19 @@ from taiga.projects.history import models as history_models from taiga.projects.attachments import models as attachments_models from taiga.timeline import models as timeline_models from taiga.users import models as users_models -from taiga.projects.notifications import services as notifications_services from taiga.projects.votes import services as votes_service -from taiga.projects.history import services as history_service -_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] - - -class FileField(serializers.WritableField): - read_only = False - - def to_native(self, obj): - if not obj: - return None - - data = base64.b64encode(obj.read()).decode('utf-8') - - return OrderedDict([ - ("data", data), - ("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 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): - 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): - 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, "") - - def __init__(self, slug_field, *args, **kwargs): - self.slug_field = slug_field - super().__init__(*args, **kwargs) - - def to_native(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): - 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]} - - -class HistoryValuesField(JsonField): - def to_native(self, obj): - if obj is None: - return [] - if "users" in obj: - obj['users'] = list(map(UserPkField().to_native, 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): - if obj is None: - return [] - - if "assigned_to" in obj: - obj['assigned_to'] = list(map(UserPkField().to_native, 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 WatcheableObjectModelSerializer(serializers.ModelSerializer): - watchers = UserRelatedField(many=True, required=False) - - def __init__(self, *args, **kwargs): - self._watchers_field = self.base_fields.pop("watchers", None) - super(WatcheableObjectModelSerializer, 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(WatcheableObjectModelSerializer, 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(WatcheableObjectModelSerializer, self).to_native(obj) - ret["watchers"] = [user.email for user in obj.get_watchers()] - return ret - - -class HistoryExportSerializer(serializers.ModelSerializer): - 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") - - -class HistoryExportSerializerMixin(serializers.ModelSerializer): - history = serializers.SerializerMethodField("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,)) - - return HistoryExportSerializer(history_qs, many=True).data - - -class AttachmentExportSerializer(serializers.ModelSerializer): - 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 AttachmentExportSerializerMixin(serializers.ModelSerializer): - attachments = serializers.SerializerMethodField("get_attachments") - - def get_attachments(self, obj): - content_type = ContentType.objects.get_for_model(obj.__class__) - attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, - content_type=content_type) - return AttachmentExportSerializer(attachments_qs, many=True).data +from .fields import (FileField, RelatedNoneSafeField, UserRelatedField, + UserPkField, CommentField, ProjectRelatedField, + HistoryUserField, HistoryValuesField, HistoryDiffField, + TimelineDataField) +from .mixins import (HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + CustomAttributesValuesExportSerializerMixin, + WatcheableObjectModelSerializerMixin) +from .cache import (_custom_tasks_attributes_cache, + _custom_userstories_attributes_cache, + _custom_issues_attributes_cache) class PointsExportSerializer(serializers.ModelSerializer): @@ -424,31 +124,6 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") - - def custom_attributes_queryset(self, project): - raise NotImplementedError() - - def get_custom_attributes_values(self, obj): - def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): - ret = {} - for attr in custom_attributes: - value = values.get(str(attr["id"]), None) - if value is not None: - ret[attr["name"]] = value - - return ret - - try: - 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) - except ObjectDoesNotExist: - return None - - class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): attributes_values = JsonField(source="attributes_values",required=True) _custom_attribute_model = None @@ -530,7 +205,7 @@ class RolePointsExportSerializer(serializers.ModelSerializer): exclude = ('id', 'user_story') -class MilestoneExportSerializer(WatcheableObjectModelSerializer): +class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) modified_date = serializers.DateTimeField(required=False) estimated_start = serializers.DateField(required=False) @@ -559,7 +234,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializer): class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) @@ -578,7 +253,7 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) @@ -598,7 +273,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -623,7 +298,7 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - WatcheableObjectModelSerializer): + WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False) modified_date = serializers.DateTimeField(required=False) @@ -640,31 +315,6 @@ class WikiLinkExportSerializer(serializers.ModelSerializer): -class TimelineDataField(serializers.WritableField): - read_only = False - - def to_native(self, data): - new_data = copy.deepcopy(data) - try: - user = cached_get_user_by_pk(new_data["user"]["id"]) - new_data["user"]["email"] = user.email - del new_data["user"]["id"] - 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 - - class TimelineExportSerializer(serializers.ModelSerializer): data = TimelineDataField() class Meta: @@ -672,7 +322,7 @@ class TimelineExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project', 'namespace', 'object_id') -class ProjectExportSerializer(WatcheableObjectModelSerializer): +class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): logo = FileField(required=False) anon_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False)