How to serialize some nested relational models in django using DRF? - django

I have some Django models with different relations to each other — Many-to-many, and Foreignkey. By that means, I want to serialize them using djnago-rest.
Here are the models:
class CommonFieldsAbstract(models.Model):
name = models.CharField(max_length=30, unique=True)
class ServerModel(CommonFieldsAbstract):
server_ip = models.GenericIPAddressField(default='172.17.0.1')
server_port = models.IntegerField(default='9001')
class SNMPLineModel(CommonFieldsAbstract):
ip_address = models.GenericIPAddressField()
port = models.IntegerField(default=161)
class SNMPModel(CommonFieldsAbstract): # target
line = models.ForeignKey(SNMPLineModel, on_delete=CASCADE)
servers = models.ManyToManyField(ServerModel)
class MetaDataModel(models.Model):
key = models.CharField(max_length=20)
value = models.CharField(max_length=20)
snmp_device = models.ForeignKey(SNMPModel, on_delete=CASCADE)
Before, I used to use the following approach to create the JSON manually:
def meta_data_json(meta_data):
meta_data_list = []
for meta in meta_data:
meta_data_list.append({
meta.key: meta.value
})
return meta_data_list
def server_json(servers):
return [{'ip': server.server_ip,
'port': server.server_port}
for server in servers]
def create_json():
snmp = SNMPModel.objects.filter(name__contains='a-name')
return {
'name': snmp.name,
'address': snmp.line.ip_address,
'port': snmp.line.port,
'servers': server_json(snmp.servers.all()),
'meta_data': meta_data_json(MetaDataModel.objects.filter(
snmp_device=snmp.pk
)
),
'device_pk': snmp.pk
}
My Question:
Now, how can I create such an above json via django-rest-framework instead?
I don't have any problem with many-to-many fields. In fact, my problem is the foreignkey(s).
Here's what I've done so far:
# serializers.py
from rest_framework import serializers
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
fields = [
'id',
'key',
'value',
]
model = MetaDataModel
class ServerSerializer(serializers.ModelSerializer):
class Meta:
fields = [
'id',
'server_ip',
'server_port',
]
model = ServerModel
class LineSerializer(serializers.ModelSerializer):
port = serializers.RelatedField(many=True)
class Meta:
fields = '__all__'
model = SNMPLineModel
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True, read_only=True) # It is ok
meta_data = MetaDataSerializer(many=True, read_only=True) # It's not ok
line = LineSerializer(many=True, read_only=True) # It's not ok
address = serializers.CharField(source=SNMPLineModel.ip_address) # It's not ok
port = serializers.CharField(source=SNMPLineModel.port) # It's not ok
class Meta:
fields = [
'id',
'servers',
'name',
'address',
'port',
'line',
'meta_data'
]
model = SNMPModel
# views.py
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, JsonResponse
#csrf_exempt
def snippet_detail(request, name):
try:
snmp_conf = SNMPModel.objects.filter(name__contains=name)
except SNMPModel.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SNMPSerializer(snmp_conf, many=True)
return JsonResponse(serializer.data, status=200, safe=False)
# urls.py
from django.urls import path
urlpatterns = [
path('snippets/<name>/', views.snippet_detail)
]
Any help would be greatly appreciated.

The serializers.SerializerMethodField() is a useful method to add in relations like this.
get_meta_data() is a bit of magic evaluating the fieldname to call the method.
Address and port seem to be a simple relation and line.FOO should work.
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True, read_only=True) # It is ok
meta_data = serializers.SerializerMethodField()
line = serializers.SerializerMethodField()
address = serializers.CharField(source="line.ip_address", read_only=True)
port = serializers.CharField(source="line.port" , read_only=True)
class Meta:
fields = ['id', 'servers', 'name', 'address', 'port', 'line', 'meta_data']
model = SNMPModel
def get_meta_data(self, instance):
metadatamodels = MetaDataModel.objects.filter(snmp_device=instance)
serializer = MetaDataSerializer(instance=metadatamodels, many=True, read_only=True)
return serializer.data
def get_line(self, instance):
serializer = LineSerializer(instance.line, read_only=True)
return serializer.data

For solving the mentioned problems I did the following solutions:
For a Forward Foreignkey you do not need many=True
For a Reverse Foreignkey you need using related-model_set while you haven't defined related_name.
For an extra field you just need the exact queryset.
Therefore, I reached the following code snippet without using .SerializerMethodField() as Michael used on his answer:
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True)
meta_data = MetaDataSerializer(many=True, source="metadatamodel_set")
line = LineSerializer()
address = serializers.CharField(source="line.ip_address")
port = serializers.CharField(source="line.port")
class Meta:
fields = (
"id",
"servers",
"name",
"address",
"port",
"line",
"meta_data",
)
model = SNMPModel

