Skip to content

Instantly share code, notes, and snippets.

@abirafdirp
Last active June 22, 2022 03:10
Show Gist options
  • Save abirafdirp/c11ee2d7788f3643f29f503b4f548cdd to your computer and use it in GitHub Desktop.
Save abirafdirp/c11ee2d7788f3643f29f503b4f548cdd to your computer and use it in GitHub Desktop.
Serialize tree model structure in DRF
from rest_framework import serializers
from .models import ClonedTopic, Topic, Note, ExternalDocument
# The reason Create and List is separated is that,
# if we specify depth, then somehow some of the fields are disabled/not,
# available when we do a POST, so we can't essentialy create an object!
# This issue is reproduceable you can test it for yourself,
# but as for the reason why, I am not investigate it yet.
# A separate serializer without depth is needed for CreateView.
# As for why UpdateRetrieveDelete is separated is that to update
# a foreign key field we just need the ID instead of the whole nested
# representation.
# So the idea here, the ListView will provide all of the nested data,
# and as far as the current use case, we don't need the retrieve API,
# because the main application is a SPA, all of the initial data is retrieved
# from the ListView and cached in the client.
class ExternalDocumentSerializer(serializers.ModelSerializer):
class Meta:
model = ExternalDocument
fields = '__all__'
class TopicSerializer(serializers.ModelSerializer):
content = serializers.CharField(allow_blank=True, allow_null=True)
external_documents = ExternalDocumentSerializer(many=True, allow_null=True)
class Meta:
model = Topic
fields = '__all__'
class TopicCreateSerializer(serializers.ModelSerializer):
content = serializers.CharField(allow_blank=True)
class Meta:
model = Topic
fields = ('title', 'content', 'id')
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class ClonedTopicListSerializer(serializers.ModelSerializer):
# fancytree expects these fields on ARRIVAL, so we can't do preprocessing
# at the client side, so adding these fields must be done in the backend
# Key is added because fancytree needs it, key is ID
key = serializers.SerializerMethodField()
# Title is added because fancytree needs it for the node title
title = serializers.SerializerMethodField()
children = RecursiveField(many=True)
topic = TopicSerializer()
def get_key(self, instance):
return instance.id
def get_title(self, instance):
return instance.topic.title
class Meta:
model = ClonedTopic
fields = (
'id',
'parent',
'created',
'updated',
'topic',
'position',
'title', # mandatory by fancytree, Node name
'key', # mandatory by fancytree, ID
'children',
'notes'
)
depth = 2
ordering = ('position',)
class ClonedTopicCreateSerializer(serializers.ModelSerializer):
# custom implementation, backend/frontend is coupled
root_id = serializers.IntegerField(required=False)
flat_children = serializers.ListField(
allow_empty=True,
allow_null=True,
child=serializers.JSONField(),
required=False
)
# custom implementation, backend/frontend is coupled
@staticmethod
def create_children(root, flat_children):
"""
Because we want to clone the tree, all of the parent must be relative
to the generated flat_children list itself and cannot use the original
parent.
Basically, to get the the Cloned Topic in the database we can use
the id,
but to get the parent, because it is a new tree, then the parent_id
must reference to the generated flat children's element itself,
that's why I need temporary mapping.
The root of the tree must be already created before this function runs
and supplied to the argument instead of having it in the
flat_children list,
this is because to follow DRF behavior,
we want to create A Cloned Topic,
but with children (writable nested serializer concept in DRF).
The Cloned Topic (root tree) is created first,
then we create the subsequent children.
So if parent_id is null means that the parent_id is the root.
"""
flat_children_map = []
for children in flat_children:
cloned_topic = ClonedTopic.objects.get(id=children['relative_id'])
# set pk to None to clone
cloned_topic.pk = None
cloned_topic.save()
# create a temporary mapping to hold the relative id and the
# created object
flat_children_map.append(
{
'relative_id': children['relative_id'],
'cloned_topic': cloned_topic
}
)
if not children['parent_id']:
parent = root
else:
# find the created Cloned Topic in the mapping,
# if its relative_id matches the parent_id from the client
# then that Cloned Topic is the parent.
# parent should not equal to None,
# if that's the case then the client constructed a bad
# flat_children,
# that is when one of its parent_id does not points to one of
# the relative_id in the flat_children.
parent = next((
cloned_topic_in_mapping['cloned_topic']
for cloned_topic_in_mapping
in flat_children_map
if cloned_topic_in_mapping['relative_id']
== children['parent_id']
),
None
)
assert parent
cloned_topic.parent = parent
cloned_topic.save()
# custom implementation, backend/frontend is coupled
def create(self, validated_data):
# if no root id, then it's a normal create, otherwise it's when pasting
try:
root_id = validated_data['root_id']
except KeyError:
return super(ClonedTopicCreateSerializer, self).create(
validated_data
)
# when pasting
root = ClonedTopic.objects.get(id=root_id)
# set pk to None to clone
root.pk = None
# parent will never be null, because in the frontend, we can only paste
# cloned topics into a cloned topic or see
# pasteClonedTopicsFromBuffer module implementation
root.parent = validated_data['parent']
root.position = validated_data['position']
root.save()
if validated_data['flat_children']:
self.create_children(
root=root,
flat_children=validated_data['flat_children']
)
return root
class Meta:
model = ClonedTopic
fields = '__all__'
class ClonedTopicUpdateRetrieveSerializer(serializers.ModelSerializer):
parent = serializers.PrimaryKeyRelatedField(
allow_empty=True, allow_null=True, queryset=ClonedTopic.objects.all())
class Meta:
model = ClonedTopic
fields = '__all__'
class NoteUpdateSerializer(serializers.ModelSerializer):
content = serializers.CharField(allow_blank=True)
class Meta:
model = Note
fields = '__all__'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment