Django DRF: read_only_fields not working properly - django

I have the following models
class Breed(models.Model)::
name = models.CharField(max_length=200)
class Pet(models.Model):
owner = models.ForeignKey(
"User",
on_delete=models.CASCADE,
)
name = models.CharField(max_length=200)
breed = models.ForeignKey(
"Breed",
on_delete=models.CASCADE,
)
I am trying to add few fileds for representation purpose. I dont want them to be included while create or update
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email')
breed_name = serializers.CharField(source='breed.str')
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["breed_name","owner_email"]
This is not working. I see the owner_email and breed_name in the HTMLform (the DRF api page)
Where as
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email',read_only=True)
breed_name = serializers.CharField(source='breed.str',read_only=True)
class Meta:
model = Pet
fields = "__all__"
This is working. I dont see them in the HTMLform
Also i observed, if i use a model field directly in read_only_fields then it works.
class PetSerializer(serializers.ModelSerializer):
class Meta:
model = Pet
fields = "__all__"
read_only_fields = ["name"]
This will make all name not shown in update or create
Why read_only_fields is not working properly

This is very interesting. I looked into the code and found the root cause, specifically this lines in the implementation for ModelSerializer:
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
fields[field_name] = declared_fields[field_name]
continue
....
Here was my script for the investigation
from django.db import models
from rest_framework import serializers
class MyModel(models.Model):
xero_contact_id = models.UUIDField(unique=True)
name = models.CharField(max_length=255, default="Some name")
class Meta:
db_table = "my_model"
class MySerializer(serializers.ModelSerializer):
owner_email = serializers.CharField()
breed_name = serializers.CharField(max_length=255)
class Meta:
model = MyModel
fields = '__all__'
read_only_fields = ["breed_name", "owner_email", "xero_contact_id"]
serializer = MySerializer()
print(repr(serializer))
I added some prints and here is what I saw:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField()
breed_name = CharField(max_length=255)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
As you can see, the read_only argument is in the extra_kwargs. The problem is that for all the fields that are only declared in the ModelSerializer itself (visible from declared_fields) and not in the model class, they don't read from the extra_kwargs, they just read what was set in the field itself as visible in the code snippet above fields[field_name] = declared_fields[field_name] then performs a continue. Thus, the option for read_only was ignored.
I fixed it by modifying the implementation of ModelSerializer to also consider the extra_kwargs even for non-model fields
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
field_class = type(declared_fields[field_name])
declared_field_args = declared_fields[field_name].__dict__['_args']
declared_field_kwargs = declared_fields[field_name].__dict__['_kwargs']
extra_field_kwargs = extra_kwargs.get(field_name, {})
# Old implementation doesn't take into account the extra_kwargs
# fields[field_name] = declared_fields[field_name]
# New implementation takes into account the extra_kwargs
fields[field_name] = field_class(*declared_field_args, **declared_field_kwargs, **extra_field_kwargs)
continue
....
Now, read_only was correctly set to the target fields, including non-model fields:
>>> print(repr(serializer))
field_names ['id', 'owner_email', 'breed_name', 'xero_contact_id', 'name']
declared_fields OrderedDict([('owner_email', CharField()), ('breed_name', CharField(max_length=255))])
extra_kwargs {'breed_name': {'read_only': True}, 'owner_email': {'read_only': True}, 'xero_contact_id': {'read_only': True}}
MySerializer():
id = IntegerField(label='ID', read_only=True)
owner_email = CharField(read_only=True)
breed_name = CharField(max_length=255, read_only=True)
xero_contact_id = UUIDField(read_only=True)
name = CharField(max_length=255, required=False)
This doesn't seem to be in the DRF docs. Sounds like a feature we can request to DRF :) So the solution for the meantime is as what #JPG pointed out, use read_only=True explicitly in the extra non-model fields.

The read_only_fields meta option will work for the fields which are not explicitly defined in the Serializer.
So, in your case, you need to add the read_only=True to those explicitly defined fields, as
class PetSerializer(serializers.ModelSerializer):
owner_email = serializers.CharField(source='owner.email', read_only=True)
breed_name = serializers.CharField(source='breed.str', read_only=True)
class Meta:
model = Pet
fields = "__all__"

