Merge pull request #781 from taigaio/export-import-serializer-validator-separation

Migrating export_import api to new serializers/validators
remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-07-28 16:46:22 +02:00 committed by GitHub
commit 340bc7670b
12 changed files with 1294 additions and 765 deletions

View File

@ -18,7 +18,8 @@
from django.forms import widgets from django.forms import widgets
from django.utils.translation import ugettext as _ 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 import serpy
@ -128,4 +129,21 @@ class I18NJsonField(Field):
class FileField(Field): class FileField(Field):
def to_value(self, value): def to_value(self, value):
if value:
return value.name 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)

View File

@ -44,11 +44,11 @@ from taiga.users import services as users_services
from . import exceptions as err from . import exceptions as err
from . import mixins from . import mixins
from . import permissions from . import permissions
from . import validators
from . import serializers from . import serializers
from . import services from . import services
from . import tasks from . import tasks
from . import throttling from . import throttling
from .renderers import ExportRenderer
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
@ -102,8 +102,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Validate if the project can be imported # Validate if the project can be imported
is_private = data.get('is_private', False) is_private = data.get('is_private', False)
total_memberships = len([m for m in data.get("memberships", []) total_memberships = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
if m.get("email", None) != data["owner"]])
total_memberships = total_memberships + 1 # 1 is the owner total_memberships = total_memberships + 1 # 1 is the owner
(enough_slots, error_message) = users_services.has_available_slot_for_import_new_project( (enough_slots, error_message) = users_services.has_available_slot_for_import_new_project(
self.request.user, self.request.user,
@ -147,31 +146,31 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create project values choicess # Create project values choicess
if "points" in data: if "points" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"points", serializers.PointsExportSerializer) "points", validators.PointsExportValidator)
if "issue_types" in data: if "issue_types" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"issue_types", "issue_types",
serializers.IssueTypeExportSerializer) validators.IssueTypeExportValidator)
if "issue_statuses" in data: if "issue_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"issue_statuses", "issue_statuses",
serializers.IssueStatusExportSerializer,) validators.IssueStatusExportValidator,)
if "us_statuses" in data: if "us_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"us_statuses", "us_statuses",
serializers.UserStoryStatusExportSerializer,) validators.UserStoryStatusExportValidator,)
if "task_statuses" in data: if "task_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"task_statuses", "task_statuses",
serializers.TaskStatusExportSerializer) validators.TaskStatusExportValidator)
if "priorities" in data: if "priorities" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"priorities", "priorities",
serializers.PriorityExportSerializer) validators.PriorityExportValidator)
if "severities" in data: if "severities" in data:
services.store.store_project_attributes_values(project_serialized.object, data, services.store.store_project_attributes_values(project_serialized.object, data,
"severities", "severities",
serializers.SeverityExportSerializer) validators.SeverityExportValidator)
if ("points" in data or "issues_types" in data or if ("points" in data or "issues_types" in data or
"issues_statuses" in data or "us_statuses" 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: if "userstorycustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data, services.store.store_custom_attributes(project_serialized.object, data,
"userstorycustomattributes", "userstorycustomattributes",
serializers.UserStoryCustomAttributeExportSerializer) validators.UserStoryCustomAttributeExportValidator)
if "taskcustomattributes" in data: if "taskcustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data, services.store.store_custom_attributes(project_serialized.object, data,
"taskcustomattributes", "taskcustomattributes",
serializers.TaskCustomAttributeExportSerializer) validators.TaskCustomAttributeExportValidator)
if "issuecustomattributes" in data: if "issuecustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data, services.store.store_custom_attributes(project_serialized.object, data,
"issuecustomattributes", "issuecustomattributes",
serializers.IssueCustomAttributeExportSerializer) validators.IssueCustomAttributeExportValidator)
# Is there any error? # Is there any error?
errors = services.store.get_errors() errors = services.store.get_errors()
@ -201,7 +200,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
# Importer process is OK # Importer process is OK
response_data = project_serialized.data response_data = serializers.ProjectExportSerializer(project_serialized.object).data
response_data['id'] = project_serialized.object.id response_data['id'] = project_serialized.object.id
headers = self.get_success_headers(response_data) headers = self.get_success_headers(response_data)
return response.Created(response_data, headers=headers) return response.Created(response_data, headers=headers)
@ -218,8 +217,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(milestone.data) data = serializers.MilestoneExportSerializer(milestone.object).data
return response.Created(milestone.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@detail_route(methods=['post']) @detail_route(methods=['post'])
@method_decorator(atomic) @method_decorator(atomic)
@ -233,8 +233,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(us.data) data = serializers.UserStoryExportSerializer(us.object).data
return response.Created(us.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@detail_route(methods=['post']) @detail_route(methods=['post'])
@method_decorator(atomic) @method_decorator(atomic)
@ -251,8 +252,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(task.data) data = serializers.TaskExportSerializer(task.object).data
return response.Created(task.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@detail_route(methods=['post']) @detail_route(methods=['post'])
@method_decorator(atomic) @method_decorator(atomic)
@ -269,8 +271,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(issue.data) data = serializers.IssueExportSerializer(issue.object).data
return response.Created(issue.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@detail_route(methods=['post']) @detail_route(methods=['post'])
@method_decorator(atomic) @method_decorator(atomic)
@ -284,8 +287,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(wiki_page.data) data = serializers.WikiPageExportSerializer(wiki_page.object).data
return response.Created(wiki_page.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@detail_route(methods=['post']) @detail_route(methods=['post'])
@method_decorator(atomic) @method_decorator(atomic)
@ -299,8 +303,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors: if errors:
raise exc.BadRequest(errors) raise exc.BadRequest(errors)
headers = self.get_success_headers(wiki_link.data) data = serializers.WikiLinkExportSerializer(wiki_link.object).data
return response.Created(wiki_link.data, headers=headers) headers = self.get_success_headers(data)
return response.Created(data, headers=headers)
@list_route(methods=["POST"]) @list_route(methods=["POST"])
@method_decorator(atomic) @method_decorator(atomic)

View File

@ -21,24 +21,15 @@ import os
import copy import copy
from collections import OrderedDict 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.api import serializers
from taiga.base.exceptions import ValidationError from taiga.base.fields import Field
from taiga.base.fields import JsonField
from taiga.mdrender.service import render as mdrender
from taiga.users import models as users_models 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): class FileField(Field):
read_only = False def to_value(self, obj):
def to_native(self, obj):
if not obj: if not obj:
return None return None
@ -49,202 +40,74 @@ class FileField(serializers.WritableField):
("name", os.path.basename(obj.name)), ("name", os.path.basename(obj.name)),
]) ])
def from_native(self, data):
if not data:
return None
decoded_data = b'' class ContentTypeField(Field):
# The original file was encoded by chunks but we don't really know its def to_value(self, obj):
# 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):
if obj: if obj:
return [obj.app_label, obj.model] return [obj.app_label, obj.model]
return None return None
def from_native(self, data):
try:
return ContentType.objects.get_by_natural_key(*data)
except Exception:
return None
class UserRelatedField(Field):
class RelatedNoneSafeField(serializers.RelatedField): def to_value(self, obj):
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: if obj:
return obj.email return obj.email
return None return None
def from_native(self, data):
try:
return cached_get_user_by_email(data)
except users_models.User.DoesNotExist:
return None
class UserPkField(Field):
class UserPkField(serializers.RelatedField): def to_value(self, obj):
read_only = False
def to_native(self, obj):
try: try:
user = cached_get_user_by_pk(obj) user = cached_get_user_by_pk(obj)
return user.email return user.email
except users_models.User.DoesNotExist: except users_models.User.DoesNotExist:
return None 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): def __init__(self, slug_field, *args, **kwargs):
self.slug_field = slug_field self.slug_field = slug_field
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_native(self, obj): def to_value(self, obj):
if obj: if obj:
return getattr(obj, self.slug_field) return getattr(obj, self.slug_field)
return None 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(Field):
class HistoryUserField(JsonField): def to_value(self, obj):
def to_native(self, obj):
if obj is None or obj == {}: if obj is None or obj == {}:
return [] return []
try: try:
user = cached_get_user_by_pk(obj['pk']) user = cached_get_user_by_pk(obj['pk'])
except users_models.User.DoesNotExist: except users_models.User.DoesNotExist:
user = None user = None
return (UserRelatedField().to_native(user), obj['name']) return (UserRelatedField().to_value(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): class HistoryValuesField(Field):
def to_native(self, obj): def to_value(self, obj):
if obj is None: if obj is None:
return [] return []
if "users" in obj: if "users" in obj:
obj['users'] = list(map(UserPkField().to_native, obj['users'])) obj['users'] = list(map(UserPkField().to_value, obj['users']))
return obj 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(Field):
class HistoryDiffField(JsonField): def to_value(self, obj):
def to_native(self, obj):
if obj is None: if obj is None:
return [] return []
if "assigned_to" in obj: 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 return obj
def from_native(self, data):
if data is None:
return []
if "assigned_to" in data: class TimelineDataField(Field):
data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) def to_value(self, data):
return data
class TimelineDataField(serializers.WritableField):
read_only = False
def to_native(self, data):
new_data = copy.deepcopy(data) new_data = copy.deepcopy(data)
try: try:
user = cached_get_user_by_pk(new_data["user"]["id"]) user = cached_get_user_by_pk(new_data["user"]["id"])
@ -253,14 +116,3 @@ class TimelineDataField(serializers.WritableField):
except Exception: except Exception:
pass pass
return new_data 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