Related

Django Rest Framework - URL With Query Parameters in Serializer

I have a Story and Post models, where a Post belongs to a Story. I want a URL to get all Posts associated with a given Story.
I was able to override the get_queryset of my PostViewSet in order to filter posts by story with URLs like http://localhost:8000/posts/?story=1/. This works beautifully if I type in the URL directly. Now I want to return this kind of url in my StorySerializer. I would like to be able to get Story responses that look like this
[
{
"url": "http://localhost:8000/stories/1/",
"title": "Hero's Journey",
"openings": 0,
"date_created": "2020-06-28T16:53:35.150630Z",
"posts": "http://localhost:8000/posts/?story=1/"
},
{
"url": "http://localhost:8000/stories/2/",
"title": "Halo 3",
"openings": 0,
"date_created": "2020-06-28T18:17:12.973586Z",
"posts": "http://localhost:8000/posts/?story=2/"
}
]
Is there DRF support for this kind of thing? I was trying to use a HyperlinkedIdentityField with 'post-list' View in my StorySerializer, but I couldn't find a combination of parameters that would work. The current exception I get is
AttributeError: 'Story' object has no attribute 'posts'
Serializers
class StorySerializer(serializers.HyperlinkedModelSerializer):
posts = serializers.HyperlinkedIdentityField(
view_name = 'post-list',
many=True,
lookup_field = 'pk',
lookup_url_kwarg = 'story',
)
class Meta:
model = models.Story
fields = ['url', 'title', 'openings', 'date_created', 'posts']
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.Post
fields = ['url', 'story', 'user', 'text', 'date_created']
Views
class StoryViewSet(viewsets.ModelViewSet):
queryset = models.Story.objects.all()
serializer_class = serializers.StorySerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = models.Post.objects.all()
serializer_class = serializers.PostSerializer
def get_queryset(self):
queryset = self.queryset
story_id = self.request.query_params.get('story', None)
if story_id is not None:
queryset = queryset.filter(story=story_id)
return queryset
Models
class Story(models.Model):
title = models.CharField(max_length=100)
date_created = models.DateTimeField(default=timezone.now)
openings = models.PositiveSmallIntegerField(default=0)
participant = models.ManyToManyField(User)
class Post(models.Model):
text = models.CharField(max_length=300)
user = models.ForeignKey(
User,
on_delete=models.PROTECT)
story = models.ForeignKey(
Story,
on_delete=models.PROTECT)
date_created = models.DateTimeField(default=timezone.now)
I was able to find a great solution here, overriding the get_url method to map 'pk' value to 'story' directly.
https://stackoverflow.com/a/27584761/7308261
from rest_framework.reverse import reverse
import urllib
class StoryPostsHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
def get_url(self, obj, view_name, request, format):
lookup_field_value = getattr(obj, self.lookup_field, None)
result = '{}?{}'.format(
reverse(view_name, kwargs={}, request=request, format=format),
urllib.parse.urlencode({'story': lookup_field_value})
)
return result
class StorySerializer(serializers.HyperlinkedModelSerializer):
posts = StoryPostsHyperlinkedIdentityField(
view_name='post-list',
)
class Meta:
model = models.Story
fields = ['url', 'title', 'openings', 'date_created', 'posts']

Represent json data with PrimaryKeyRelatedField in django

