Django DRF: POST to CreateAPIView with list of PrimaryKeyRelatedFields - django

I have a many-to-one relationship between the following models
class Story(models.Model):
id = models.CharField(max_length=12, primary_key=True)
class Article(models.Model):
id = models.CharField(max_length=16, primary_key=True)
title = models.CharField(max_length=500)
address = models.URLField()
story = models.ForeignKey(to=Story, blank=True, null=True, on_delete=models.CASCADE)
Suppose I post several article objects to the database successfully.
I identify that the articles with the ids
['1', '2', '3']
are all reporting on a particular Story.
I want create a Story via a POST method to a CreateAPIView view like this
POST http://127.0.0.1/news/story {articles': ['1', '2', '3']}
Here is my serializer
class StorySerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
articles = serializers.PrimaryKeyRelatedField(many=True, allow_empty=False, queryset=Article.objects.all())
class Meta:
model = Story
fields = ('id', 'articles')
Here is my view
class StoryList(generics.ListCreateAPIView):
serializer_class = StorySerializer
queryset = Story.objects.all()
I want to ensure that 1) the articles exist. 2) the article story is updated before the Story object is created.
Suppose I run this as it is, I will get the following error:
Got a TypeError when calling Story.objects.create(). This may be
because you have a writable field on the serializer class that is not
a valid argument to Story.objects.create(). You may need to make the
field read-only, or override the StorySerializer.create() method to
handle this correctly.
So here is an attempt to override the create() method:
def create(self, validated_data):
story_id = None
for article_id in validated_data['articles']:
article = Article.objects.get(id=article_id)
story_id = article.story_id
if story_id:
break
story = Story.objects.get(id=story_id) if story_id else Story.objects.create()
for article_id in validated_data['articles']:
article = Article.objects.get(id=article_id)
article.story_id = story.id
article.save()
story.save()
return story
def update(self, instance, validated_data):
return self.create(validated_data)
The idea here is make sure there are no overlapping stories by merging them.
When I try POST to this view, I encounter a DoesNotExist thrown by the line Article.objects.get(id=article_id)
My questions are
1) Minor : Why am I getting this error
2) Major : Is there a cleaner / correct way of addressing such a use case in django?
Thank you

class StoryList(generics.ListCreateAPIView):
serializer_class = StorySerializer
query_set = Story.objects.all()
It's should be quertset not query_set.
1) Minor : Why am I getting this error
You gived an illegal article_id which is not exist.
2) Major : Is there a cleaner / correct way of addressing such a use case in django?
drf-writable-nested can handle nested write in drf well.

Related

display only some fields in get api response django serializer