View File

@ -16,56 +16,62 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from taiga.base.api import serializers 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.history import models as history_models
from taiga.projects.attachments import models as attachments_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 taiga.projects.history import services as history_service
from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
JsonField, HistoryValuesField, CommentField, FileField) HistoryValuesField, FileField)
class HistoryExportSerializer(serializers.ModelSerializer): class HistoryExportSerializer(serializers.LightSerializer):
user = HistoryUserField() user = HistoryUserField()
diff = HistoryDiffField(required=False) diff = HistoryDiffField()
snapshot = JsonField(required=False) snapshot = Field()
values = HistoryValuesField(required=False) values = HistoryValuesField()
comment = CommentField(required=False) comment = Field()
delete_comment_date = serializers.DateTimeField(required=False) delete_comment_date = DateTimeField()
delete_comment_user = HistoryUserField(required=False) delete_comment_user = HistoryUserField()
comment_versions = Field()
class Meta: created_at = DateTimeField()
model = history_models.HistoryEntry edit_comment_date = DateTimeField()
exclude = ("id", "comment_html", "key") is_hidden = Field()
is_snapshot = Field()
type = Field()
class HistoryExportSerializerMixin(serializers.ModelSerializer): class HistoryExportSerializerMixin(serializers.LightSerializer):
history = serializers.SerializerMethodField("get_history") history = MethodField("get_history")
def get_history(self, obj): def get_history(self, obj):
history_qs = history_service.get_history_queryset_by_model_instance(obj, history_qs = history_service.get_history_queryset_by_model_instance(
types=(history_models.HistoryType.change, history_models.HistoryType.create,)) obj,
types=(history_models.HistoryType.change, history_models.HistoryType.create,)
)
return HistoryExportSerializer(history_qs, many=True).data return HistoryExportSerializer(history_qs, many=True).data
class AttachmentExportSerializer(serializers.ModelSerializer): class AttachmentExportSerializer(serializers.LightSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField()
attached_file = FileField() attached_file = FileField()
modified_date = serializers.DateTimeField(required=False) created_date = DateTimeField()
modified_date = DateTimeField()
class Meta: description = Field()
model = attachments_models.Attachment is_deprecated = Field()
exclude = ('id', 'content_type', 'object_id', 'project') name = Field()
order = Field()
sha1 = Field()
size = Field()
class AttachmentExportSerializerMixin(serializers.ModelSerializer): class AttachmentExportSerializerMixin(serializers.LightSerializer):
attachments = serializers.SerializerMethodField("get_attachments") attachments = MethodField()
def get_attachments(self, obj): def get_attachments(self, obj):
content_type = ContentType.objects.get_for_model(obj.__class__) content_type = ContentType.objects.get_for_model(obj.__class__)
@ -74,8 +80,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer):
return AttachmentExportSerializer(attachments_qs, many=True).data return AttachmentExportSerializer(attachments_qs, many=True).data
class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): class CustomAttributesValuesExportSerializerMixin(serializers.LightSerializer):
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") custom_attributes_values = MethodField("get_custom_attributes_values")
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
raise NotImplementedError() raise NotImplementedError()
@ -99,43 +105,8 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
return None return None
class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer): class WatcheableObjectLightSerializerMixin(serializers.LightSerializer):
watchers = UserRelatedField(many=True, required=False) watchers = MethodField()
def __init__(self, *args, **kwargs): def get_watchers(self, obj):
self._watchers_field = self.base_fields.pop("watchers", None) return [user.email for user in obj.get_watchers()]
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

