Django rest framework POST many to many with extra fields - django

I am trying to create a model in Django that has a many-to-many relationship to another model with extra fields. I am using the rest framework to provide CRUD operations on these and am having a chicken-and-egg scenario I believe...
The issue is that when I go to POST the new MainObject, it throws an error in the many-to-many part due to not having a MainObject id to point to. But I want it to point to the MainObject I am creating, which doesn't exist at time of POST'ing. I believe this to be an issue with the serializers, but am unsure of how to resolve it. I assume my assumptions might also be off in how I am formulating the POST data.
I am using Django 2.1.8
Model Code
class RelatedObject(models.Model):
...
class MainObject(models.Model):
related_objects = models.ManyToManyField(RelatedObject, through='ManyRelatedObject')
class ManyRelatedObject(models.Model):
main_object = models.ForeignKey(MainObject, on_delete=models.DO_NOTHING)
related_object = models.ForeignKey(RelatedObject, on_delete=models.DO_NOTHING)
other_attribute = models.BooleanField(...)
Serializer Code
class ManyRelatedObjectSerializer(serializers.ModelSerializer):
main_object = serializers.PrimaryKeyRelatedField(queryset=MainObject.objects.all())
related_object = serializers.PrimaryKeyRelatedField(queryset=RelatedObject.objects.all())
class Meta:
model = ManyRelatedObject
fields = '__all__'
class MainObjectSerializer(serializers.ModelSerializer):
related_object = ManyRelatedObjectSerializer(many=True)
class Meta:
model = MainObject
fields = '__all__'
POST Payload
( It is assumed that there exists a RelatedObject that has an id of 1)
{
"related_object": [
{
"related_object": 1,
"other_attribute": true
}
],
...
}
Response
{
"related_object": [
{
"main_object": [
"This field is required."
]
}
]
}
Goal Response:
{
"id": 1,
"related_object": [
{
"main_object": 1,
"related_object": 1,
"other_attribute": true
}
],
...
}
REST endpoint setup
class MainObjectViewSet(viewsets.ModelViewSet):
queryset = MainObject.objects.all()
serializer_class = MainObjectSerializer

Override the __init__() method of the MainObjectSerializer.
class MainObjectSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['request'].method == 'GET':
self.fields['related_object'] = ManyRelatedObjectSerializer(many=True)
related_object = ManyRelatedObjectSerializer(many=True)# remove this line
class Meta:
model = MainObject
fields = '__all__'
What this snippt do is, the serializer will render the response/output using ManyRelatedObjectSerializer serializer, if the request is a HTTP GET, otherwise it will render the stock mode(PrimaryKeyRelatedField)

For posterity, ended up hand-jamming this with poorly overridden create and update methods, due to time constraints. Seems ridiculous that django can't handle this scenario, seems like it's a fairly common use case...

Related

Create a field with conditions

