-
-
Save llloo/240aa1de84dac14e8235f2ef689ad41b to your computer and use it in GitHub Desktop.
Serialize tree model structure in DRF
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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