View File

@ -16,231 +16,183 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField, PgArrayField from taiga.base.fields import Field, DateTimeField, MethodField
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 taiga.projects.votes import services as votes_service from taiga.projects.votes import services as votes_service
from .fields import (FileField, UserRelatedField, from .fields import (FileField, UserRelatedField, TimelineDataField,
ProjectRelatedField, ContentTypeField, SlugRelatedField)
TimelineDataField, ContentTypeField)
from .mixins import (HistoryExportSerializerMixin, from .mixins import (HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, AttachmentExportSerializerMixin,
CustomAttributesValuesExportSerializerMixin, CustomAttributesValuesExportSerializerMixin,
WatcheableObjectModelSerializerMixin) WatcheableObjectLightSerializerMixin)
from .cache import (_custom_tasks_attributes_cache, from .cache import (_custom_tasks_attributes_cache,
_custom_userstories_attributes_cache, _custom_userstories_attributes_cache,
_custom_issues_attributes_cache) _custom_issues_attributes_cache)
class PointsExportSerializer(serializers.ModelSerializer): class RelatedExportSerializer(serializers.LightSerializer):
class Meta: def to_value(self, value):
model = projects_models.Points if hasattr(value, 'all'):
exclude = ('id', 'project') return super().to_value(value.all())
return super().to_value(value)
class UserStoryStatusExportSerializer(serializers.ModelSerializer): class PointsExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.UserStoryStatus order = Field()
exclude = ('id', 'project') value = Field()
class TaskStatusExportSerializer(serializers.ModelSerializer): class UserStoryStatusExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.TaskStatus slug = Field()
exclude = ('id', 'project') order = Field()
is_closed = Field()
is_archived = Field()
color = Field()
wip_limit = Field()
class IssueStatusExportSerializer(serializers.ModelSerializer): class TaskStatusExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.IssueStatus slug = Field()
exclude = ('id', 'project') order = Field()
is_closed = Field()
color = Field()
class PriorityExportSerializer(serializers.ModelSerializer): class IssueStatusExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.Priority slug = Field()
exclude = ('id', 'project') order = Field()
is_closed = Field()
color = Field()
class SeverityExportSerializer(serializers.ModelSerializer): class PriorityExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.Severity order = Field()
exclude = ('id', 'project') color = Field()
class IssueTypeExportSerializer(serializers.ModelSerializer): class SeverityExportSerializer(RelatedExportSerializer):
class Meta: name = Field()
model = projects_models.IssueType order = Field()
exclude = ('id', 'project') color = Field()
class RoleExportSerializer(serializers.ModelSerializer): class IssueTypeExportSerializer(RelatedExportSerializer):
permissions = PgArrayField(required=False) name = Field()
order = Field()
class Meta: color = Field()
model = users_models.Role
exclude = ('id', 'project')
class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer): class RoleExportSerializer(RelatedExportSerializer):
modified_date = serializers.DateTimeField(required=False) name = Field()
slug = Field()
class Meta: order = Field()
model = custom_attributes_models.UserStoryCustomAttribute computable = Field()
exclude = ('id', 'project') permissions = Field()
class TaskCustomAttributeExportSerializer(serializers.ModelSerializer): class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer):
modified_date = serializers.DateTimeField(required=False) name = Field()
description = Field()
class Meta: type = Field()
model = custom_attributes_models.TaskCustomAttribute order = Field()
exclude = ('id', 'project') created_date = DateTimeField()
modified_date = DateTimeField()
class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): class TaskCustomAttributeExportSerializer(RelatedExportSerializer):
modified_date = serializers.DateTimeField(required=False) name = Field()
description = Field()
class Meta: type = Field()
model = custom_attributes_models.IssueCustomAttribute order = Field()
exclude = ('id', 'project') created_date = DateTimeField()
modified_date = DateTimeField()
class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): class IssueCustomAttributeExportSerializer(RelatedExportSerializer):
attributes_values = JsonField(source="attributes_values", required=True) name = Field()
_custom_attribute_model = None description = Field()
_container_field = None type = Field()
order = Field()
created_date = DateTimeField()
modified_date = DateTimeField()
class Meta:
exclude = ("id",)
def validate_attributes_values(self, attrs, source): class BaseCustomAttributesValuesExportSerializer(RelatedExportSerializer):
# values must be a dict attributes_values = Field(required=True)
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 UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute user_story = Field(attr="user_story.id")
_container_model = "userstories.UserStory"
_container_field = "user_story"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.UserStoryCustomAttributesValues
class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.TaskCustomAttribute task = Field(attr="task.id")
_container_field = "task"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.TaskCustomAttributesValues
class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.IssueCustomAttribute issue = Field(attr="issue.id")
_container_field = "issue"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.IssueCustomAttributesValues
class MembershipExportSerializer(serializers.ModelSerializer): class MembershipExportSerializer(RelatedExportSerializer):
user = UserRelatedField(required=False) user = UserRelatedField()
role = ProjectRelatedField(slug_field="name") role = SlugRelatedField(slug_field="name")
invited_by = UserRelatedField(required=False) invited_by = UserRelatedField()
is_admin = Field()
class Meta: email = Field()
model = projects_models.Membership created_at = DateTimeField()
exclude = ('id', 'project', 'token') invitation_extra_text = Field()
user_order = Field()
def full_clean(self, instance):
return instance
class RolePointsExportSerializer(serializers.ModelSerializer): class RolePointsExportSerializer(RelatedExportSerializer):
role = ProjectRelatedField(slug_field="name") role = SlugRelatedField(slug_field="name")
points = ProjectRelatedField(slug_field="name") points = SlugRelatedField(slug_field="name")
class Meta:
model = userstories_models.RolePoints
exclude = ('id', 'user_story')
class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): class MilestoneExportSerializer(WatcheableObjectLightSerializerMixin, RelatedExportSerializer):
owner = UserRelatedField(required=False) name = Field()
modified_date = serializers.DateTimeField(required=False) owner = UserRelatedField()
estimated_start = serializers.DateField(required=False) created_date = DateTimeField()
estimated_finish = serializers.DateField(required=False) modified_date = DateTimeField()
estimated_start = Field()
def __init__(self, *args, **kwargs): estimated_finish = Field()
project = kwargs.pop('project', None) slug = Field()
super(MilestoneExportSerializer, self).__init__(*args, **kwargs) closed = Field()
if project: disponibility = Field()
self.project = project order = Field()
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 TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin,
AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): HistoryExportSerializerMixin,
owner = UserRelatedField(required=False) AttachmentExportSerializerMixin,
status = ProjectRelatedField(slug_field="name") WatcheableObjectLightSerializerMixin,
user_story = ProjectRelatedField(slug_field="ref", required=False) RelatedExportSerializer):
milestone = ProjectRelatedField(slug_field="name", required=False) owner = UserRelatedField()
assigned_to = UserRelatedField(required=False) status = SlugRelatedField(slug_field="name")
modified_date = serializers.DateTimeField(required=False) user_story = SlugRelatedField(slug_field="ref")
milestone = SlugRelatedField(slug_field="name")
class Meta: assigned_to = UserRelatedField()
model = tasks_models.Task modified_date = DateTimeField()
exclude = ('id', 'project') 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): def custom_attributes_queryset(self, project):
if project.id not in _custom_tasks_attributes_cache: if project.id not in _custom_tasks_attributes_cache:
@ -248,19 +200,35 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
return _custom_tasks_attributes_cache[project.id] return _custom_tasks_attributes_cache[project.id]
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): HistoryExportSerializerMixin,
role_points = RolePointsExportSerializer(many=True, required=False) AttachmentExportSerializerMixin,
owner = UserRelatedField(required=False) WatcheableObjectLightSerializerMixin,
assigned_to = UserRelatedField(required=False) RelatedExportSerializer):
status = ProjectRelatedField(slug_field="name") role_points = RolePointsExportSerializer(many=True)
milestone = ProjectRelatedField(slug_field="name", required=False) owner = UserRelatedField()
modified_date = serializers.DateTimeField(required=False) assigned_to = UserRelatedField()
generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) status = SlugRelatedField(slug_field="name")
milestone = SlugRelatedField(slug_field="name")
class Meta: modified_date = DateTimeField()
model = userstories_models.UserStory created_date = DateTimeField()
exclude = ('id', 'project', 'points', 'tasks') 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): def custom_attributes_queryset(self, project):
if project.id not in _custom_userstories_attributes_cache: if project.id not in _custom_userstories_attributes_cache:
@ -270,21 +238,31 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
return _custom_userstories_attributes_cache[project.id] return _custom_userstories_attributes_cache[project.id]
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): HistoryExportSerializerMixin,
owner = UserRelatedField(required=False) AttachmentExportSerializerMixin,
status = ProjectRelatedField(slug_field="name") WatcheableObjectLightSerializerMixin,
assigned_to = UserRelatedField(required=False) RelatedExportSerializer):
priority = ProjectRelatedField(slug_field="name") owner = UserRelatedField()
severity = ProjectRelatedField(slug_field="name") status = SlugRelatedField(slug_field="name")
type = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField()
milestone = ProjectRelatedField(slug_field="name", required=False) priority = SlugRelatedField(slug_field="name")
votes = serializers.SerializerMethodField("get_votes") severity = SlugRelatedField(slug_field="name")
modified_date = serializers.DateTimeField(required=False) 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: ref = Field()
model = issues_models.Issue subject = Field()
exclude = ('id', 'project') description = Field()
external_reference = Field()
version = Field()
blocked_note = Field()
is_blocked = Field()
tags = Field()
def get_votes(self, obj): def get_votes(self, obj):
return [x.email for x in votes_service.get_voters(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] return _custom_issues_attributes_cache[project.id]
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, class WikiPageExportSerializer(HistoryExportSerializerMixin,
WatcheableObjectModelSerializerMixin): AttachmentExportSerializerMixin,
owner = UserRelatedField(required=False) WatcheableObjectLightSerializerMixin,
last_modifier = UserRelatedField(required=False) RelatedExportSerializer):
modified_date = serializers.DateTimeField(required=False) slug = Field()
owner = UserRelatedField()
class Meta: last_modifier = UserRelatedField()
model = wiki_models.WikiPage modified_date = DateTimeField()
exclude = ('id', 'project') created_date = DateTimeField()
content = Field()
version = Field()
class WikiLinkExportSerializer(serializers.ModelSerializer): class WikiLinkExportSerializer(RelatedExportSerializer):
class Meta: title = Field()
model = wiki_models.WikiLink href = Field()
exclude = ('id', 'project') order = Field()
class TimelineExportSerializer(serializers.ModelSerializer): class TimelineExportSerializer(RelatedExportSerializer):
data = TimelineDataField() data = TimelineDataField()
data_content_type = ContentTypeField() data_content_type = ContentTypeField()
event_type = Field()
class Meta: created = DateTimeField()
model = timeline_models.Timeline
exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
logo = FileField(required=False) name = Field()
anon_permissions = PgArrayField(required=False) slug = Field()
public_permissions = PgArrayField(required=False) description = Field()
modified_date = serializers.DateTimeField(required=False) created_date = DateTimeField()
roles = RoleExportSerializer(many=True, required=False) logo = FileField()
owner = UserRelatedField(required=False) total_milestones = Field()
memberships = MembershipExportSerializer(many=True, required=False) total_story_points = Field()
points = PointsExportSerializer(many=True, required=False) is_backlog_activated = Field()
us_statuses = UserStoryStatusExportSerializer(many=True, required=False) is_kanban_activated = Field()
task_statuses = TaskStatusExportSerializer(many=True, required=False) is_wiki_activated = Field()
issue_types = IssueTypeExportSerializer(many=True, required=False) is_issues_activated = Field()
issue_statuses = IssueStatusExportSerializer(many=True, required=False) videoconferences = Field()
priorities = PriorityExportSerializer(many=True, required=False) videoconferences_extra_data = Field()
severities = SeverityExportSerializer(many=True, required=False) creation_template = SlugRelatedField(slug_field="slug")
tags_colors = JsonField(required=False) is_private = Field()
default_points = serializers.SlugRelatedField(slug_field="name", required=False) is_featured = Field()
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) is_looking_for_people = Field()
default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) looking_for_people_note = Field()
default_priority = serializers.SlugRelatedField(slug_field="name", required=False) userstories_csv_uuid = Field()
default_severity = serializers.SlugRelatedField(slug_field="name", required=False) tasks_csv_uuid = Field()
default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) issues_csv_uuid = Field()
default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) transfer_token = Field()
userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) blocked_code = Field()
taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) totals_updated_datetime = DateTimeField()
issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) total_fans = Field()
user_stories = UserStoryExportSerializer(many=True, required=False) total_fans_last_week = Field()
tasks = TaskExportSerializer(many=True, required=False) total_fans_last_month = Field()
milestones = MilestoneExportSerializer(many=True, required=False) total_fans_last_year = Field()
issues = IssueExportSerializer(many=True, required=False) total_activity = Field()
wiki_links = WikiLinkExportSerializer(many=True, required=False) total_activity_last_week = Field()
wiki_pages = WikiPageExportSerializer(many=True, required=False) total_activity_last_month = Field()
total_activity_last_year = Field()
class Meta: anon_permissions = Field()
model = projects_models.Project public_permissions = Field()
exclude = ('id', 'creation_template', 'members') 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()