I have an example model which has a fk relation with user model and Blog model. Now I have a get api which only requires certain fields of user to be displayed.
My model:
class Example(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="user_examples",
)
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
null=True,
related_name="blog_examples",
)
/................./
Now my view:
class ExampleView(viewsets.ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def list(self, request, *args, **kwargs):
id = self.kwargs.get('pk')
queryset = Example.objects.filter(blog=id)
serializer = self.serializer_class(queryset,many=True)
return Response(serializer.data,status=200)
My serializer:
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = Example
fields = ['user','blog','status']
depth = 1
Now when I call with this get api, I get all example objects that is required but all the unnecessary fields of user like password, group etc . What I want is only user's email and full name. Same goes with blog, I only want certain fields not all of them. Now how to achieve this in a best way??
You will have to specify the required fields in nested serializers. e.g.
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ['title', 'author']
class ExampleSerializer(serializers.ModelSerializer):
blog = BlogSerializer()
class Meta:
model = Example
fields = ['user','blog','status']
are you setting depth in serializer's init method or anywhere else? beacause ideally it should only display id's and not anything else. if yes then set depth to zero and use serializer's method field to return data that you need on frontend. I can provide you with example code samples

How can I prefetch nested tables?

I'm working on an app with a DRF API. Development has been going on for some months, but only now are we running into performance issues when populating the database with actual data. I have done some profiling and found out that many endpoints query the database hundreds of times to fetch the necessary data. This is something that can be solved with select_related and prefetch_related, that much I know, but I'm having a hard time picturing it.
Models
class LegalFile(models.Model):
code = models.CharField(max_length=75)
description = models.CharField(max_length=255)
contactrole = models.ManyToManyField(LegalFileContactRole, blank=True, related_name='contactroles')
class LegalFileContactRole(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.DO_NOTHING, blank=True, related_name='legal_file_contact')
subtype = models.ForeignKey(ContactSubType, on_delete=models.DO_NOTHING, related_name='legal_file_contact_role', null=True)
class Contact(models.Model):
name = models.CharField(max_length=150)
surname_1 = models.CharField(max_length=50,blank=True, null=True)
surname_2 = models.CharField(max_length=50,blank=True, null=True)
class ContactSubType(models.Model):
pass
Say I want to list all the LegalFiles in the database, and fetch the names and both surnames of the contacts associated to each LegalFile through LegalFileContactRole. Is there a DRYer way than using lowercase notation to prefetch like LegalFile.prefetch_related('contactrole__contact__name', 'contactrole__contact__surname_1', 'contactrole__contact__surname_2')? This kind of nested relationship is a recurring thing in the app.
Editing in response to Marc Compte's comment:
I have not explained myself properly, I think. In fact, this is missing a good chunk of context - I have not mentioned serializers at all, for instance.
I have taken the approach described in this post and created a method to set eager loading up:
class LegalFileReadSerializer(serializers.ModelSerializer):
contactrole = LegalFileContactRoleSerializer(many=True)
#classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('contactrole__contact__name', 'contactrole__contact__surname_1', 'contactrole__contact__surname_2')
return queryset
class Meta:
model = LegalFile
fields = '__all__'
read_only_fields = ('__all__',)
class LegalFileViewSet(CustomViewSet):
model = models.LegalFile
read_serializer_class = serializers.LegalFileReadSerializer
write_serializer_class = serializers.LegalFileWriteSerializer
def get_queryset(self):
queryset = super().get_queryset()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
(Note this is still work in progress and will have to be decoupled further for reuse, in a mixin or something)
Could I, so to speak, chain serializers? Like:
class LegalFileReadSerializer(serializers.ModelSerializer):
contactrole = LegalFileContactRoleSerializer(many=True)
#classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('contactrole',)
return queryset
class Meta:
model = LegalFile
fields = '__all__'
read_only_fields = ('__all__',)
class LegalFileContactRoleSerializer(serializers.ModelSerializer):
contact = ContactReadSerializer()
subtype = ContactSubTypeReadSerializer()
#classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related('contact',)
return queryset
class Meta:
model = models.LegalFileContactRole
fields = '__all__'
class ContactReadSerializer(serializers.ModelSerializer):
# And so on
pass
This, in my head at least, makes sense, but I don't know if this behavior is possible or even advisable. It seems that if I chain serializers like this (implying again that it's possible to do so) I'm gonna run into problems picking up too much unneeded information.

DRF serialize through related models data