I'm a beginner in django rest framework.
I am wondring if there is a way to create a field from a GET. For example if "count" == 0, create a field named "available" : "out_of_stock" else "available
models.py
class Count(models.Model):
name = models.CharField(max_length=100)
count = models.IntergerField()
serializers.py
class CountSerializer(serializers.ModelSerializer):
class Meta:
model = Count
fields = '__all__'
views.py
class CountViewSet(viewsets.ModelViewSet):
queryset = Count.objects.all()
serializer_class = CountSerializer
output
[
{
"id": 1,
"count": 10,
},
{
"id": 2,
"count": 0,
}
]
First, for a good practise, avoid using fields = '__all__' and instead define your fields specifically.
For ex: fields = ['name', 'count'], you will see doing so will pay off shortly as I continue.
DRF has the feature to mark a serializer field as read_only or write_only so it works on specific requests, as it looks from the naming read_only intended for GET and write_only for POST.
You can do what you are looking for in so many ways, but I think the easiest way to do it would be using SerializerMethodField like this:
class CountSerializer(serializers.ModelSerializer):
available = serializers.SerializerMethodField()
def get_available((self, obj):
value = 'out_of_stock' if obj.count == 0 else 'available'
return value
class Meta:
model = Count
fields = ['name', 'count', 'available']
For more advanced needs, you can read about dynamic serializer fields on drf docs, see.

How to make custom response in djangorestframework

So I like the idea of using class-based views and ModelSerializers but I have an issue with it for my particular use case. Maybe I am not using it as it's intended to be used.
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = CarModel
fields = ['car_name']
# A car can have build for multiple years
class MakelHistorySerializer(serializers.ModelSerializer):
car = CarSerializer(many=True, read_only=True)
class Meta:
model = MakeHistoryModel
fields = ['model_year', 'car']
The response is:
{
"car": {
"car_name": "Fiesta"
},
"model_year": "2020"
}
My two model classes, CarModel and MakeHistoryModel have ["id", "car_name", "manufacturer"] and ["id", "car_id", "model_year", "country_id"] fields respectively.
What kind of a response I really want is:
{
"car_name": "Fiesta",
"model_year": "2020"
}
How would I do this?
You don't need to first serializer (CarSerializer).Just this serializer which has SerializerMethodField enough for your output:
class MakelHistorySerializer(serializers.ModelSerializer):
car_name = serializers.SerializerMethodField()
class Meta:
model = MakeHistoryModel
fields = ['model_year', 'car_name']
def get_car_name(self,obj):
return obj.car.name if obj.car_id else ''
# I don't know your model so to avoid NoneType error, I added this check

Django REST Framework: nested relationship, how to submit json?

I'm using Django 2.1, DRF 3.7.7.
I've some models and their relative (model) serializers: these models are nested, and so are the serializers.
Let me give an example:
# models.py
class City(models.Model):
name = models.CharField(max_length=50)
class Person(models.Model):
surname = models.CharField(max_length=30)
birth_place = models.ForeignKey(City)
# serializers.py
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = models.CitySerializer
fields = "__all__"
class PersonSerializer(serializers.ModelSerializer):
birth_place = CitySerializer()
class Meta:
model = models.Person
fields = "__all__"
If I submit an AJAX request with a json like:
{'surname': 'smith', 'birth_place': 42}
I get back a Bad Request response, containing: Invalid data. Expected a dictionary, but got int.
If I submit a nested json like:
{'surname': 'smith', 'birth_place': {'id': 42, 'name': 'Toronto'}}
the relation is not converted, the id field is ignored and the rest is parsed to:
OrderedDict([('birth_place', OrderedDict([('name', 'Toronto')]))])
The following is the post method I'm using on a class-based view:
def post(self, request):
print("Original data:", request.data)
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
self.data = serializer.validated_data
print("Parsed data:", self.data)
...
I only need to get data from the endpoints connected to the serializers, I don't need to write/save anything through the REST interface, since the POST processing of the form is done by Django.
TL;DR: How should I correctly submit a JSON request to a nested serializer, without having to write handmade conversions? Did I commit errors in setting up the serializers?
Edit: I've discovered that by adding id = serializers.IntegerField() to the serializer parent class (e.g. City), the serializer parser now processes the id. At least now I'm able to perform actions in the backend with django.
Generic writing for nested serializers is not available by default. And there is a reason for that:
Consider, you are creating a person with a birthplace, using a POST request. It is not clear if the submitted city is a new one or an existing one. Should it return an error if there isn't such a city? Or should it be created?
This is why, if you want to handle this kind of relationship in your serializer, you need to write your own create() and update() methods of your serializer.
Here is the relevant part of the DRF docs: http://www.django-rest-framework.org/api-guide/relations/#writable-nested-serializers
It's definitely not clearly put into the docs of django-rest. If you follow the process of serializers processing the data for creation then it becomes clear that django manages m2m by saving the parent instance first and then adding the m2m values, but somehow the m2m fields don't go through the validation if you mark them as read_only.
The solution to this is to overr run_validation method of the serializer. The serializer should look like this:
class ExampleSerializer(serializers.ModelSerializer):
queryset = SomeModel.objects.all()
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = SomeModel
fields = ['pk', 'name', 'tags']
def run_validation(self, data):
validated_data = super(StudyResourceSerializer, self).run_validation(data)
validated_data['tags'] = data['tags']
return validated_data
The request body should look like this:
{
"tags": [51, 54],
"name": "inheritance is a mess"
}

Append "_id" to foreign key fields in Django Rest Framework

I have a model like so:
class MyModel(models.Model):
thing = models.ForeignKey('Thing')
Serializers and ViewSet like so:
class ThingSerializer(serializers.ModelSerializer):
class Meta:
model = Thing
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
For MyModel list endpoint, DRF return objects like:
[
{ id: 1, thing: 1 },
{ id: 2, thing: 1 },
{ id: 3, thing: 2 },
{ id: 4, thing: 4 }
]
Is there a way to tell DRF to automatically include "_id" on the end of ForeignKey fields that are just IDs and not the actual related object? e.g.
[
{ id: 1, thing_id: 1 },
{ id: 2, thing_id: 1 },
{ id: 3, thing_id: 2 },
{ id: 4, thing_id: 4 }
]
Found same request and solution here:
https://github.com/tomchristie/django-rest-framework/issues/3121
https://gist.github.com/ostcar/eb78515a41ab41d1755b
The AppendIdSerializerMixin.get_fields override suffices for output of JSON objects (with _id appended) but when writing back to the API, it's a little more complicated and the logic in IdPrimaryKeyRelatedField and IdManyRelatedField handles that.
class IdManyRelatedField(relations.ManyRelatedField):
field_name_suffix = '_ids'
def bind(self, field_name, parent):
self.source = field_name[:-len(self.field_name_suffix)]
super(IdManyRelatedField, self).bind(field_name, parent)
class IdPrimaryKeyRelatedField(relations.PrimaryKeyRelatedField):
"""
Field that the field name to FIELD_NAME_id.
Only works together the our ModelSerializer.
"""
many_related_field_class = IdManyRelatedField
field_name_suffix = '_id'
def bind(self, field_name, parent):
"""
Called when the field is bound to the serializer.
Changes the source so that the original field name is used (removes
the _id suffix).
"""
if field_name:
self.source = field_name[:-len(self.field_name_suffix)]
super(IdPrimaryKeyRelatedField, self).bind(field_name, parent)
class AppendIdSerializerMixin(object):
'''
Append '_id' to FK field names
https://gist.github.com/ostcar/eb78515a41ab41d1755b
'''
serializer_related_field = IdPrimaryKeyRelatedField
def get_fields(self):
fields = super(AppendIdSerializerMixin, self).get_fields()
new_fields = type(fields)()
for field_name, field in fields.items():
if getattr(field, 'field_name_suffix', None):
field_name += field.field_name_suffix
new_fields[field_name] = field
return new_fields
class MyModelSerializer(AppendIdSerializerMixin, serializers.ModelSerializer):
class Meta:
model = MyModel
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
This has worked for me as of today in July 2022.
def ParentModel():
...
def ChildModel():
parent = models.ForeignKey(ParentModel)
serializers.py
def ChildSerializer():
parent_id = serializers.PrimaryKeyRelatedField(
source="parent",
queryset=ParentModel.objects.all(),
)
class Meta:
exclude = ["parent"]
fields = "__all__"
The source="parent" in the serializer field, is what allows this rename to happen; it needs to match the ForeignKey field name in ChildModel.
Note: The exclude = ["parent"] is required if you are using fields = "__all__" so that DRF doesn't require or return the default field name.
Note 2: If using UUID for your ID field then you'll need to add pk_field=UUIDField(format="hex_verbose"), to the serializer field you're renaming to {field}_id
You can use db_column model field option in your model:
class MyModel(models.Model):
thing = models.ForeignKey('Thing', db_column='thing_id')
Or if you don't want to change your model, you can do so by changing source serializer field in your serializer:
class ThingSerializer(serializers.ModelSerializer):
thing_id = serializers.IntegerField(source='thing')
class Meta:
model = Thing
fields = ('thing_id','other_field', 'another_field')
ok matt simply add that parameter to your model class:
thing = models.ForeignKey(Thing, related_name='thing_id')
It appears to be rather complicated since it's not something DRF allows to configure. But like always, you can override things.
Everything seems to happens in the model_meta.py file. In this file, you can replace
forward_relations[field.name] = RelationInfo(
by
forward_relations[field.name + '_id'] = RelationInfo(
Careful, you need to do it twice in that function.
Once you've done that, you have still work to do as the ModelSeralizer depends on the real model_meta. It appears you need to replace those three lines:
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L865
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L944
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L1425
Then, you need to implement a MyModelSerializer which overrides ModelSerializer and the three methods: create, get_fields, get_unique_together_validators. I tested it on GET requests and it works.
As you can see, it's a significant amount of code rewriting which implies difficulties for maintaining upgrades. Then, I would strongly recommend to think twice before doing so. In the mean time, you can still open an issue on the DRF project for making it more configurable (and maintainable).

Django Rest Framework filterset on many to many field

I'm trying to filter manytomany field by it's name field but I can't set it up properly. Could any body have a look at this?
Models
class Criteria(models.Model):
name = models.CharField(max_length=400, primary_key=True)
tests = models.ManyToManyField(Test)
class Test(models.Model):
name = models.CharField(max_length=4000)
Views
class CriteriaViewSet(DefaultsMixin, viewsets.ModelViewSet):
queryset = Criteria.objects.all()
filter_class = CriteriaFilter
filter_fields = ('tests',)
def get_serializer_class(self):
if self.action == 'list':
return CriteriaSerializer
return CriteriaDetailSerializer
Filterset
class CriteriaFilter(django_filters.FilterSet):
test = django_filters.CharFilter(name="tests__name", lookup_type='contains')
class Meta:
model = Criteria
fields = ('tests',)
Serializers
class CriteriaSerializer(serializers.ModelSerializer):
tests = serializers.StringRelatedField(many=True)
links = serializers.SerializerMethodField()
class Meta:
model = Criteria
fields = ('name', 'tests', 'links')
def get_links(self, obj):
request = self.context['request']
return {
'self': reverse('api:criterium-detail',
kwargs={'pk': obj.pk},
request=request),
}
With the configuration above what I get on the URL:
/api/criteria/?test=FB1400
is empty results array even though there is a number of Criteria containing addressed test:
HTTP 200 OK Content-Type: application/json Vary: Accept Allow: GET, POST, HEAD, OPTIONS
{
"count": 0,
"next": null,
"previous": null,
"results": []
}
I was trying different lookups (exact, in, contains) as well as different values in fields but none of those works for me...
I was also wondering if I should set up django filter backend somewhere but I'm not sure how to do this and I don't know if it's necessary in this case.
Not sure if you solved it, but this post is similar to this other which was solved by the way:
Django Rest Framework (GET filter on ManyToMany field)
Hope this helps.