View File

@ -19,13 +19,10 @@
# This makes all code that import services works and # This makes all code that import services works and
# is not the baddest practice ;) # is not the baddest practice ;)
import base64
import gc import gc
import os
from django.core.files.storage import default_storage
from taiga.base.utils import json from taiga.base.utils import json
from taiga.base.fields import MethodField
from taiga.timeline.service import get_project_timeline from taiga.timeline.service import get_project_timeline
from taiga.base.api.fields import get_component from taiga.base.api.fields import get_component
@ -37,31 +34,29 @@ def render_project(project, outfile, chunk_size = 8190):
outfile.write(b'{\n') outfile.write(b'{\n')
first_field = True first_field = True
for field_name in serializer.fields.keys(): for field_name in serializer._field_map.keys():
# Avoid writing "," in the last element # Avoid writing "," in the last element
if not first_field: if not first_field:
outfile.write(b",\n") outfile.write(b",\n")
else: else:
first_field = False first_field = False
field = serializer.fields.get(field_name) field = serializer._field_map.get(field_name)
field.initialize(parent=serializer, field_name=field_name) # field.initialize(parent=serializer, field_name=field_name)
# These four "special" fields hava attachments so we use them in a special way # These four "special" fields hava attachments so we use them in a special way
if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]: if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]:
value = get_component(project, field_name) value = get_component(project, field_name)
if field_name != "wiki_pages": 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": if field_name == "issues":
value = value.select_related('severity', 'priority', 'type') value = value.select_related('severity', 'priority', 'type')
value = value.prefetch_related('history_entry', 'attachments') value = value.prefetch_related('history_entry', 'attachments')
outfile.write('"{}": [\n'.format(field_name).encode()) 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 first_item = True
for item in value.iterator(): for item in value.iterator():
# Avoid writing "," in the last element # Avoid writing "," in the last element
@ -70,47 +65,18 @@ def render_project(project, outfile, chunk_size = 8190):
else: else:
first_item = False first_item = False
field.many = False
dumped_value = json.dumps(field.to_native(item)) dumped_value = json.dumps(field.to_value(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()) 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']}')
outfile.flush() outfile.flush()
gc.collect() gc.collect()
outfile.write(b']') outfile.write(b']')
else: 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()) outfile.write('"{}": {}'.format(field_name, json.dumps(value)).encode())
# Generate the timeline # Generate the timeline
@ -127,4 +93,3 @@ def render_project(project, outfile, chunk_size = 8190):
outfile.write(dumped_value.encode()) outfile.write(dumped_value.encode())
outfile.write(b']}\n') outfile.write(b']}\n')