I'm designing a mailbox with Django. My code is as follows:
#models.py
class Post(models.Model):
text = models.CharField(max_length=256)
sender = models.ForeignKey(User)
receiver = models.ForeignKey(User)
class Comment(models.Model):
post = models.ForeignKey(Post)
text = models.CharField(max_length=256)
#serializers.py
class CommentSerializer(serializers.ModelSerializer):
post = serializers.PrimaryKeyRelatedField()
class Meta:
model = Comment
fields = [
'id',
'text',
'post'
]
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = [
'id',
'text',
'sender',
'receiver',
]
class MainUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email']
I tried to customize serializer and have a serializer as follows:
class PostSerializer(serializers.Field):
def to_representation(self, value):
return PostSerializer(value, context={'request': self.context['request']}).data
def to_internal_value(self, id):
try:
id = int(id)
except ValueError:
raise serializers.ValidationError("Id should be int.")
try:
post = Post.objects.get(pk=id)
except User.DoesNotExist:
raise serializers.ValidationError("Such a post does not exist")
return user
I want to represent comment objects like this
{
"post":{
"text" = "Hello"
"sender" = 1
"receiver" = 2
}
"text": "Greate"
}
My code works great but The problem is it doesn't show the Combo Box for selecting the post. I also tried to customize the PrimaryKeyRelatedField's to_represent method in this way:
class PostSerializer(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
post_id = super(PostSerializer, self).to_representation(value)
post = Post.objects.get(pk=user_id)
return PostSerializer(
user, {"context":self.context['request']}
).data
but it says the unhashable type: 'ReturnDict' and as I understand we could return anything but simple things such as int or string. Is there a way to do this?

Use serializer of model having foreign key to do CRUD on parent table in Django Rest Framework

In my API, I have two models Question and Option as shown below
class Question(models.Model):
body = models.TextField()
class Options(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
option = models.CharField(max_length=100)
is_correct = models.SmallIntegerField()
While creating a question it would be nicer the options can be created at the same time. And already existed question should not be created but the options can be changed if the options are different from previous.
I am using ModelSerializer and ModelViewSet. I use different urls and views for Question and Option.
serializers.py
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = Question
fields = '__all__'
class OptionReadSerializer(serializers.ModelSerializer):
question = QuestionSerializer(read_only=True)
class Meta:
model = Option
fields = ('question', 'option', 'is_correct')
class OptionWriteSerializer(serializer.ModelSerializer):
class Meta:
model = Option
fields = ('question', 'option', 'is_correct')
views.py
class QuestionViewSet(ModelViewSet):
seriaizer_class = QuestionSerializer
queryset = Question.objects.all()
class OptionViewSet(ModelViewSet):
queryset = Option.objects.all()
def get_serializer_class(self):
if self.request.method == 'POST':
return OptionWriteSerializer
return OptionReadSerializer
urls.py
from django.urls import include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('api/question', QuestionViewset, base_name='question')
router.register('api/option', OptionViewSet, base_name='option')
urlpatterns = [
path('', include(router.urls))
]
In this way, I always have to create questions first and then I can individually add the option for that question. I think this may not be a practical approach.
It would be nicer that question and option can be added at the same time and similar to all CRUD operations.
The expected result and posting data in JSON format are as shown below:
{
"body": "Which country won the FIFA world cup 2018",
"options": [
{
"option": "England",
"is_correct": 0
},
{
"option": "Germany",
"is_correct": 0
},
{
"option": "France",
"is_correct": 1
}
]
}
We can use PrimaryKeyRelatedField.
tldr;
I believe a Question can have multiple Options attached to it. Rather than having an Option hooked to a Question.
Something like this:
class Question(models.Model):
body = models.TextField()
options = models.ManyToManyField(Option)
class Options(models.Model):
text = models.CharField(max_length=100)
is_correct = models.BooleanField()
Then we can use PrimaryKeyRelatedField something like this:
class QuestionSerializer(serializers.ModelSerializer):
options = serializers.PrimaryKeyRelatedField(queryset=Options.objects.all(), many=True, read_only=False)
class Meta:
model = Question
fields = '__all__'
Reference : https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
In models I added related_name='options' in foreign key field of Option model
models.py
class Question(models.Model):
body = models.TextField()
class Options(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
option = models.CharField(max_length=100)
is_correct = models.SmallIntegerField()
In QuestionWriteSerializer I override the update() and create() method. For creating and updating the logic was handled from QuestionWriteSerialzer.
serializers.py
class OptionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = Option
fields = ('id', 'question', 'option', 'is_correct')
class QuestionReadSerializer(serializers.ModelSerializer):
options = OptionSerializer(many=True, read_only=True)
class Meta:
model = Question
fields = ('id', 'body', 'options')
class QuestionWriteSerializers(serializers.ModelSerializer):
options = OptionSerializer(many=True)
class Meta:
model = Question
fields = ('id', 'body', 'options')
def create(self, validated_data):
options_data = validated_data.pop('options')
question_created = Questions.objects.update_or_create(**validated_data)
option_query = Options.objects.filter(question=question_created[0])
if len(option_query) > 1:
for existeding_option in option_query:
option_query.delete()
for option_data in options_data:
Options.objects.create(question=question_created[0], **option_data)
return question_created[0]
def update(self, instance, validated_data):
options = validated_data.pop('options')
instance.body = validated_data.get('body', instance.body)
instance.save()
keep_options = []
for option_data in options:
if 'id' in option_data.keys():
if Options.objects.filter(id=option_data['id'], question_id=instance.id).exists():
o = Options.objects.get(id=option_data['id'])
o.option = option_data.get('option', o.option)
o.is_correct = option_data.get('is_correct', o.is_correct)
o.save()
keep_options.append(o.id)
else:
continue
else:
o = Options.objects.create(**option_data, question=instance)
keep_options.append(o.id)
for option_data in instance.options.all():
if option_data.id not in keep_options:
Options.objects.filter(id=option_data.id).delete()
return instance
The QuestionViewSet is almost the same and I removed the OptionViewSet and controlled all things from QuestionViewSet
views.py
class QuestionViewSet(ModelViewSet):
queryset = Question.objects.all()
def get_serializer_class(self) or self.request.method == 'PUT' or self.request.method == 'PATCH':
if self.request.method == 'POST':
return QuestionWriteSerializer
return QuestionReadSerializer
def create(self, request, *args, **kwargs):
"""
Overriding create() method to change response format
"""
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response({
'message': 'Successfully created question',
'data': serializer.data,
'status': 'HTTP_201_CREATED',
}, status=status.HTTP_201_CREATED, headers=headers)
else:
return Response({
'message': 'Can not create',
'data': serializer.errors,
'status': 'HT',
}, status=status.HTTP_400_BAD_REQUEST)

Rest Call gives error : Incorrect type. Expected pk value, received str

This post has an update below.
I currently have these two models. I am trying to create a job using CreateAPIView. Before I show the view here are my models
class modelJobCategory(models.Model):
description = models.CharField(max_length=200, unique=True)
other = models.CharField(max_length=200, unique=False , blank=True , null=True)
class modelJob(models.Model):
category = models.ManyToManyField(modelJobCategory,null=True,default=None,blank=True)
description = models.CharField(max_length=200, unique=False)
These two are my serializers
class Serializer_CreateJobCategory(ModelSerializer):
class Meta:
model = modelJobCategory
fields = [
'description',
]
class Serializer_CreateJob(ModelSerializer):
class Meta:
model = modelJob
category = Serializer_CreateJobCategory
fields = [
'category',
'description',
]
def create(self, validated_data):
job = modelJob.objects.create(user=user,category=?,...) #How to get category ?
return job
Now this is my view
class CreateJob_CreateAPIView(CreateAPIView):
serializer_class = Serializer_CreateJob
queryset = modelJob.objects.all()
def post(self, request, format=None):
serializer = Serializer_CreateJob(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Now I am passing the following JSON
{
"category" :{
"description": "Foo"
},
"description" : "World"
}
However I get the exception
{
"category": [
"Incorrect type. Expected pk value, received str."
]
}
I came across the same question here and it mentions i need to define a slug field which I am not sure where. Any suggestion on how I can fix this ?
Update:
So my create Job serializer looks like this now however it returns back the error
Got AttributeError when attempting to get a value for field category
on serializer Serializer_CreateJob. The serializer field might be
named incorrectly and not match any attribute or key on the modelJob
instance. Original exception text was: 'ManyRelatedManager' object has
no attribute 'description'.
class Serializer_CreateJob(ModelSerializer):
category = serializers.CharField(source='category.description')
class Meta:
model = modelJob
category = Serializer_CreateJobCategory()
fields = [
'category',
'description',
]
def create(self, validated_data):
category_data = validated_data.pop('category')
category = modelJobCategory.objects.get(description=category_data['description'])
job = modelJob.objects.create(description=validated_data["description"])
job.category.add(category)
job.save()
return job
Any suggestions on how I can fix this now ?
Can you try this?
class Serializer_CreateJob(ModelSerializer):
category = serializers.SlugRelatedField(
many=True,
queryset=modelJobCategory.objects.all(),
slug_field='description'
)
class Meta:
model = modelJob
fields = [
'category',
'description',
]
Try to explicitly define category field and use source=category.description like this:
from rest_framework import serializers
class Serializer_CreateJob(ModelSerializer):
category = serializers.CharField(source='category.description')
class Meta:
model = modelJob
category = Serializer_CreateJobCategory
fields = [
'category',
'description',
]
def create(self, validated_data):
category_data = validated_data.pop('category')
category = Category.objects.get(description=category_data['description'])
job = modelJob.objects.create(description=validated_data['description'],category=category,...) #categy object found by it's description
return job

django rest framework create nested objects "Models" by POST

I'm trying POST a new a Nested Object, the problem is just create the "top" object (Playlist), but don't create the "ChannelItem"...
My Models:
class Playlist(models.Model):
provider = models.IntegerField()
channel_id = models.CharField(max_length=100)
channel_version = models.CharField(blank=True, max_length=100)
start = models.DateTimeField()
url = models.CharField(max_length=500)
class ChannelItem(models.Model):
playlist = models.ForeignKey(Playlist, editable=False, related_name='channelitems')
content_id = models.CharField(max_length=100)
content_version = models.CharField(blank=True, max_length=100)
My Serializer:
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('content_id', 'content_version')
exclude = ('id')
depth = 1
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start',
'url', 'channelitems')
depth = 2
channelitems = ChannelItemSerializer()
I use the curl to post the following data :
'{"provider":125,"channel_id":"xyz", "channel_version":"xsqt",
"start":"2012-12-17T11:04:35","url":"http://192.168.1.83:8080/maaaaa",
"channelitems":[{"content_id":"0.flv", "content_version":"ss"},
{"content_id":"1.flv","content_version":"ss"}]}' http://localhost:8000/playlist_scheduler/playlists/
I receive the message:
HTTP/1.1 201 CREATED
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 17 Dec 2012 20:12:54 GMT
Server: 0.0.0.0
{"id": 25, "provider": 125, "channel_id": "xyz", "channel_version": "xsqt",
"start":"2012-12-17T11:04:35", "url": "http://localhost:8080/something",
"channelitems": []}
Nested representations do not currently support read-write, and should instead be read-only.
You should probably look into using a flat representation instead, using pk or hyperlinked relations.
If you need the nested representation, you may want to consider having two separate endpoints - a flat writable endpoint, and a nested read-only endpoint.
If someone needs a quick-and-dirty solution for that, I came up with this one I'll be temporary using in a project:
class NestedManyToManyField(serializers.WritableField):
def to_native(self, value):
serializer = self.Meta.serializer(value.all(), many=True, context=self.context)
return serializer.data
def from_native(self, data):
serializer = self.Meta.serializer(data=data, many=True, context=self.context)
serializer.is_valid()
serializer.save()
return serializer.object
class Meta:
serializer = None
Then create your own subclass of NestedManyToManyField:
class TopicNestedSerializer(NestedManyToManyField):
class Meta:
serializer = MyOriginalSerializer
An example of MyOriginalSerializer:
class MyOriginalSerializer(serializers.ModelSerializer):
class Meta:
model = models.MyModel
fields = ('id', 'title',)
This works fine for me so far. But be aware there are clean fixes coming:
https://github.com/tomchristie/django-rest-framework/issues/960
https://github.com/tomchristie/django-rest-framework/pull/817
after a long effort I made a first version that funcinasse ...
I believe that with some improvement could be included within the ModelSerializer
class ChannelItemSerializer(serializers.ModelSerializer):
class Meta:
model = ChannelItem
fields = ('id', 'content_id', 'content_version')
def field_from_native(self, data, files, field_name, into):
try:
if self._use_files:
_files = files[field_name]
else:
_data = data[field_name]
except KeyError:
if getattr(self, 'default', None):
_data = self.default
else:
if getattr(self, 'required', None):
raise ValidationError(self.error_messages['required'])
return
if type(_data) is list:
into[field_name] = []
for item in _data:
into[field_name].append(self._custom_from_native(item))
else:
into[field_name] = self._custom_from_native(_data)
def _custom_from_native(self, data):
self._errors = {}
if data is not None:
attrs = self.restore_fields(data, None)
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
if not self._errors:
return self.restore_object(attrs, instance=getattr(self, 'object', None))
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ('id', 'provider', 'channel_id', 'channel_version', 'start', 'url', 'channel_items')
depth = 1
channel_items = ChannelItemSerializer()
def restore_object(self, attrs, instance=None):
self.foreign_data = {}
for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model():
field_name = obj.field.related_query_name()
if field_name in attrs:
self.foreign_data[field_name] = attrs.pop(field_name)
return super(PlaylistSerializer, self).restore_object(attrs, instance)
def save(self, save_m2m=True):
super(PlaylistSerializer, self).save(save_m2m)
if getattr(self, 'foreign_data', None):
for accessor_name, object_list in self.foreign_data.items():
setattr(self.object, accessor_name, object_list)
self.foreign_data = {}
return self.object
For me, I have a hybrid workaround that I'm OK with. Namely, create a view that has:
the ManyToMany field in its un-nested serializer form
alias the nested ManyToMany field into a variable with _objs as the suffix and specify it as as read only
when you PUT back to the server reconcile the two aliased fields and store the result in the un-nested serializer field
e.g.
class MSerializer(serializers.HyperlinkedModelSerializer):
foo_objs = TempSensorSerializer(source='foos', many=True, allow_add_remove=True,required=False,read_only=True)
class Meta:
model = M
fields = ('url', 'foos', 'foo_objs')
I don't love this solution, but it beats trying to separately query and collate the nested fields after retrieving the initial container.