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

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).

Related

How do I handle foreign key relationship in the urlpattern in the django-rest-framwork

In my models.py I have the following classes:
class Project(models.Model):
name = models.CharField(max_length=100)
class ProjectMaterial(models.Model):
project = models.ForeignKey("Project", on_delete=models.CASCADE)
material = models.CharField(max_length=150)
units = models.IntegerField()
My serializers are like this:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
class ProjectMaterialSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectMaterial
fields = "__all__"
My current views.py looks like this:
class ProjectList(generics.ListCreateAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
class ProjectMaterialList(generics.ListCreateAPIView):
queryset = ProjectMaterial.objects.all()
serializer_class = ProjectMaterialSerializer
How should I create my urlpatterns to make a PUT request to change the units value for a
project with an id=1 for a material with an id=3?
I suppose you want to change the value of a Material Object where id = 3. in this case you really dont want to add the FK to the url_patterns. instead you can send data data related to FK via a PUT request.
urlpatterns = [
path('<id>/edit/', MaterialUpdateView.as_view(), name='material-update'),
]
If you really want to change the FK. send the data via a PUT or PATCH request like this
data = {
id: 3,
project: 1,
material: "some material"
units: 25,
}
If you want to update "ProjectMaterial" record with id=3 and that has FK relationship to "Project" record with id=1. All you need is "ProjectMaterial" id in URL and the data that needs to be updated for the corresponding "Project" record(Since it is in relationship with ProjectMaterial).
urlpatterns = [
path('/material/<id>/', ProjectMaterialDetail.as_View(), name='project_material')
]
If you want to update only the "units" field of "ProjectMaterial", you just inherit UpdateModelMixin into the new view class, "ProjectMaterialDetail". You can inherit "RetrieveModelMixin" into the same class. All you need to do is to make sure you send data in correct format to the ProjectMaterial serializer in "PUT" method of "ProjectMaterialDetail" view.
{
id: 5,
units: 152,
}
You can override Update method in serializer or you can call "partial_update" method in "PUT" method.

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.

Django rest framework POST many to many with extra fields

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

Django Rest Framework: How to pass a list of UUIDs for a nested relationship to a serializer?

TL;DR: What could be the reason the incoming data for one of my serializers does not get processed?
I'm working on a serializer for a nested relationship. The serializer should get a list of UUIDs, so that I can make many to many relationships. Here is the model:
class Order(
UniversallyUniqueIdentifiable,
SoftDeletableModel,
TimeStampedModel,
models.Model
):
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
custom_choice_items = models.ManyToManyField(CustomChoiceItem, blank=True)
price = models.ForeignKey(MenuItemPrice, on_delete=models.CASCADE)
amount = models.PositiveSmallIntegerField(
validators=[MinValueValidator(MINIMUM_ORDER_AMOUNT)]
)
Here is the data (my post body) with which I hit the route in my tests:
data = {
"checkin_uuid": self.checkin.uuid,
"custom_choice_items": [],
"menu_item": self.menu_item.uuid,
"price": self.menu_item_price.uuid,
"amount": ORDER_AMOUNT,
}
response = self.client.post(self.order_consumer_create_url, self.data)
Note that the empty list for custom_choice_items does not change anything. Even if I fill it with values the same error occurs. And last but not least here are the serializers:
class CustomChoiceItemUUIDSerializer(serializers.ModelSerializer):
"""Serializer just for the uuids, which is used when creating orders."""
class Meta:
model = CustomChoiceItem
fields = ["uuid"]
....
# The serializer that does not work
class OrderSerializer(serializers.ModelSerializer):
menu_item = serializers.UUIDField(source="menu_item.uuid")
custom_choice_items = CustomChoiceItemUUIDSerializer()
price = serializers.UUIDField(source="price.uuid")
wish = serializers.CharField(required=False)
class Meta:
model = Order
fields = [
"uuid",
"menu_item",
"custom_choice_items",
"price",
"amount",
"wish",
]
The problem is now, that when I leave out many=True, I get the error:
{'custom_choice_items': [ErrorDetail(string='This field is required.', code='required')]}
And If I set many=True I just simply don't get any data. By that I mean e.g. the value of validated_data["custom_choice_items"] in the serializers create() method is just empty.
What goes wrong here?
I even checked that the data is in the request self.context["request"].data includes a key custom_choice_items the way I pass the data to this view!
EDIT: Here is the data I pass to custom_choice_items:
data = {
“checkin_uuid”: self.checkin.uuid,
“custom_choice_items”: [{“uuid”: custom_choice_item.uuid}],
“menu_item”: self.menu_item.uuid,
“price”: self.menu_item_price.uuid,
“amount”: ORDER_AMOUNT,
}
self.client.credentials(HTTP_AUTHORIZATION=“Token ” + self.token.key)
response = self.client.post(self.order_consumer_create_url, data)
When you post data using the test api client, if the data contains nested structure you should use format=json, like this:
response = self.client.post(self.order_consumer_create_url, data, format='json')
Did you override .create method in the serializer? Something like this should work:
from django.db import transaction
class OrderSerializer(serializers.ModelSerializer):
# your fields and Meta class here
#transaction.atomic
def create(self, validated_data):
custom_choice_items = validated_data.pop('custom_choice_items')
order = super().create(validated_data)
order.custom_choice_items.add(*custom_choice_items)
return order
By the way you don't really need to define CustomChoiceItemUUIDSerializer if is just the primary key of that.

Django REST Framework: adding additional field to ModelSerializer

I want to serialize a model, but want to include an additional field that requires doing some database lookups on the model instance to be serialized:
class FooSerializer(serializers.ModelSerializer):
my_field = ... # result of some database queries on the input Foo object
class Meta:
model = Foo
fields = ('id', 'name', 'myfield')
What is the right way to do this? I see that you can pass in extra "context" to the serializer, is the right answer to pass in the additional field in a context dictionary?
With that approach, the logic of getting the field I need would not be self-contained with the serializer definition, which is ideal since every serialized instance will need my_field. Elsewhere in the DRF serializers documentation it says "extra fields can correspond to any property or callable on the model". Are "extra fields" what I'm talking about?
Should I define a function in Foo's model definition that returns my_field value, and in the serializer I hook up my_field to that callable? What does that look like?
Happy to clarify the question if necessary.
I think SerializerMethodField is what you're looking for:
class FooSerializer(serializers.ModelSerializer):
my_field = serializers.SerializerMethodField('is_named_bar')
def is_named_bar(self, foo):
return foo.name == "bar"
class Meta:
model = Foo
fields = ('id', 'name', 'my_field')
http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
You can change your model method to property and use it in serializer with this approach.
class Foo(models.Model):
. . .
#property
def my_field(self):
return stuff
. . .
class FooSerializer(ModelSerializer):
my_field = serializers.ReadOnlyField(source='my_field')
class Meta:
model = Foo
fields = ('my_field',)
Edit: With recent versions of rest framework (I tried 3.3.3), you don't need to change to property. Model method will just work fine.
With the last version of Django Rest Framework, you need to create a method in your model with the name of the field you want to add. No need for #property and source='field' raise an error.
class Foo(models.Model):
. . .
def foo(self):
return 'stuff'
. . .
class FooSerializer(ModelSerializer):
foo = serializers.ReadOnlyField()
class Meta:
model = Foo
fields = ('foo',)
if you want read and write on your extra field, you can use a new custom serializer, that extends serializers.Serializer, and use it like this
class ExtraFieldSerializer(serializers.Serializer):
def to_representation(self, instance):
# this would have the same as body as in a SerializerMethodField
return 'my logic here'
def to_internal_value(self, data):
# This must return a dictionary that will be used to
# update the caller's validation data, i.e. if the result
# produced should just be set back into the field that this
# serializer is set to, return the following:
return {
self.field_name: 'Any python object made with data: %s' % data
}
class MyModelSerializer(serializers.ModelSerializer):
my_extra_field = ExtraFieldSerializer(source='*')
class Meta:
model = MyModel
fields = ['id', 'my_extra_field']
i use this in related nested fields with some custom logic
My response to a similar question (here) might be useful.
If you have a Model Method defined in the following way:
class MyModel(models.Model):
...
def model_method(self):
return "some_calculated_result"
You can add the result of calling said method to your serializer like so:
class MyModelSerializer(serializers.ModelSerializer):
model_method_field = serializers.CharField(source='model_method')
p.s. Since the custom field isn't really a field in your model, you'll usually want to make it read-only, like so:
class Meta:
model = MyModel
read_only_fields = (
'model_method_field',
)
If you want to add field dynamically for each object u can use to_represention.
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = ('id', 'name',)
def to_representation(self, instance):
representation = super().to_representation(instance)
if instance.name!='': #condition
representation['email']=instance.name+"#xyz.com"#adding key and value
representation['currency']=instance.task.profile.currency #adding key and value some other relation field
return representation
return representation
In this way you can add key and value for each obj dynamically
hope u like it
This worked for me.
If we want to just add an additional field in ModelSerializer, we can
do it like below, and also the field can be assigned some val after
some calculations of lookup. Or in some cases, if we want to send the
parameters in API response.
In model.py
class Foo(models.Model):
"""Model Foo"""
name = models.CharField(max_length=30, help_text="Customer Name")
In serializer.py
class FooSerializer(serializers.ModelSerializer):
retrieved_time = serializers.SerializerMethodField()
#classmethod
def get_retrieved_time(self, object):
"""getter method to add field retrieved_time"""
return None
class Meta:
model = Foo
fields = ('id', 'name', 'retrieved_time ')
Hope this could help someone.
class Demo(models.Model):
...
#property
def property_name(self):
...
If you want to use the same property name:
class DemoSerializer(serializers.ModelSerializer):
property_name = serializers.ReadOnlyField()
class Meta:
model = Product
fields = '__all__' # or you can choose your own fields
If you want to use different property name, just change this:
new_property_name = serializers.ReadOnlyField(source='property_name')
As Chemical Programer said in this comment, in latest DRF you can just do it like this:
class FooSerializer(serializers.ModelSerializer):
extra_field = serializers.SerializerMethodField()
def get_extra_field(self, foo_instance):
return foo_instance.a + foo_instance.b
class Meta:
model = Foo
fields = ('extra_field', ...)
DRF docs source
Even though, this is not what author has wanted, it still can be considered useful for people here:
If you are using .save() ModelSerializer's method, you can pass **kwargs into it. By this, you can save multiple dynamic values.
i.e. .save(**{'foo':'bar', 'lorem':'ipsum'})
Add the following in serializer class:
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['package_id'] = "custom value"
return representation