View File

@ -39,7 +39,7 @@ from taiga.timeline.service import build_project_namespace
from taiga.users import services as users_service from taiga.users import services as users_service
from .. import exceptions as err 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: if key not in excluded_fields:
project_data[key] = value project_data[key] = value
serialized = serializers.ProjectExportSerializer(data=project_data) validator = validators.ProjectExportValidator(data=project_data)
if serialized.is_valid(): if validator.is_valid():
serialized.object._importing = True validator.object._importing = True
serialized.object.save() validator.object.save()
serialized.save_watchers() validator.save_watchers()
return serialized return validator
add_errors("project", serialized.errors) add_errors("project", validator.errors)
return None return None
@ -133,54 +133,55 @@ def _store_custom_attributes_values(obj, data_values, obj_field, serializer_clas
def _store_attachment(project, obj, attachment): def _store_attachment(project, obj, attachment):
serialized = serializers.AttachmentExportSerializer(data=attachment) validator = validators.AttachmentExportValidator(data=attachment)
if serialized.is_valid(): if validator.is_valid():
serialized.object.content_type = ContentType.objects.get_for_model(obj.__class__) validator.object.content_type = ContentType.objects.get_for_model(obj.__class__)
serialized.object.object_id = obj.id validator.object.object_id = obj.id
serialized.object.project = project validator.object.project = project
if serialized.object.owner is None: if validator.object.owner is None:
serialized.object.owner = serialized.object.project.owner validator.object.owner = validator.object.project.owner
serialized.object._importing = True validator.object._importing = True
serialized.object.size = serialized.object.attached_file.size validator.object.size = validator.object.attached_file.size
serialized.object.name = os.path.basename(serialized.object.attached_file.name) validator.object.name = os.path.basename(validator.object.attached_file.name)
serialized.save() validator.save()
return serialized return validator
add_errors("attachments", serialized.errors) add_errors("attachments", validator.errors)
return serialized return validator
def _store_history(project, obj, history): def _store_history(project, obj, history):
serialized = serializers.HistoryExportSerializer(data=history, context={"project": project}) validator = validators.HistoryExportValidator(data=history, context={"project": project})
if serialized.is_valid(): if validator.is_valid():
serialized.object.key = make_key_from_model_object(obj) validator.object.key = make_key_from_model_object(obj)
if serialized.object.diff is None: if validator.object.diff is None:
serialized.object.diff = [] validator.object.diff = []
serialized.object._importing = True validator.object.project_id = project.id
serialized.save() validator.object._importing = True
return serialized validator.save()
add_errors("history", serialized.errors) return validator
return serialized add_errors("history", validator.errors)
return validator
## ROLES ## ROLES
def _store_role(project, role): def _store_role(project, role):
serialized = serializers.RoleExportSerializer(data=role) validator = validators.RoleExportValidator(data=role)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
return serialized return validator
add_errors("roles", serialized.errors) add_errors("roles", validator.errors)
return None return None
def store_roles(project, data): def store_roles(project, data):
results = [] results = []
for role in data.get("roles", []): for role in data.get("roles", []):
serialized = _store_role(project, role) validator = _store_role(project, role)
if serialized: if validator:
results.append(serialized) results.append(validator)
return results return results
@ -188,17 +189,17 @@ def store_roles(project, data):
## MEMGERSHIPS ## MEMGERSHIPS
def _store_membership(project, membership): def _store_membership(project, membership):
serialized = serializers.MembershipExportSerializer(data=membership, context={"project": project}) validator = validators.MembershipExportValidator(data=membership, context={"project": project})
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.object.token = str(uuid.uuid1()) validator.object.token = str(uuid.uuid1())
serialized.object.user = find_invited_user(serialized.object.email, validator.object.user = find_invited_user(validator.object.email,
default=serialized.object.user) default=validator.object.user)
serialized.save() validator.save()
return serialized return validator
add_errors("memberships", serialized.errors) add_errors("memberships", validator.errors)
return None return None
@ -212,13 +213,13 @@ def store_memberships(project, data):
## PROJECT ATTRIBUTES ## PROJECT ATTRIBUTES
def _store_project_attribute_value(project, data, field, serializer): def _store_project_attribute_value(project, data, field, serializer):
serialized = serializer(data=data) validator = serializer(data=data)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
return serialized.object return validator.object
add_errors(field, serialized.errors) add_errors(field, validator.errors)
return None return None
@ -253,13 +254,13 @@ def store_default_project_attributes_values(project, data):
## CUSTOM ATTRIBUTES ## CUSTOM ATTRIBUTES
def _store_custom_attribute(project, data, field, serializer): def _store_custom_attribute(project, data, field, serializer):
serialized = serializer(data=data) validator = serializer(data=data)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
return serialized.object return validator.object
add_errors(field, serialized.errors) add_errors(field, validator.errors)
return None return None
@ -273,19 +274,19 @@ def store_custom_attributes(project, data, field, serializer):
## MILESTONE ## MILESTONE
def store_milestone(project, milestone): def store_milestone(project, milestone):
serialized = serializers.MilestoneExportSerializer(data=milestone, project=project) validator = validators.MilestoneExportValidator(data=milestone, project=project)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
serialized.save_watchers() validator.save_watchers()
for task_without_us in milestone.get("tasks_without_us", []): for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None task_without_us["user_story"] = None
store_task(project, task_without_us) store_task(project, task_without_us)
return serialized return validator
add_errors("milestones", serialized.errors) add_errors("milestones", validator.errors)
return None return None
@ -300,20 +301,20 @@ def store_milestones(project, data):
## USER STORIES ## USER STORIES
def _store_role_point(project, us, role_point): def _store_role_point(project, us, role_point):
serialized = serializers.RolePointsExportSerializer(data=role_point, context={"project": project}) validator = validators.RolePointsExportValidator(data=role_point, context={"project": project})
if serialized.is_valid(): if validator.is_valid():
try: try:
existing_role_point = us.role_points.get(role=serialized.object.role) existing_role_point = us.role_points.get(role=validator.object.role)
existing_role_point.points = serialized.object.points existing_role_point.points = validator.object.points
existing_role_point.save() existing_role_point.save()
return existing_role_point return existing_role_point
except RolePoints.DoesNotExist: except RolePoints.DoesNotExist:
serialized.object.user_story = us validator.object.user_story = us
serialized.save() validator.save()
return serialized.object return validator.object
add_errors("role_points", serialized.errors) add_errors("role_points", validator.errors)
return None return None
def store_user_story(project, data): 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 us_data = {key: value for key, value in data.items() if key not in
["role_points", "custom_attributes_values"]} ["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(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
if serialized.object.owner is None: if validator.object.owner is None:
serialized.object.owner = serialized.object.project.owner validator.object.owner = validator.object.project.owner
serialized.object._importing = True validator.object._importing = True
serialized.object._not_notify = True validator.object._not_notify = True
serialized.save() validator.save()
serialized.save_watchers() validator.save_watchers()
if serialized.object.ref: if validator.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name): if not seq.exists(sequence_name):
seq.create(sequence_name) seq.create(sequence_name)
seq.set_max(sequence_name, serialized.object.ref) seq.set_max(sequence_name, validator.object.ref)
else: else:
serialized.object.ref, _ = refs.make_reference(serialized.object, project) validator.object.ref, _ = refs.make_reference(validator.object, project)
serialized.object.save() validator.object.save()
for us_attachment in data.get("attachments", []): 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", []): 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", []) history_entries = data.get("history", [])
for history in history_entries: for history in history_entries:
_store_history(project, serialized.object, history) _store_history(project, validator.object, history)
if not history_entries: 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) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: 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_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values) custom_attributes, custom_attributes_values)
_store_custom_attributes_values(serialized.object, custom_attributes_values, _store_custom_attributes_values(validator.object, custom_attributes_values,
"user_story", serializers.UserStoryCustomAttributesValuesExportSerializer) "user_story", validators.UserStoryCustomAttributesValuesExportValidator)
return serialized return validator
add_errors("user_stories", serialized.errors) add_errors("user_stories", validator.errors)
return None return None
@ -384,47 +385,47 @@ def store_task(project, data):
if "status" not in data and project.default_task_status: if "status" not in data and project.default_task_status:
data["status"] = project.default_task_status.name data["status"] = project.default_task_status.name
serialized = serializers.TaskExportSerializer(data=data, context={"project": project}) validator = validators.TaskExportValidator(data=data, context={"project": project})
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
if serialized.object.owner is None: if validator.object.owner is None:
serialized.object.owner = serialized.object.project.owner validator.object.owner = validator.object.project.owner
serialized.object._importing = True validator.object._importing = True
serialized.object._not_notify = True validator.object._not_notify = True
serialized.save() validator.save()
serialized.save_watchers() validator.save_watchers()
if serialized.object.ref: if validator.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name): if not seq.exists(sequence_name):
seq.create(sequence_name) seq.create(sequence_name)
seq.set_max(sequence_name, serialized.object.ref) seq.set_max(sequence_name, validator.object.ref)
else: else:
serialized.object.ref, _ = refs.make_reference(serialized.object, project) validator.object.ref, _ = refs.make_reference(validator.object, project)
serialized.object.save() validator.object.save()
for task_attachment in data.get("attachments", []): 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", []) history_entries = data.get("history", [])
for history in history_entries: for history in history_entries:
_store_history(project, serialized.object, history) _store_history(project, validator.object, history)
if not history_entries: 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) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: 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_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values) custom_attributes, custom_attributes_values)
_store_custom_attributes_values(serialized.object, custom_attributes_values, _store_custom_attributes_values(validator.object, custom_attributes_values,
"task", serializers.TaskCustomAttributesValuesExportSerializer) "task", validators.TaskCustomAttributesValuesExportValidator)
return serialized return validator
add_errors("tasks", serialized.errors) add_errors("tasks", validator.errors)
return None return None
@ -439,7 +440,7 @@ def store_tasks(project, data):
## ISSUES ## ISSUES
def store_issue(project, data): 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: if "type" not in data and project.default_issue_type:
data["type"] = project.default_issue_type.name 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: if "severity" not in data and project.default_severity:
data["severity"] = project.default_severity.name data["severity"] = project.default_severity.name
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
if serialized.object.owner is None: if validator.object.owner is None:
serialized.object.owner = serialized.object.project.owner validator.object.owner = validator.object.project.owner
serialized.object._importing = True validator.object._importing = True
serialized.object._not_notify = True validator.object._not_notify = True
serialized.save() validator.save()
serialized.save_watchers() validator.save_watchers()
if serialized.object.ref: if validator.object.ref:
sequence_name = refs.make_sequence_name(project) sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name): if not seq.exists(sequence_name):
seq.create(sequence_name) seq.create(sequence_name)
seq.set_max(sequence_name, serialized.object.ref) seq.set_max(sequence_name, validator.object.ref)
else: else:
serialized.object.ref, _ = refs.make_reference(serialized.object, project) validator.object.ref, _ = refs.make_reference(validator.object, project)
serialized.object.save() validator.object.save()
for attachment in data.get("attachments", []): for attachment in data.get("attachments", []):
_store_attachment(project, serialized.object, attachment) _store_attachment(project, validator.object, attachment)
history_entries = data.get("history", []) history_entries = data.get("history", [])
for history in history_entries: for history in history_entries:
_store_history(project, serialized.object, history) _store_history(project, validator.object, history)
if not history_entries: 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) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: 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_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes, custom_attributes_values) custom_attributes, custom_attributes_values)
_store_custom_attributes_values(serialized.object, custom_attributes_values, _store_custom_attributes_values(validator.object, custom_attributes_values,
"issue", serializers.IssueCustomAttributesValuesExportSerializer) "issue", validators.IssueCustomAttributesValuesExportValidator)
return serialized return validator
add_errors("issues", serialized.errors) add_errors("issues", validator.errors)
return None return None
@ -507,29 +508,29 @@ def store_issues(project, data):
def store_wiki_page(project, wiki_page): def store_wiki_page(project, wiki_page):
wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", ""))) wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", "")))
serialized = serializers.WikiPageExportSerializer(data=wiki_page) validator = validators.WikiPageExportValidator(data=wiki_page)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
if serialized.object.owner is None: if validator.object.owner is None:
serialized.object.owner = serialized.object.project.owner validator.object.owner = validator.object.project.owner
serialized.object._importing = True validator.object._importing = True
serialized.object._not_notify = True validator.object._not_notify = True
serialized.save() validator.save()
serialized.save_watchers() validator.save_watchers()
for attachment in wiki_page.get("attachments", []): 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", []) history_entries = wiki_page.get("history", [])
for history in history_entries: for history in history_entries:
_store_history(project, serialized.object, history) _store_history(project, validator.object, history)
if not history_entries: 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 return None
@ -543,14 +544,14 @@ def store_wiki_pages(project, data):
## WIKI LINKS ## WIKI LINKS
def store_wiki_link(project, wiki_link): def store_wiki_link(project, wiki_link):
serialized = serializers.WikiLinkExportSerializer(data=wiki_link) validator = validators.WikiLinkExportValidator(data=wiki_link)
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
return serialized return validator
add_errors("wiki_links", serialized.errors) add_errors("wiki_links", validator.errors)
return None return None
@ -572,17 +573,17 @@ def store_tags_colors(project, data):
## TIMELINE ## TIMELINE
def _store_timeline_entry(project, timeline): def _store_timeline_entry(project, timeline):
serialized = serializers.TimelineExportSerializer(data=timeline, context={"project": project}) validator = validators.TimelineExportValidator(data=timeline, context={"project": project})
if serialized.is_valid(): if validator.is_valid():
serialized.object.project = project validator.object.project = project
serialized.object.namespace = build_project_namespace(project) validator.object.namespace = build_project_namespace(project)
serialized.object.object_id = project.id validator.object.object_id = project.id
serialized.object.content_type = ContentType.objects.get_for_model(project.__class__) validator.object.content_type = ContentType.objects.get_for_model(project.__class__)
serialized.object._importing = True validator.object._importing = True
serialized.save() validator.save()
return serialized return validator
add_errors("timeline", serialized.errors) add_errors("timeline", validator.errors)
return serialized return validator
def store_timeline_entries(project, data): 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): def _create_project_object(data):
# Create the project # 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) errors = get_errors(clear=True)
raise err.TaigaImportError(_("error importing project data"), None, errors=errors) 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): 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) check_if_there_is_some_error(_("error importing memberships"), project)
# Create project attributes values # Create project attributes values
store_project_attributes_values(project, data, "us_statuses", serializers.UserStoryStatusExportSerializer) store_project_attributes_values(project, data, "us_statuses", validators.UserStoryStatusExportValidator)
store_project_attributes_values(project, data, "points", serializers.PointsExportSerializer) store_project_attributes_values(project, data, "points", validators.PointsExportValidator)
store_project_attributes_values(project, data, "task_statuses", serializers.TaskStatusExportSerializer) store_project_attributes_values(project, data, "task_statuses", validators.TaskStatusExportValidator)
store_project_attributes_values(project, data, "issue_types", serializers.IssueTypeExportSerializer) store_project_attributes_values(project, data, "issue_types", validators.IssueTypeExportValidator)
store_project_attributes_values(project, data, "issue_statuses", serializers.IssueStatusExportSerializer) store_project_attributes_values(project, data, "issue_statuses", validators.IssueStatusExportValidator)
store_project_attributes_values(project, data, "priorities", serializers.PriorityExportSerializer) store_project_attributes_values(project, data, "priorities", validators.PriorityExportValidator)
store_project_attributes_values(project, data, "severities", serializers.SeverityExportSerializer) store_project_attributes_values(project, data, "severities", validators.SeverityExportValidator)
check_if_there_is_some_error(_("error importing lists of project attributes"), project) check_if_there_is_some_error(_("error importing lists of project attributes"), project)
# Create default values for project attributes # Create default values for project attributes
@ -669,11 +670,11 @@ def _populate_project_object(project, data):
# Create custom attributes # Create custom attributes
store_custom_attributes(project, data, "userstorycustomattributes", store_custom_attributes(project, data, "userstorycustomattributes",
serializers.UserStoryCustomAttributeExportSerializer) validators.UserStoryCustomAttributeExportValidator)
store_custom_attributes(project, data, "taskcustomattributes", store_custom_attributes(project, data, "taskcustomattributes",
serializers.TaskCustomAttributeExportSerializer) validators.TaskCustomAttributeExportValidator)
store_custom_attributes(project, data, "issuecustomattributes", store_custom_attributes(project, data, "issuecustomattributes",
serializers.IssueCustomAttributeExportSerializer) validators.IssueCustomAttributeExportValidator)
check_if_there_is_some_error(_("error importing custom attributes"), project) check_if_there_is_some_error(_("error importing custom attributes"), project)
# Create milestones # Create milestones

View File

@ -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

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from 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]

View File

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import 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

View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.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

View File

@ -0,0 +1,349 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.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')