I've got a problem with DRF serialization. My main goal is to create an instance which has a related field but instead of providing the related models id i want to use it's other unique field. At the same time when I will serialize my model to present it (not create) i would like to get the default related fields value. Here's an example
class Comment(models.Model):
description = models.TextField()
creator = models.ForeignKey(User, ...)
x = Creator.objects.get(pk=1)
print(x.unique_field)
> 'some data'
client.post('comment-detail', data={
'description': 'some description',
'creator_unique_field': 'some data'
})
# this should create a comment while using some unique creators field
# which is not pk or id
print(client.get('comment-detail', data={'pk':1}))
{
'description' 'some description',
'creator': 1,
}
I don't know If i should change models Serializer of ViewSet's create() and retrieve(). Im starting with DRF and can't get my head around it
Overriding the Serializer create method is a good place for this. one can query for the unique_field user there.
class CommentView(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(creator=self.request.user)
class CommentSerializer(serializers.Serializer):
creator_unique_field = serializer.SomeField()
def create(self, validated_data):
creator = Creator.objects.get(unique_field=validated_data['creator_unique_field'])
comment, created = Comment.objects.get_or_create(creator=creator, **validated_data)
return comment
class Meta:
model = Comment
fields = '__all__'

Polymorphic models serializer

I'm using a Polymorphic model for setting up notifications:
My models:
class Notification(PolymorphicModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_by = models.ForeignKey(ElsUser, on_delete=models.CASCADE, default=None, related_name="creatednotifications")
created_on = models.DateTimeField(default=timezone.now)
created_for = models.ForeignKey(ElsUser, on_delete=models.CASCADE, default=None, related_name="receivednotifications")
read = models.DateTimeField(default=None, null=True, blank=True)
message = models.CharField(default=None, blank=True, null=True, max_length=800)
#property
def total(self):
return self.objects.filter(created_for=self.request.user).count()
#property
def unread(self):
return self.objects.filter(created_for=self.request.user,read=None).count()
#property
def read(self):
return self.objects.filter(created_for=self.request.user).exclude(read=None).count()
class WorkflowNotification(Notification):
# permission_transition = models.ForeignKey(WorkflowStatePermissionTransition, on_delete=models.CASCADE)
action = models.ForeignKey(UserAction, on_delete=models.CASCADE)
Currently i have just one model WorkFlowNotification inheriting from the Polymorphic model,but many would be there in the future.
Im trying to get the count(total) of notifications for the logged in user in the API ..total is given as property field to help in the same
my serializer:
class NotificationSerializer(serializers.ModelSerializer):
total = serializers.ReadOnlyField()
read = serializers.ReadOnlyField()
unread = serializers.ReadOnlyField()
class Meta:
model = Notification
fields = ['id', 'total','read', 'unread']
In the view:
class NotificationsMeta(generics.ListAPIView):
serializer_class = NotificationSerializer
queryset = Notification.objects.all()
When i try to run the server it shows:
Got AttributeError when attempting to get a value for field `total` on serializer `NotificationSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `WorkflowNotification` instance.
Original exception text was: Manager isn't accessible via WorkflowNotification instances.
Since you need the 'meta data' only, what is the use of making a model serializer? Or any serializer, for that matter? Serializers will give you serialized instances of the objects of your model. So if you have multiple objects, you will get multiple serialized objects in response.
Just make your view a normal APIView. Since there is no necessity of serializing anything.
class NotificationsMeta(APIView):
def get(self, request, format=None):
qs = Notification.objects.filter(created_for=self.request.user)
response = {
'total': qs.count(),
'read': qs.filter(read=None).count(),
'unread': qs.exclude(read=None).count()
}
return Response(response)
Now remove those property functions from your model.
I didn't test your queries, just copied them from your model. You will need to check if they are working properly. Hope this helps.
I am not sure about how calling a model property who is responsible for querying in model can give appropriate data from serializer. Unfortunately i do have knowledge gap about that. I am thinking about an alternative solution. I hope following should work.
class NotificationSerializer(serializers.ModelSerializer):
total = serializers.serializers.SerializerMethodField()
read = serializers.ReadOnlyField()
unread = serializers.ReadOnlyField()
class Meta:
model = Notification
fields = ['read', 'unread']
def get_total(self, obj):
user = self.context['request'].user
return Notification.objects.filter(created_for=user).count()
If this work then you can able to do similar kind of thing for read and unread too.
In order to get notification for current_user we need to overwrite get_queryset from view.
class NotificationsMeta(generics.ListAPIView):
serializer_class = NotificationSerializer
def get_queryset(self):
return Notification.objects.filter(created_for=self.request.user)

Django Rest Framework how to save a model with Related Field based on ID

I'm kind of new to DRF. I have Record model that looks like this:
class Records(models.Model):
owner = models.ForeignKey(User, null=True)
activity = models.ForeignKey(Activity, null=True)
time_start = models.DateTimeField(null=True)
time_end = models.DateTimeField(null=True)
...
The RecordSerializer is this one:
class RecordSerializer(serializers.ModelSerializer):
now = datetime.today()
owner = serializers.Field(source='owner.username')
time_start = serializers.DateTimeField(source='now')
class Meta:
model = Records
fields = ("owner", "activity", "time_start")
And this is the view:
class StartApiView(generics.CreateAPIView):
model = Records
serializer_class = RecordSerializer
def pre_save(self, obj):
obj.owner = self.request.user
The POST request is sent from Backbone and it includes a field with the activity id, for example "{activity:12}". What should I do if I want the view to save the Record and set the activity to the Activity with the id of 12?
The accepted answer was true for DRF v2.x but is no longer for newer (3.x) versions, as it would raise this AssertionError:
AssertionError: Relational field must provide a queryset argument, or set read_only=True.
For newer versions, just add the queryset argument to it:
class RecordSerializer(serializers.ModelSerializer):
activity = serializers.PrimaryKeyRelatedField(queryset=Activity.objects.all())
// [...]
Django REST Framework provides a PrimaryKeyRelatedField for exactly this use case.
class RecordSerializer(serializers.ModelSerializer):
activity = serializers.PrimaryKeyRelatedField()
owner = serializers.CharField(read_only=True, source='owner.username')
time_start = serializers.DateTimeField(source='now')
class Meta:
model = Records
fields = ("owner", "activity", "time_start")
This will produce output similar to what you are looking for, and it will accept the id of the activity when you want to update it.