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.
Related
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.
Assume I have two models and their serializers like this:
class Billing(models.Model):
...
class Transaction(models.Model):
billing = models.ForeignKey(Billing, null=False, blank=False)
...
class TransactionSerializer(serializers.ModelSerializer):
billing = serializers.PrimaryKeyRelatedField(queryset=Billing.objects.all())
class Meta:
model = Transaction
fields = '__all__'
Now I want to have an endpoint to post new transaction to a billing, something like this:
post http://address/billings/{id}/transactions [{other fields except billing because the billing exists in the address}]
For this purpose I have written a viewset like this:
class BillingTransactionList(generics.ListCreateAPIView):
serializer_class = TransactionSerializer
def get_queryset(self):
billing = get_object_or_404(Billing.objects.all(), pk=self.kwargs['pk'])
return Transation.objects.filter(billing=billing)
def perform_create(self, serializer):
billing = get_object_or_404(Billing.objects.all(), pk=self.kwargs['pk'])
return serializer.save(billing=billing)
But if billing does not exist in data that I get from request, serializer would be failed becuase it needs the billing in raw data of request. I have the billing from endpoint and I just want that serializer accepts the data and further I will add the billing as I have done in perform_create.
There is an option to add required=False to TransactionSerializer but I need this serializer in another places with required=True, Also there is an another solution to write another serializer, but in my real example the serializer is a big class and I do not want to write it again. I am looking for a simple solution to just ignore the disappearance of billing in data and let me define it whenever I want.
I am using django 1.11.3 and DRF 3.8.2.
I think it's better to use 2 different serializers (in a way that they have a parent serializer with most of the common code in it) for your actions.
but if you don't want to go with the 2 serializers solution, you can override the to_internal_value method and pick the url parameter(pk) for billing field if it is empty in the raw data (also, since the generic views pass themselves to serializers you have access to the url parameter in the serializer). so:
class TransactionSerializer(serializers.ModelSerializer):
billing = serializers.PrimaryKeyRelatedField(queryset=Billing.objects.all())
class Meta:
model = Transaction
fields = '__all__'
def to_internal_value(self, data):
billing_pk_in_url = self.context['view'].kwargs.get('pk', None)
if 'billing' not in data: ## or any other condition that you want
data['billing'] = billing_pk_in_url
return super().to_internal_value(data)
and now, you don't even have to override the perform_create method:
class BillingTransactionList(generics.ListCreateAPIView):
serializer_class = TransactionSerializer
def get_queryset(self):
billing = get_object_or_404(Billing.objects.all(), pk=self.kwargs['pk'])
return Transation.objects.filter(billing=billing)
Can you change your design? I think that would be easier. For example since billing is a field on Transaction, you could query http://address/billings/transactions?billing_id={billing_id}
Attempting to get a ViewSet to support detail operation when the instance doesn't exist seems like a difficult thing to do. It would be better to approach it from another way that allows for the billing to be nullable from the start.
If that's not an option you need to stop using get_object_or_404 if it's possible that the id passed in the url doesn't exist. You also should change the TransactionSerializer as such:
class TransactionSerializer(serializers.ModelSerializer):
billing = serializers.PrimaryKeyRelatedField(
queryset=Billing.objects.all(), allow_null=True, many=False)
...
I want to use the perform an update or create in django-rest-framework, by passing or not the id field. I've got this model
class Etiqueta(models.Model):
name_tag = models.CharField(max_length=200, blank=False, null=False)
description_tag = models.TextField(max_length=500, blank=False, null=False)
def __unicode__(self):
return self.name_tag
And in django-rest-framework I've got this serializer
from myapp.modulos.estado_1.models import Etiqueta
from rest_framework import serializers, viewsets
# Serializers define the API representation.
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Etiqueta
fields = (
'id',
'name_tag',
'description_tag'
)
# ViewSets define the view behavior.
class TagViewSet(viewsets.ModelViewSet):
queryset = Etiqueta.objects.all()
serializer_class = TagSerializer
Normally when I create an object, I perform a POST to the URL without the /:id, but if I've got an object with a local id, I want him to be created in the REST with the same id (remote id), django overwrite my local id and creates a new one. Does anybody know how achieve this? Also it is important to mention that I'm working with google-app-engine, google-cloud-datastore and django-dbindexer.
This code should work for your case -
class TagViewSet(viewsets.ModelViewSet):
queryset = Etiqueta.objects.all()
serializer_class = TagSerializer
def get_object(self):
if self.request.method == 'PUT':
obj, created = Etiquetta.objects.get_or_create(pk=self.kwargs.get('pk'))
return obj
else:
return super(TagViewSet, self).get_object()
You should have a look at how Django REST framework does currently and adapts your create method to update whenever you have an id field.
The original ViewSet.create is here and the ViewSet.update is here.
Please note that you will probably end up with two different serializers for /tag/ and /tag/:id since the later should not allow the id field to be writable while the former should.
I've write a drf views mixin for updating an object by id, if no corresponding object, just create it then update.
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").
In my app I have the following models:
class Zone(models.Model):
name = models.SlugField()
class ZonePermission(models.Model):
zone = models.ForeignKey('Zone')
user = models.ForeignKey(User)
is_administrator = models.BooleanField()
is_active = models.BooleanField()
I am using Django REST framework to create a resource that returns zone details plus a nested resource showing the authenticated user's permissions for that zone. The output should be something like this:
{
"name": "test",
"current_user_zone_permission": {
"is_administrator": true,
"is_active": true
}
}
I've created serializers like so:
class ZonePermissionSerializer(serializers.ModelSerializer):
class Meta:
model = ZonePermission
fields = ('is_administrator', 'is_active')
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer(source='zonepermission_set')
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
The problem with this is that when I request a particular zone, the nested resource returns the ZonePermission records for all the users with permissions for that zone. Is there any way of applying a filter on request.user to the nested resource?
BTW I don't want to use a HyperlinkedIdentityField for this (to minimise http requests).
Solution
This is the solution I implemented based on the answer below. I added the following code to my serializer class:
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.get(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission)
return serializer.data
Thanks very much for the solution!
I'm faced with the same scenario. The best solution that I've found is to use a SerializerMethodField and have that method query and return the desired values. You can have access to request.user in that method through self.context['request'].user.
Still, this seems like a bit of a hack. I'm fairly new to DRF, so maybe someone with more experience can chime in.
You have to use filter instead of get, otherwise if multiple record return you will get Exception.
current_user_zone_permission = serializers.SerializerMethodField('get_user_zone_permission')
def get_user_zone_permission(self, obj):
user = self.context['request'].user
zone_permission = ZonePermission.objects.filter(zone=obj, user=user)
serializer = ZonePermissionSerializer(zone_permission,many=True)
return serializer.data
Now you can subclass the ListSerializer, using the method I described here: https://stackoverflow.com/a/28354281/3246023
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
If you're using the QuerySet / filter in multiple places, you could use a getter function on your model, and then even drop the 'source' kwarg for the Serializer / Field. DRF automatically calls functions/callables if it finds them when using it's get_attribute function.
class Zone(models.Model):
name = models.SlugField()
def current_user_zone_permission(self):
return ZonePermission.objects.get(zone=self, user=user)
I like this method because it keeps your API consistent under the hood with the api over HTTP.
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
current_user_zone_permission = ZonePermissionSerializer()
class Meta:
model = Zone
fields = ('name', 'current_user_zone_permission')
Hopefully this helps some people!
Note: The names don't need to match, you can still use the source kwarg if you need/want to.
Edit: I just realised that the function on the model doesn't have access to the user or the request. So perhaps a custom model field / ListSerializer would be more suited to this task.
I would do it in one of two ways.
1) Either do it through prefetch in your view:
serializer = ZoneSerializer(Zone.objects.prefetch_related(
Prefetch('zone_permission_set',
queryset=ZonePermission.objects.filter(user=request.user),
to_attr='current_user_zone_permission'))
.get(id=pk))
2) Or do it though the .to_representation:
class ZoneSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Zone
fields = ('name',)
def to_representation(self, obj):
data = super(ZoneSerializer, self).to_representation(obj)
data['current_user_zone_permission'] = ZonePermissionSerializer(ZonePermission.objects.filter(zone=obj, user=self.context['request'].user)).data
return data