I have the following structure of Parent and Child models, where the child references the parent.
class ParentModel(models.Model):
name = models.CharField(max_length=255)
class ChildModel(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(
ParentModel, related_name='children', on_delete=models.CASCADE
)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
class ParentSerializer(serializers.ModelSerializer):
class Meta:
model = ParentModel
fields = (
'name',
'children',
)
class ChildSerializer(serializers.ModelSerializer):
class Meta:
models = ChildModel
fields = (
'name'
)
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = ParentModel.objects.all()
class ChildViewSet(viewsets.ModelViewSet):
serializer_class = ChildSerializer
def get_queryset(self):
user = self.request.user
return ChildModel.objects.filter(created_by=user)
I would like for ParentSerializer.children to only include the ChildModel objects with created_by as self.request.user.
What is the correct way to filter ParentSerializer.children to the current user?
I am open to changing the models as well.
First I think you got a n+1 issue with your code.
When DRF will serialize ParentModel, accessing current_parent.children.all() will produce an SQL query for each parent.
To prevent this you can use prefetch_related so:
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = ParentModel.objects.prefetch_related(Prefetch("children"))
This will result in 2 SQL queries instead of N+1 (with N being the number of ParentModel row).
Additionnaly you can use prefetch_related to filter the related model:
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
def get_queryset(self):
user = self.request.user
return ParentModel.objects.prefetch_related(Prefetch("children", queryset=ChildrenModel.objects.filter(created_by=user)))
Which is what you are looking for I think.
Related
I found this very useful article how to use common models in the DRF.
Common Models in Django and the DRF
So I wanted to have a create_by, created_when, updated_by and updated_when attribute for all by objects in the database. I used the viewsets.ModelViewSet together with mixins.ListModelMixin and mixins.CreateModelMixin before and it worked. I just replaced the viewsets.ModelViewSet with my new CommonViewSet class.
This is my code:
views.py
class CommonViewSet(viewsets.ModelViewSet):
"""Ensure the models are updated with the requesting user."""
def perform_create(self, serializer):
"""Ensure we have the authorized user for ownership."""
serializer.save(created_by=self.request.user, updated_by=self.request.user)
def perform_update(self, serializer):
"""Ensure we have the authorized user for ownership."""
serializer.save(updated_by=self.request.user)
class TagViewSet(CommonViewSet,
mixins.ListModelMixin,
mixins.CreateModelMixin):
"""Manage tags in the database"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer
serializers.py
class CommonSerializer(serializers.ModelSerializer):
"""Ensure the fields are included in the models."""
common_fields = ['created_by', 'created_at', 'updated_by', 'updated_at']
class TagSerializer(CommonSerializer):
"""Serializer for tag objects"""
class Meta:
model = Tag
fields = (['id', 'name'] + CommonSerializer.common_fields)
read_only_fields = ('id',)
models.py
class CommonModel(models.Model):
"""Common fields that are shared among all models."""
created_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,
editable=False, related_name="+")
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,
editable=False, related_name="+")
created_at = models.DateTimeField(auto_now_add=True,
editable=False)
updated_at = models.DateTimeField(auto_now=True,
editable=False)
class Tag(CommonModel):
"""Tag to be used for device type"""
name = models.CharField(max_length=255)
def __str__(self):
return self.name
But now I get this error message:
class TagViewSet(CommonViewSet,
TypeError: Cannot create a consistent method resolution
order (MRO) for bases CreateModelMixin, ListModelMixin
The DRF ModelViewseT Written as follows
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
So in TagViewSet, You don't need to import mixins again.
You can write this as follows
class TagViewSet(CommonViewSet):
...
I have two model, one for Question and two for answer
tow have a forginkey to one and one have a forginkey to users
for this purpose I use a To be grouped together at the same endpoint I add the second to the first in serializer
With this solution, Django accessed the database with the number of answers in the second form and I cannot use Select_related here
the question is, how can I reduce database hits to the second model
Models.py
class Consultation(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
create_at = models.DateTimeField(auto_now_add=True)
files = models.FileField(upload_to="media/files", null=True, blank=True,
validators=[FileExtensionValidator(['pdf', 'jpg', 'png'])])
def __str__(self):
return self.user.username
class Meta:
ordering = ['-create_at']
class ConsultaionAnswer(models.Model):
consultation = models.ForeignKey(
Consultation, on_delete=models.CASCADE, related_name='reply')
answer = models.TextField(null=True, blank=True)
def __str__(self):
return self.consultation.content[:20] + "..."
serializers.py
class ConsultaionAnswerSerilaizers(serializers.ModelSerializer):
class Meta:
model = ConsultaionAnswer
fields = ('answer',)
class ConsultationSerilaizers(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True,
default=serializers.CurrentUserDefault()
)
username = serializers.StringRelatedField(
source='user.username',
read_only=True,
)
reply = ConsultaionAnswerSerilaizers(read_only=True, many=True)
class Meta:
model = Consultation
fields = (
"id",
"user",
"content",
'create_at',
'files',
'username',
"reply"
)
views.py
class ConsultationView(APIView):
serializer_class = ConsultationSerilaizers
def get(self, request, format=None):
consultat = Consultation.objects.select_related().filter(user=request.user)
serializer = self.serializer_class(
consultat, many=True, context={'request': request})
return Response(serializer.data)
django debug tool bar
This should works:
consultat = Consultation.objects.prefetch_related('reply').filter(user=request.user)
Since it is a one-to-many relation, you'll need a prefetch_related instead (see this link). And you also need to specify which field(s) you want to prefetch as well.
prefetch_related, on the other hand, does a separate lookup for each relationship, and does the ‘joining’ in Python. This allows it to prefetch many-to-many and many-to-one objects, which cannot be done using select_related,
I want to make blog web , I want to order on a computed SerializerMethodField, such as likes_count. Here are my Code
models.py
class Post(models.Model):
title = models.CharField(max_length=255, unique=True)
content = models.TextField()
like = models.models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name='likes', blank=True)
serializer.py
class PostSerializer(serializers.ModelSerializer):
likes_count = serializer.SerializerMethodField()
class Meta:
model = Post
fields =[ 'title, content, likes' ]
def get_likes_count(self, obj):
return obj.like.count()
views
class PostGenericAPIView(generics.GenericAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = (filters.OrderingFilter)
You can just annotate the value of count and order by it. For example:
from django.db.models import Count
class PostGenericAPIView(generics.GenericAPIView):
queryset = Post.objects.annotate(likes_count=Count('like')).order_by('likes_count')
serializer_class = PostSerializer
filter_backends = (filters.OrderingFilter)
Also, to reduce DB hits, you can directly access the value of likes_count in serializer:
class PostSerializer(serializers.ModelSerializer):
likes_count = serializers.IntegerField() # No need for serializer method field
Here's are examples I have:
models.py:
class Example(models.Model):
title = models.CharField(...)
description = models.CharField(...)
class Foo(models.Model):
example = models.ManyToManyField(Example)
serializers.py:
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = '__all__'
depth = 1
views.py:
...
serialized_data = [FooSerializer(foo).data for foo in Foo.objects.all().get]
In output, I receive only Example's IDs, but is there any way I could get title and description fields also (details of m2mfield)? As I understand, Foo.objects.all().get simply doesn't contain this data, but maybe I could somehow get it and use it?
I could also rebuild models if needed, but currently I use m2mf because of needs to contain multiple objects as related to this model data.
update
models.py:
class Event(models.Model):
ts = models.BigIntegerField(editable=False)
class Foo(Event):
user = models.ForeignKey(User, ...)
example = *...(remains to be the same)*
foos = models.ForeignKey('self', **somemore** null=True)
serializers.py:
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = '__all__'
def to_representation(self, instance):
result = {'ts': instance.ts}
if isinstance(instance, Foo):
result['foo'] = FooSerializer(instance).data
return result
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
class FooSerializer(serializers.ModelSerializer):
# user = UserSerializer(read_only=True) # with this I have an error: Got AttributeError when attempting to get a value for field 'username' on #serializer 'UserSerializer'
class Meta:
model = Foo
fields = '__all__'
depth = 1
You could use depth attribute to achieve desired output.
The default ModelSerializer uses primary keys for relationships, but
you can also easily generate nested representations using the depth
option.The depth option should be set to an integer value that
indicates the depth of relationships that should be traversed before
reverting to a flat representation.
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = '__all__'
depth = 1
Apart from the answer, I would like to change your views.py code, cause it seems like very bad :(. Do it on DRF Way as
serialized_data = FooSerializer(Foo.objects.all(), many=True).data<br>
Example View
from rest_framework.viewsets import ModelViewSet
class FooViewset(ModelViewSet):
serializer_class = FooSerializer
queryset = Foo.objects.all()
UPDATE-1
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
exclude = ('password',) # add fields that are need to be excluded
class FooSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Foo
fields = '__all__'
depth = 1
depth = 1 will serializer all fields in the model, (It's same as setting the fields=='__all__' in Meta class of serializer)
UPDATE-2
class FooSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Foo
fields = '__all__'
depth = 1
def to_representation(self, instance):
real_data = super().to_representation(instance).copy()
# DO YOUR EXTRA CHECKS
child = UserSerializer(instance.child_foo).data
if child:
real_data.update({"child_data": child})
# After your checks, add it to "real_data"
return real_data
and I assumed I have a Foo model as
class Foo(models.Model):
example = models.ManyToManyField(Example)
user = models.ForeignKey(User)
child_foo = models.ForeignKey('self', null=True, blank=True)
In your serializer add depth = 1. Example where 'users' is the related field:
FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = ('id', 'account_name', 'users', 'created')
depth = 1
I have two models, Book and ReadBy as specified in models.py:
class Book(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(MyUser)
class ReadBy(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(MyUser)
book = models.ForeignKey(Book)
views.py:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def pre_save(self, obj):
obj.owner = self.request.user
class ReadByViewSet(viewsets.ModelViewSet):
queryset = ReadBy.objects.all()
serializer_class = ReadBySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def pre_save(self, obj):
obj.owner = self.request.user
serializers.py:
class BookSerializer(serializers.ModelSerializer):
readby = serializers.SerializerMethodField('get_ready_by')
def get_read_by(self, obj):
user = self.context['request'].user
book = obj
return ReadBy.objects.filter(book=book, owner=user).exists()
class Meta:
model = Book
fields = ('id', 'created', 'owner', 'readby')
class ReadBySerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.id')
book = serializers.Field(source='book.id')
class Meta:
model = ReadBy
fields = ('id', 'created', 'owner', 'book')
Book is a ForeignKey in ReadBy. The reason for this is that I want to see if the book has been read by defining the readby field in BookSerializer to true or false.
Now when I try to POST a ReadBy item I cannot explicitly set the book_id, even if I try do I still get the same error:
"Column 'book_id' cannot be null"
What I need to know is how I can explicitly specify book_id to be able to "read" a book.
Thanks.
class ReadBy(models.Model):
... snip ...
book = models.ForeignKey(Book, null=True, blank=True)
OK I solved this.
I forgot to define the book field in the ReadBySerializer as PrimaryKeyRelatedField:
book = serializers.PrimaryKeyRelatedField(source='book.id')
In fact it seems like I can remove this line altogether since PrimaryKeyRelatedField is default if nothing is specified.