Django rest framework updating nested serializers - django

I have two models, one with a foreignkey relationship to the other. I am wanting to be able to create or update multiple entries at the same time. I would not be creating and updating simultaneously.
I keep getting a bit confused about what is going on with people's examples. I was hoping someone could not only show me what I need to do, but also give a bit of an explanation about what is going on at each step. I'm specifically not understanding what the validated_data.get() method is doing, or what exactly is an instance, and what is being done with them.
class ExtraFieldsSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = ExtraFields
fields = [
'id',
'job',
'custom_data',
]
class JobWriteSerializer(serializers.ModelSerializer):
extra_fields = ExtraFieldsSerializer(many=True)
class Meta:
model = Job
fields = [
"extra_fields",
"custom_data_fields",
]
# Make JobSerializer able to create custom fields.
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
# Make JobSerializer able to update custom fields
def update(self, instance, validated_data):
pass

First of all, you could probably use drf-writeable-nested for that, as it pretty much does exactly what you want.
But it never hurts to understand what's going on:
def create(self, validated_data):
extra_fields = validated_data.pop("extra_fields")
user = User.objects.get(id=self.context['request'].user.id)
client = Client.objects.get(user=self.context['request'].user)
new_job = Job.objects.create(user=user, client=client, **validated_data)
new_job.save()
for field in extra_fields:
new_field = ExtraFields.objects.create(job=new_job, custom_data=field['custom_data'])
new_field.save()
return new_job
Since JobWriteSerializer is the parent serializer, we will start with that.
the validated_data argument that is passed to your create function contains, well, as the name suggests, the validated data. This means, you can assume that all the constraints you defined in your serializer (required fields, max_length, min_length etc) hold, and you don't need to check them.
The code looks fine, it seems you are popping all the extra_fields from your serializer and create ExtraField objects from them.
You specifically ask what's going on when using validated_data.get but you are using validated_data.pop. the difference is, that when using get, the retrieved data stays in the dictionary, while pop removes it.
This is especially handy for cases where you also have to create nested objects, consider this (some things omitted as they are not relevant):
class MyModel(models.Model):
text = models.CharField(max_length=10, related_name='children')
class MyChildModel(models.Model):
someVal = models.BooleanField()
model = models.ForeignKey(MyModel)
class MyChildSerializer(serializers.ModelSerializer):
someVal = serializers.BooleanField()
class MyModelSerializer(serializers.ModelSerializer):
text = serializers.TextField(...)
childen = ChildrenSerializer(many=True)
def create(self, validated_data):
children = validated_data.pop('children', []) #POP!
instance = super().create(validated_data)
for c in children:
MyChildSerializer.objects.create(model=instance, **c)
return instance
You can test this yourself, if you use get instead of pop here, your serializer will rightfully complain that there are children objects inside the validated_data object, and drf cannot create nested relations out of the box. When you pop them, the serializer does not have those fields anymore and it works.
Note that this approach would not be efficient for your case, as you manually pass data to your Job object (like the user and the client) which you do not get via the passed data, but from your request. You can, if you want, get around that by using get_serializer_context, but lets just say that this is out of the questions scope.
On to your update method, I suggest something like that (not tested, but you get the idea):
def update(self, instance, validated_data):
extra_fields = validated_data.pop('extra_fields', []) # POP the extra fields
instance = super().update(instance, validated_data) # !!!!!
for extra in extra_fields:
#retrieve your extra fields and update them
myExtra = ExtraFields.objects.get(id=extra['id])
....
The passed argument instance is actually the already existing instance of your Job model. This is the object you want to update. Note that again, I popped the extra_fields, precisely for doing what I described above: I used drf itself to modify/update the object, since I only have to implement the update for the child elements.

Related

How to customise update method for ManyToMany fields in Django Rest Framework

I have a few models with ManyToMany relationships between them and I need to override the create and update method to make the POST and PUT request work in DRF.
Here's my code so far:
class CreateFolderSerializer(serializers.ModelSerializer):
class Meta:
model = Folder
fields = ("id", "title", "description", "users")
def create(self, validated_data):
users = validated_data.pop(
'users') if 'users' in validated_data else []
folder = Folder.objects.create(**validated_data)
folder.users.set(users)
return folder
This create method works perfectly.
I tried re-creating the same logic for the update method, but it doesn't work:
class FolderSerializer(serializers.ModelSerializer):
documents = DocumentSerializer(many=True, read_only=True)
class Meta:
model = Folder
fields = '__all__'
def update(self, instance, validated_data):
users = validated_data.pop('users') if 'users' in validated_data else []
instance.users.set(users)
instance.save()
return instance
When I send a PUT request, the object does not get modified at all, it gets deleted altogether.
Any clue?
Thanks a lot.
Don' t set documents as read_only:
documents = DocumentSerializer(many=True)
By calling instance.users.set(users), you replace the precedent list, for example if you pass an empty list all associated users are removed. So if you want to leave the current users associated you need to insert their primary keys in the request data (for the users key), otherwise use instance.users.add instead of instance.users.set.

How to implement a simple "like" feature in Django REST Framework?

I'm a beginner building the backend API for a social media clone using DRF. The frontend will be built later and not in Django. I'm currently using Postman to interact with the API.
I'm trying to implement a "like" feature as you would have on Facebook or Instagram. I cannot send the correct data with Postman to update the fields which bear the many-to-many relationship.
Here is some of my code:
models.py
class User(AbstractUser):
liked_haikus = models.ManyToManyField('Haiku', through='Likes')
pass
class Haiku(models.Model):
user = models.ForeignKey(User, related_name='haikus', on_delete=models.CASCADE)
body = models.CharField(max_length=255)
liked_by = models.ManyToManyField('User', through='Likes')
created_at = models.DateTimeField(auto_now_add=True)
class Likes(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
haiku = models.ForeignKey(Haiku, on_delete=models.CASCADE)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'password', 'url', 'liked_haikus']
extra_kwargs = { 'password' : {'write_only': True}}
def create(self, validated_data):
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
token = Token.objects.create(user=user)
return user
class HaikuSerializer(serializers.ModelSerializer):
class Meta:
model = Haiku
fields = ['user', 'body', 'liked_by', 'created_at']
class LikesSerializer(serializers.ModelSerializer):
model = Likes
fields = ['haiku_id', 'user_id']
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=True, methods=['get'])
def haikus(self, request, pk=None):
user = self.get_object()
serializer = serializers.HaikuSerializer(user.haikus.all(), many=True)
return Response(serializer.data)
class UserCreateViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
class HaikuViewSet(viewsets.ModelViewSet):
queryset = Haiku.objects.all()
serializer_class = HaikuSerializer
permission_classes = [permissions.IsAuthenticated]
class LikesViewSet(viewsets.ModelViewSet):
queryset = Likes.objects.all()
serializer_class = LikesSerializer
permission_classes = [permissions.IsAuthenticated]
urls.py
router = routers.DefaultRouter(trailing_slash=False)
router.register('users', views.UserViewSet)
router.register('haikus', views.HaikuViewSet)
router.register('register', views.UserCreateViewSet)
router.register('likes', views.LikesViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api-auth-token', obtain_auth_token, name='api_token_auth')
]
Using the Django Admin I can manually set users to like posts and the fields in the db will update and reflect in API requests.
With Postman, I've tried sending both PUT and PATCH to, for example:
http://127.0.0.1:8000/haikus/2
with "form data" where key ="liked_by" and value="3" (Where 3 is a user_id). I got a 200 response and JSON data for the endpoint back, but there was no change in the data.
I've tried GET and POST to http://127.0.0.1:8000/likes and I receive the following error message:
AttributeError: 'list' object has no attribute 'values'
I've looked at nested-serializers in the DRF docs, but they don't seem to be quite the same use-case.
How can I correct my code and use Postman to properly update the many-to-many fields?
I think I need to probably write an update function to one or several of the ViewSets or Serializers, but I don't know which one and don't quite know how to go about it.
All guidance, corrections and resources appreciated.
To update the liked_by Many2Many field, the serializer expect you to provide primary key(s).
Just edit your HaikuSerializer like the following. It will work.
class HaikuSerializer(serializers.ModelSerializer):
liked_by = serializers.PrimaryKeyRelatedField(
many=True,
queryset=User.objects.all())
class Meta:
model = models.Haiku
fields = ['created_by', 'body', 'liked_by', 'created_at']
def update(self, instance, validated_data):
liked_by = validated_data.pop('liked_by')
for i in liked_by:
instance.liked_by.add(i)
instance.save()
return instance
adnan kaya has provided the correct code and I have upvoted him and checked him off as the correct answer. I want go through his solution to explain it for future readers of this question.
liked_by = serializers.PrimaryKeyRelatedField(
many=True,
queryset=User.objects.all())
You can read about PrimaryKeyRelatedField here: https://www.django-rest-framework.org/api-guide/relations/
Since liked_by is a ManyToManyField it has special properties in that ManyToMany relations create a new table in the DB that relates pks to each other. This line tells Django that this field is going to refer to one of these tables via its primary key. It tells it that liked by is going to have multiple objects in it and it tells it that these objects are going to come from a particular queryset.
def update(self, instance, validated_data):
liked_by = validated_data.pop('liked_by')
for i in liked_by:
instance.liked_by.add(i)
instance.save()
return instance
ModelSerializers is a class that provides its own built in create and update functions that are fairly basic and operate in a straightforward manner. Update, for example, will just update the field. It will take the incoming data and use it to replace the existing data in the field it is directed at.
You can read more about ModelSerializers here: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
You can overwrite these functions and specify custom functions by declaring them. I have declared update here. Update is a function that takes 3 arguments. The first is self. You can call this whatever you want, but there is a strong convention to call it self for readability. Essentially this is importing the class the function belongs, into the function so you can utilize all that classes functions and variables. Next is instance. Instance is the data that is currently in the entry you are trying to update. It is a dictionary like object. Finally, there is validated_data. This is the data you are trying to send to the entry to update it. When using form data, for example, to update a database, this will be a dictionary.
liked_by = validated_data.pop('liked_by')
Because validated_data is a dictionary you can use the .pop() method on it. Pop can take the key of the dictionary and "pop it off" leaving you with the value (more formally, .pop('key') will return its 'value'). This is nice because, at least in my case, it is the value that you want added to the entry.
for i in liked_by:
instance.liked_by.add(i)
this is a simple python for-loop. A for loop is here because in my use-case the value of the validated_data dictionary is potentially a list.
The .add() method is a special method that can be used with ManytoMany relationships. You can read about the special methods for ManytoMany relations here: https://docs.djangoproject.com/en/3.1/ref/models/relations/
It does what it advertises. It will add the value you send send to it to data you call it for, instead of replacing that data. In this case it is instance.liked_by (the current contents of the entry).
instance.save()
This saves the new state of the instance.
return instance
returns the new instance, now with the validated data appended to it.
I'm not sure if this is the most ideal, pythonic, or efficient way implementing a like feature to a social media web app, but it is a straightforward way of doing it. This code can be repurposed to add all sorts of many-to-many relationships into your models (friends lists/followers and tags for example).
This is my understanding of what is going on here and I hope it can help make sense of the confusing topic of ManytoMany relationships for clearer.

Advance serialization in DRF

I'm trying to achieve something with Django Rest Framework.
The idea is for a model to have several fields of several types in read-only, and have the same fields writable for the user that would take precedence when serving the data.
Since this should not be very clear, an example :
The model would be :
class Site(models.Model):
title = models.CharField(_('Title'),max_length=300)
title_modified = models.CharField(_('Title'),max_length=300)
The viewset to be defined :
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
The serializer :
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
What i want to do is be able to only serve the "title" data to the client, but the title would have either the title field if title_modified is empty or the title_modified field if it's not empty.
On the same idea when the client writes to the title i would like my server to write the data to title_modified instead and always leave the title info untouched.
I don't know how to achieve this a way that's generic enough to be applicable to all types of fields.
I thought it would simply require some magic on the serialization/unserialization but i can't seem to find it.
Any idea would be appreciated.
Thanks.
Since you are using ModelViewSets, you can override the default actions like .list(), .retrieve(), .create(), etc to do what you want or create your custom actions. Relevant info for ModelViewSets can be found here and here.
Actually, there are plenty of ways to go about this, and you do not even need to use ModelViewSet. You can actually use the generic views for this one. The real trick is to leverage the power of the CBVs and OOP in general. Here is a sample code wherein you provide a custom retrieval process of a single instance, while retaining all the rest's out-of-the-box behavior that a ModelViewSet provides.
class SiteViewSet(viewsets.ModelViewSet):
serializer_class = SiteSerializer
queryset = Site.objects.all()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# You can use the serializer_class specified above (SiteSerializer)
serializer = self.get_serializer(instance)
# Or perform some other manipulation on your instance first,
# then use a totally different serializer for your needs
instance = data_manipulation(instance)
serializer = AnotherSiteSerializer(instance)
# Finally return serialized data
return Response(serializer.data)
# Or if you want, return random gibberish.
return Response({'hello': 'world'})
I think you can override the to_representation() method of serializer to solve your problem:
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
depth = 1
exclude = ('title')
def to_representation(self, instance):
rep = super(SiteSerializer, self).to_representation(instance)
if not rep.get('title_modified', ''):
rep['title_modified'] = instance.title
return rep
This will return title as title_modified if title_modified is empty. User will always work on title_modified as required.
For more details please read modelserializer and Advanced serializer usage.

Modify Django Rest Framework ModelViewSet behavior

I basically have the following model in my project:
class ShellMessage(TimeStampedModel):
# There is a hidden created and modified field in this model.
ACTION_TYPE = (
('1' , 'Action 1'),
('2' , 'Action 2')
)
type = models.CharField(max_length=2,choices=ACTION_TYPE,default='1')
action = models.CharField(max_length=100)
result = models.CharField(max_length=300, blank=True)
creator = models.ForeignKey(User)
I created a serializer:
class ShellMessageSerializer(serializers.ModelSerializer):
class Meta:
model = ShellMessage
fields = ('action', 'type', 'result', 'creator')
And a ModelViewSet:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
My issue is the following:
When I create a new ShellMessage with a POST to my API, I don't want to provide the foreignKey of 'creator' but instead just the username of the guy and then process it in my ViewSet to find the user associated with this username and save it in my ShellMessage object.
How can I achieve this using Django rest Framework? I wanted to supercharge create() or pre_save() methods but I'm stuck as all my changes overwrite 'normal' framework behavior and cause unexpected errors.
Thank you.
I finally find my solution just after posting my question :)
So I did the following:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def pre_save(self, obj):
obj.creator = self.request.user
return super(ShellListViewSet, self).pre_save(obj)
This is working as expected. I hope I did well.
UPDATE: This topic seems to be a duplicate to Editing django-rest-framework serializer object before save
If you intend to intercept and perform some processing before the object gets saved in the model database, then what you're looking for is overriding the method "perform_create" (for POST) or "perform_update" (for PUT/PATCH) which is present within the viewsets.ModelViewSet class.
This reference http://www.cdrf.co/3.1/rest_framework.viewsets/ModelViewSet.html lists all available methods within viewsets.ModelViewSet where you can see that the "create" method calls "perform_create" which in turn performs the actual saving through the serializer object (the object that has access to the model):
def perform_create(self, serializer):
serializer.save()
We can override this functionality that is present in the base class (viewsets.ModelViewSet) through the derived class (the ShellListViewSet in this example) and modify the model attribute(s) that you want to be changed upon saving:
class ShellListViewSet(viewsets.ModelViewSet):
serializer_class = ShellMessageSerializer
queryset = ShellMessage.objects.all()
def findCreator(self):
# You can perform additional processing here to find proper creator
return self.request.user
def perform_create(self, serializer):
# Save with the new value for the target model fields
serializer.save(creator = self.findCreator())
You can also opt to modify the model fields separately and then save (probably not advisable but is possible):
serializer.validated_data['creator'] = self.findCreator()
serializer.save()
Later if the object is already created and you also want to apply the same logic during an update (PUT, PATCH), then within "perform_update" you can either do the same as above through the "serializer.validated_data['creator']" or you could also change it directly through the instance:
serializer.instance.creator = self.findCreator()
serializer.save()
But beware with such updating directly through the instance as from https://www.django-rest-framework.org/api-guide/serializers/ :
class MyModelSerializer(serializers.Serializer):
field_name = serializers.CharField(max_length=200)
def create(self, validated_data):
return MyModel(**validated_data)
def update(self, instance, validated_data):
instance.field_name = validated_data.get('field_name', instance.field_name)
return instance
This means that whatever you assign to the "instance.field_name" object could be overriden if there is a "field_name" data set within the "validated_data" (so in other terms, if the HTTP Body of the PUT/PATCH Request contains that particular "field_name" resulting to it being present in the "validated_data" and thus overriding whatever value you set to the "instance.field_name").

can I use duck-typing with class-based views

I may be completely off the reservation here. (Feel free to tell me if I am.)
My use case is that I have a list of schools. The school model is pretty simple:
class School(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:
class UserSchool(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
master_school = models.ForeignKey(School)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
So I set up a form to handle the editing of the UserSchool:
class UserSchoolForm(forms.ModelForm):
class Meta:
model = UserSchool
fields = ['name','mascot']
And now I have my EditSchool form:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get(self, request, *args, **kwargs):
school = self.get_object()
# make a copy of the school for this user
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
self.object = user_school
form = UserSchoolForm()
context = self.get_context_data(form=form)
return self.render_to_response(context)
I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.
Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?
(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)
Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.
To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:
return form_class(**self.get_form_kwargs())
And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:
kwargs.update({'instance': self.object})
Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.
I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get_object(self, queryset=None):
school = super(EditSchool, self).get_object(queryset=queryset)
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=self.request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
return user_school
There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.