Django rest nested serializer validation by id - django

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

Related

Django DRF: read_only_fields not working properly

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__"

django get file size and put it in serializer

serializers.py
class ModFileSerializer(serializers.ModelSerializer):
size = serializers.CharField(default=os.path.getsize('file'), max_length=16)
class Meta:
model = models.ModFile
fields = '__all__'
read_only_fields = (
'size',
)
models.py
class ModFile(models.Model):
downloads = models.IntegerField(default=0)
file = models.FileField(upload_to='mods/')
created_at = models.DateTimeField(auto_now_add=True)
Here I have a serializer for the ModFile model and the only thing that's missing is the file's size, I know os.path.getsize() needs the exact location of the file so how do I actually access the file field from the model in order to pass in the getsize() function or is there a better way to do it?
class ModFileSerializer(serializers.ModelSerializer):
size = serializers.SerializerMethodField()
def get_size(self, obj):
return os.path.getsize(obj.file.path)
class Meta:
model = models.ModFile
fields = '__all__'
read_only_fields = (
'size',
)
I think this would work any time you use this serializer.
obj in the get_size method is the instance itself.
Add a to_representation method to your serializer which is much easy.
class ModFileSerializer(serializers.ModelSerializer):
class Meta:
model = models.ModFile
fields = '__all__'
def to_representation(self, instance):
rep = super(ModFileSerializer, self).to_representation(instance)
rep['size'] = instance.file.size
return rep

Serialization of child models

I have models:
class CommonEditor(models.Model):
def __str__(self):
return 'Common Atributes Mask'
class Color(models.Model):
name = models.CharField(max_length=25)
editor = models.ForeignKey(CommonEditor, on_delete=models.PROTECT, null=True)
So I make serialization this way:
class ColorSerializer(serializers.ModelSerializer):
class Meta:
model = Color
fields = '__all__'
class CommonAttributesSerializer(serializers.ModelSerializer):
color = ColorSerializer(many=True, read_only=True)
class Meta:
model = CommonEditor
fields = ('pk', 'color')
And then view:
class CommonAttributeAPIView(generics.ListCreateAPIView):
serializer_class = CommonAttributesSerializer
queryset = CommonEditor.objects.all()
I get only pk of my CommonEditor Model. Why can't i get the full Atributes Mask and how can I fix it? Big thanks!
Default name for reverse foreign key relation is modelname_set or in your case color_set. So try to rename color field to color_set:
class CommonAttributesSerializer(serializers.ModelSerializer):
color_set = ColorSerializer(many=True, read_only=True)
class Meta:
model = CommonEditor
fields = ('pk', 'color_set')
This can also be achieved via SerializerMethodField and can be seen as follow:
class CommonAttributesSerializer(serializers.ModelSerializer):
color = serializers.SerializerMethodField()
class Meta:
model = CommonEditor
fields = ('pk', 'color')
def get_color(self, common_editor):
return ColorSerializer(common_editor.color_set.all(), many=True).data
Documentation: http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
The CommonAttributesSerializer search for a color attribute in CommonEditor's instance, but it couldn't find. In DRF serializer, a parameter called source will says explicitly where to look for the data. So , change the serializer as below:
class CommonAttributesSerializer(serializers.ModelSerializer):
color = ColorSerializer(many=True, read_only=True, <b>source='color_set'</b>)
class Meta:
model = CommonEditor
fields = ('pk', 'color')
Reference : DRF Fields -source

django queryset annotate all field in childtable

models.py
class Userinfo(models.Model):
useruid = models.BigAutoField(db_column='UserUID', primary_key=True)
useremail = models.CharField(
db_column='UserEmail', unique=True, max_length=100)
userpassword = models.CharField(db_column='UserPassword', max_length=128)
passwordsalt = models.CharField(db_column='PasswordSalt', max_length=128)
class Meta:
managed = False
db_table = 'userinfo'
class Postinfo(models.Model):
postuid = models.BigAutoField(db_column='PostUID',primary_key=True)
useruid = models.ForeignKey(
'Userinfo', db_column='UserUID', on_delete=models.CASCADE)
content = models.TextField(db_column='Content')
class Meta:
managed = False
db_table = 'postinfo'
if i get userlist and user's last post
i think use annotate
models.Userinfo.objects.all().annotate(lastpost="??").order_by("-useruid")
what values in "??"
like this form
[{userinfo1,"lastpost":{postinfofields}},{userinfo2,"lastpost":{postinfofields}},{userinfo3,"lastpost":{postinfofields}}]
can i this query not use forloop?
Serializer.py
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Postinfo
fields = ('postuid','content')
class UserSerializer(serializers.ModelSerializer):
lastpost = PostSerializer()
class Meta:
model = Userinfo
fields = ['useruid', 'useremail', 'lastpost']
view.py
userinfos = models.Userinfo.objects.all().order_by("-useruid")
result = UserSerializer(userinfos,many=True)
print(result.data)
raise Exception
Got AttributeError when attempting to get a value for field `lastpost` on serializer `UserSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Userinfo` instance.
Original exception text was: 'Userinfo' object has no attribute 'lastpost'.
if i add read_only=True print this
[OrderedDict([('useuid', 1), ('useremail', 'test')]), OrderedDict([('useruid', 2), ('useremail', 'test2')])]
You can use model's property for this:
class Userinfo(models.Model):
useruid = models.BigAutoField(db_column='UserUID', primary_key=True)
useremail = models.CharField(
db_column='UserEmail', unique=True, max_length=100)
userpassword = models.CharField(db_column='UserPassword', max_length=128)
passwordsalt = models.CharField(db_column='PasswordSalt', max_length=128)
#property
def lastpost(self):
return self.postinfo_set.latest('postuid')
Now you dont need annotation, just use it for example in template like this:
{{ user.lastpost.content }}
UPD
To serialize property with ModelSerializer just add serializer's field 'lastpost':
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Postinfo
fields = ('postuid','content')
class UserSerializer(serializers.ModelSerializer):
lastpost = PostSerializer()
class Meta:
model = Userinfo
fields = ['useruid', 'useremail', 'lastpost']
UPD2
You can also implement logic directly on the serializer level, without model's property. Just use SerializerMethodField:
class UserSerializer(serializers.ModelSerializer):
lastpost = SerializerMethodField()
class Meta:
model = Userinfo
fields = ['useruid', 'useremail', 'lastpost']
def get_lastpos(self, obj):
last = obj.postinfo_set.latest('postuid')
serializer = PostSerializer(last)
return serializer.data

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