Related

Adding a custom, non-model attribute to query set in Django?

Newbie to DRF and have a model called posts. And another called user. The post object looks as follows:
class Post(models.Model):
"""
Post model
"""
title = models.CharField(max_length=250)
body = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='forum_posts')
parent_post = models.ForeignKey('self',
on_delete=models.CASCADE,
blank=True,
null=True)
time_stamp = models.DateTimeField(default=timezone.now)
objects = models.Manager()
The serializer for this model is:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = models.Post
fields = ('id', 'title', 'body', 'parent_post', 'author', 'time_stamp')
extra_kwargs = {'id': {'read_only': True},
'author': {'read_only': True}}
When returning data for this model, I want to add an extra attribute to each object within the query set called "author_username". The username should be the username belonging to the post's author id. I also want to do this without modifying the model to add another attribute such as "author_username" since this'll be redundant (already have an FK for author). So, ideally, the json for an object would look like:
'post_id': 1
'post_title': 'Example post'
'post_body': 'Example post'
'author_id': 1
'parent_post_id': null
'time_stamp': '2022'
'author_username': 'testUser'
How can I go about doing this?
Here's my view:
class PostList(generics.ListCreateAPIView):
permission_classes = [IsAuthenticatedOrReadOnly]
queryset = models.Post.objects.all()
serializer_class = serializers.PostSerializer
The source argument can be passed to a serializer field to access an attribute from a related model
class PostSerializer(serializers.ModelSerializer):
author_username = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = models.Post
...
You should add a select_related call to your view's queryset
class PostList(generics.ListCreateAPIView):
...
queryset = models.Post.objects.select_related('author')
...

Django Rest Framework Serializer - return related field

I have a model with a one-to-one relationship with a main model:
class User(models.Model):
id = models.BigIntegerField(primary_key=True)
username = models.CharField(max_length=100, blank=True)
class AggregatedStats(models.Model):
user_id = models.ForeignKey('User', on_delete=models.DO_NOTHING, unique=True)
followers_30d = models.BigIntegerField(blank=True)
I have written the following serializers:
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'followers']
class AggregatedStatsSerializer(serializers.HyperlinkedModelSerializer):
username = UserSerializer(source='user.username')
class Meta:
model = AggregatedStats
fields = ['followers_30d', 'username']
I am trying to return the username from the User model, but whatever I try to get it, the best I can do is get the hyperlinked related field from user, but not the actual "username" attribute. How would you return this?
You can simply create a field and return it:
class AggregatedStatsSerializer(serializers.HyperlinkedModelSerializer):
username = SerializerMethodField()
class Meta:
model = AggregatedStats
fields = ['followers_30d', 'username']
def get_username(self, obj):
return obj.user_id.username

DRY way to rename ModelSerializer fields without duplicating model/field definitions?

Working with a legacy database with an awful schema and want to rename several ModelSerializer fields without having to redefine fields already defined on the model.
Here's an example model:
class LegacyItem(models.Model):
# Note: ignore things that feel "wrong" here (just an example)
legacyitem_id = models.IntegerField(primary_key=True)
legacyitem_notes = models.CharField(max_length=4000, blank=True, null=True)
directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
created_by_directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
created_date = models.DateField(auto_now_add=True)
modified_by_directory_id = models.ForeignKey('Directory', models.DO_NOTHING)
modified_date = models.DateField(auto_now=True)
Here's a working serializer:
class LegacyItemSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = LegacyItem
read_only_fields = [
'created_date',
'modified_date',
]
The goal is to bulk rename model fields for the API so we can abstract away the awful schema. Here's a working serializer showing the sort of renaming we want to do:
class LegacyItemSerializer(serializers.ModelSerializer):
created_by = serializers.PrimaryKeyRelatedField(
read_only=True,
source='created_by_directory_id',
)
customer = serializers.PrimaryKeyRelatedField(
queryset=Directory.objects.all(),
source='directory_id',
)
id = serializers.PrimaryKeyRelatedField(
read_only=True,
source='legacyitem_id'
)
modified_by = serializers.PrimaryKeyRelatedField(
read_only=True,
source='modified_by_directory_id',
)
notes = serializers.DateField(source='legacyitem_notes')
class Meta:
fields = [
'id',
'customer',
'notes',
'created_by',
'created_date',
'modified_by',
'modified_date',
]
model = LegacyItem
read_only_fields = [
'created_date',
'modified_date',
]
All we want to do is rename the fields. We don't want to change the validation and would rather keep most of that validation on the model and the model's fields and not also put it into the serializer. We will have to do this sort of thing for a lot of models and it's very tedious and bad practice (IMO) to be duplicating model validation on the serializer just to rename a field.
Is there a DRY way to do this? Maybe something we can put into LegacyItemSerializer.Meta or some method we can override? When working with Django forms, for example, you could do this easily by tweaking labels/widgets in the ModelForm.Meta. You would never have to redefine ModelForm fields/validation.
In a perfect world, maybe something like:
class LegacyItemSerializer(serializers.ModelSerializer):
class Meta:
model = LegacyItem
model_field_args = {
'legacyitem_id': {'name': 'id'},
'directory_id': {'name': 'customer'},
'legacyitem_notes': {'name': 'notes'},
'created_by_directory_id': {'name': 'created_by'},
'modified_by_directory_id': {'name': 'modified_by'},
}

Django Rest Framework Ordering Filter, order by nested list length

I'm using OrderingFilter globally through settings.py and it works great.
Now I would like to order on the size of a nested list from a ManyToManyField. Is that possible with the default OrderingFilter?
If not, is there a way I can do it with a custom filter, while keeping the query param ordering in the url (http://example.com/recipes/?ordering=). For the sake of consistency.
Oh and the ManyToManyField is a through table one.
These are my models.py:
class Recipe(models.Model):
name = models.CharField(max_length=255)
cook_time = models.FloatField()
ingredients = models.ManyToManyField(IngredientTag, through=Ingredient)
My serializers.py:
class IngredientTagSerializer(serializers.ModelSerializer):
class Meta:
model = IngredientTag
fields = ('id', 'label')
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ('amount', 'unit', 'ingredient_tag')
depth = 1
class RecipeSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(source='ingredient_set', many=True)
class Meta:
model = Recipe
fields = ('id', 'url', 'name', 'ingredients', 'cook_time')
read_only_fields = ('owner',)
depth = 2
And my views.py:
class RecipeViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows recipes to be viewed or edited.
"""
queryset = Recipe.objects.all().order_by()
serializer_class = RecipeSerializer
permission_classes = (DRYPermissions,)
ordering_fields = ('cook_time',) #Need ingredient count somewhere?
Thanks!
Try:
class RecipeSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(source='ingredient_set', many=True)
ingredients_length = serializers.SerializerMethodField()
class Meta:
model = Recipe
fields = ('id', 'url', 'name', 'ingredients', 'cook_time')
read_only_fields = ('owner',)
depth = 2
def get_ingredients_length(self, obj):
return obj.ingredients.count()
Then order by ingredients_length
EDIT
In model.py, try this:
#property
def ingredient_length(self):
return self.ingredient_set.count()

Django rest nested serializer validation by id

I have following django-rest serializers:
class FileSerializer(serializers.ModelSerializer):
class Meta:
model = FileModel
fields = ('id', '_file')
class SomeSerializer(serializers.ModelSerializer):
files = FileSerializer(many=True, required= False)
class Meta:
model = SomeModel
fields = ('id', 'files')
And models
class File(models.Model):
some_obj = models.ForeignKey('SomeObj',related_name='files', blank=True, null=True)
_file = models.FileField(upload_to=get_file_path)
The problem comes, when I create SomeSerializer with existing File objects
s = SomeSerializer(data = {'files': [{'id' : 1}]})
s.is_valid()
s.errors
Returns
False
{'_file': [u'No file was submitted.']}
How to solve this? Thanks.
Well, the message is pretty obvious. You don't provide "_file" to the serializer. This should fix:
s = SomeSerializer(data = {'files': [{'id' : 1, '_file': <somedata>}]})
_file field is missing, then if it's not required, set required to False:
class FileSerializer(serializers.ModelSerializer):
_file = serializer.FileField(required=False)
class Meta:
model = FileModel
fields = ('id', '_file')