How do I pass 'user_id' to CustomerSerializer? - python

I don't know how to pass user_id from requests.user.id and use it in the CustomerSerializer to save the object to the database. The error stems from the fact that user_id exists in the customer table in the database but it does not show up as a field to be passed in the rest_framework API frontend (only phone and profile_image do).
Here is the Customer model:
class Customer(models.Model):
phone = models.CharField(max_length=14)
profile_image = models.ImageField(blank=True, null=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Here is the ViewSet:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
#action(detail=False, methods=['GET', 'PUT'])
def me(self, request):
customer, created = Customer.objects.get_or_create(user_id=request.user.id)
if request.method == 'GET':
serializer = CustomerSerializer(customer)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = CustomerSerializer(customer, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
... and here is the Serializer:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
```python
... and when I try to save a new customer by sending a POST request to the endpoint with the following data:
```json
{
"profile_image": "Images/image.png",
"phone": "009293930"
}
I get the following error:
IntegrityError at /api/customers/
(1048, "Column 'user_id' cannot be null")
Request Method: POST
Request URL: http://127.0.0.1:8000/api/customers/
Django Version: 4.0.6
Exception Type: IntegrityError
Exception Value:
(1048, "Column 'user_id' cannot be null")
Exception Location: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages/pymysql/err.py, line 143, in raise_mysql_exception
Python Executable: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/bin/python
Python Version: 3.8.10
Python Path:
['/home/caleb/Desktop/Cribr',
'/home/caleb/Desktop/Cribr',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_display',
'/usr/lib/python38.zip',
'/usr/lib/python3.8',
'/usr/lib/python3.8/lib-dynload',
'/home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_matplotlib_backend']
Server time: Thu, 28 Jul 2022 23:38:53 +0000
I figured the issue here is that the serializer class is not getting the user_id value from the POST request. I tried passing request.user.id to the serializer from the viewset through a context object (i.e., context={'user_id': request.user.id}) but I couldn't figure out how to then add it to the validated data which the serializer passes to the save method.
Any help on this issue will be much appreciated. Thanks in advance.

The benefit of using DRF and viewsets is that most of the work has already been done for you. In instances such as this, you usually just need to tweak a few things to get it working the way you want. I've re-written your solution for you below:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
def get_object(self):
customer, created = Customer.objects.get_or_create(user_id=self.request.user.id)
return customer
#action(detail=False, methods=['GET'])
def me(self, request):
return self.retrieve(request)
def create(self, request, *args, **kwargs):
customer_exists = Customer.objects.filter(user=request.user).exists()
if customer_exists:
return self.update(request, *args, **kwargs)
else:
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
From the DRF docs:
The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
The actions provided by the ModelViewSet class are .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy().
The ModelViewSet will also set up a urlconf for you, which, excluding list will expect an object pk (primary key) to be provided in the url to allow the view to know what resource in the database you are trying to access. In your case, you want to determine that resource based on the authentication credentials provided in the request. To do this, we can override get_object to get or create the customer based on the authenticated user's id.
The next change we make is to define our action for the GET method. We want to be able to retrieve a resource, without specifying the pk in the url conf, hence detail=False. We can then simply call the builtin retrieve function from this action, which in turn will use get_object to get and return the customer object.
Thirdly, your PUT request will be directed to update, which is inherited from ModelViewSet, so you don't need to do anything here as you've already overwritten get_object.
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
Lastly, you want to set user on your serializer to read only (or just remove it entirely), as this should always be set based on the credentials passed in the request.
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
A great resource for looking at all the functions that you inherit from DRF classes is https://www.cdrf.co/.
Good luck, hope this helps!

Okay, I managed to solve it by overriding the create method in the serializer. I added the following:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
# NEW ---------------------------------
def create(self, validated_data):
user = self.context['request'].user
customer = Customer.objects.filter(user_id=user)
if customer.exists():
raise serializers.ValidationError(
'Customer already exists')
else:
customer = Customer.objects.create(
user=user, **validated_data)
return customer
The object saves fine now.

Related

Provide request data from view into serializer ModelViewSet Django

I try to make a custom list function inside ProductViewSet because I need to download an extra field - the highest product price in database. How can I provide the request argument from def list into serializer? I mean right now I get error 'NoneType' object has no attribute 'user' in this line: if request.user.is_authenticated.
So how can I fix it that he can read self.context.get('request') correctly?
class ProductViewSet(viewsets.ModelViewSet):
...
def list(self, request):
queryset = Product.objects.all()
serializer = ProductSerializer(queryset, many=True)
return Response(serializer.data)
class ProductSerializer(serializers.ModelSerializer):
...
class Meta:
model = Product
...
def get_is_followed(self, obj):
request = self.context.get('request')
if request.user.is_authenticated:
return Product.objects.filter(id=obj.id, followers=request.user.id).exists()
I want to work it like a default ModelViewSet list but with an extra field.
You have used ModelViewSet which already has a serializer_class attribute. You can simply provide the serializer_class and the serializer context is taken care by DRF itself. So, instead of writing
serializer = ProductSerializer(queryset, many=True) you should write in this way:
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
If your concern is only the request object as a context to the serializer then there is no need to override the list method in the ProductViewSet. By default three contexts (request, format and view) are passed to the serializer but if you need extra contexts then you can always override def get_serializer_context(self) method in the view.
This is the default signature of the get_serializer_context:
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}

Django Swagger won't allow me to use POST method (no parameters shown)

I'm using djangorestframework together with drf-spectacular modules for a Django project, and I'm trying to build some basic API methods for my Project model. Its structure looks like this:
from django.db import models
# Create your models here.
class Project(models.Model):
title = models.CharField(max_length = 128)
description = models.TextField()
image = models.URLField()
date = models.DateTimeField(auto_now_add=True)
I also have a serializer for the model, looking like this:
from rest_framework import serializers
from api.models.Project import Project
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ['title', 'description', 'image', 'date']
Then, in views.py, I created two functions: project_list_view, which either lets you to GET all the Project objects from the database, or lets you POST a new object. And finally, project_detail_view, which lets you GET a Project object by typing in its pk (integer id). These are my two functions:
#api_view(['GET', 'POST'])
def project_list_view(request):
if request.method == 'GET':
projects = Project.objects.all()
serializer = ProjectSerializer(projects, many=True)
return Response(serializer.data)
elif request.method == "POST":
serializer = ProjectSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#api_view(['GET'])
def project_detail_view(request, pk):
if request.method == "GET":
try:
project = Project.objects.get(pk = pk)
serializer = ProjectSerializer(project, many = False)
return Response(serializer.data, status = status.HTTP_200_OK)
except:
return Response(status=status.HTTP_404_NOT_FOUND)
The GET from project_list_view and project_detail_view work, but my problem lays in the POST method.
My Swagger is set to display its API Schema when accessing http://127.0.0.1:8000/docs/, and as I said, GET methods work properly, but when I'm trying to click on "Try it out" at the POST method, the fields are not displayed. I can only press "Execute" without actually being able to complete anything. After I click on "Execute", Swagger returns a 404 Bad Request response.
This is how POST looks like in Swagger:
My question is: Why won't Swagger display fields for each parameter of the model? Thank you.
Swagger Grabs the fields from a serializer_class variable.
I really recommend you change the format to Class-Based Views.
Something using mixins or generic class.
Your view could be like
class ProjectView(mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
permission_classes = [permissions.IsAuthenticated, ]
serializer_class = ProjectSerializer
queryset = Project.objects.all()
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
More on Mixins and Generic Views

Understanding Django Rest Framework's ModelViewSet Router

I have my Comment model:
class Comment(models.Model):
"""A comment is a content shared by a user in some post."""
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=False)
post = models.ForeignKey('posts.Post', on_delete=models.CASCADE, null=False)
content = models.TextField(max_length=1000, null=False, blank=False)
def __str__(self):
"""Return the comment str."""
return "'{}'".format(self.content)
Its serializer:
class CommentSerializer(serializers.ModelSerializer):
"""Comment model serializer."""
user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Comment
fields = '__all__'
def create(self, validated_data):
"""Create a new comment in some post, by request.user."""
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
def list(self, request):
"""List all the comments from some post."""
if 'post' not in request.query_params:
raise ValidationError('Post id must be provided.')
q = self.queryset.filter(post=request.query_params['post'])
serializer = CommentSerializer(q, many=True)
return Response(serializer.data)
The viewset:
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
queryset = Comment.objects.all()
def get_permissions(self):
permissions = []
if self.action == 'create' or self.action == 'destroy':
permissions.append(IsAuthenticated)
return [p() for p in permissions]
def get_object(self):
"""Return comment by primary key."""
return get_object_or_404(Comment, id=self.kwargs['pk']) # this is the drf's get_object_or_404 function
def destroy(self, request, *args, **kwargs):
"""Delete a comment created by request.user from a post."""
pdb.set_trace()
instance = self.get_object()
if instance.user != request.user:
raise ValidationError('Comment does not belong to the authenticated user.')
self.perform_destroy(instance)
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
So far so good when it comes to list, create and retrieve. But when it comes to delete/destroy (Idk the difference) I don't know how to get the URL for the DELETE request.
I'm using Postman to do so.
Urls.py:
router = routers.SimpleRouter()
router.register(r'comments', CommentViewSet, basename='comments')
pdb.set_trace() # Put a pdb here to see what does router.urls have.
urlpatterns = [
# Non api/simple django views
path('create_comment/', create_comment, name='create_comment'),
path('delete_comment/', delete_comment, name='delete_comment'),
# rest api views
path('rest/', include(router.urls))
]
When I debug the router.urls, the terminal shows this:
[<URLPattern '^comments/$' [name='comments-list']>, <URLPattern '^comments/(?P<pk>[^/.]+)/$' [name='comments-detail']>]
Why there is no url for create and destroy?
I could create some Comments from the api, and I didn't even know how did I get the POST request url for the create function, I just guessed lol, but that's not what you want when you're programming right?
I have checked the Routers documentation but I don't get it. Please give some help! Many thanks.
The answer is simple, Django uses the comments-list list for create and list operation and comments-detail for update and delete operations.
That is, there are only two URL end-points, but it supports several actions, which can be performed by changing the HTTP methods
You can use HTTP GET foo-bar/comments/ to retrieve all comments where as HTTP POST foo-bar/comments/ can be used to create a new comment.

How to update user information using Django Rest Framework?

I am trying to implement authentication by combining Django Rest Framework and Angular, but I am suffering from user information update.
Angular sends it to Django with the PUT method, Django accepts the request with View "AuthInfoUpdateView".
class AuthInfoUpdateView(generics.GenericAPIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = AccountSerializer
lookup_field = 'email'
queryset = Account.objects.all()
def put(self, request, *args, **kwargs):
serializer = AccountSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
At this time, Django accepts the request as below.
request.data = {'email': 'test3#example.com', 'username': 'test3', 'profile': 'i am test3'}
request.user = test3#example.com
And the serializer is implementing as below.
from django.contrib.auth import update_session_auth_hash
from rest_framework import serializers
from .models import Account, AccountManager
class AccountSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False)
class Meta:
model = Account
fields = ('id', 'username', 'email', 'profile', 'password')
def create(self, validated_data):
return Account.objects.create_user(request_data=validated_data)
def update(self, instance, validated_data):
insntance.username = validated_data.get('username', instance.username)
insntance.email = validated_data.get('email', instance.email)
insntance.profile = validated_data.get('profile', instance.profile)
instance = super().update(instance, validated_data)
return instance
I tried to update the user from Angular in such an implementation, and the following response is returned.
"{"username":["account with this username already exists."],"email":["account with this email address already exists."]}"
It is thought that it is because you did not specify the record to update, but is there a way to solve it smartly without changing the current configuration so much?
I need your help.
use
class AuthInfoUpdateView(generics.UpdateAPIView):
use http method patch can partial_update your instance.
method PATCH -> partial update instance
method PUT -> update instance

django-rest-framework deserialize request to create a object with user foreign key

I'd like to create a SpiderService with a foreign key request.user.
models:
from django.contrib.auth.models import User
class SpiderService(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
serializers:
class SpiderServiceSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.id')
class Meta:
model = SpiderService
fields = ('id', 'user', 'name')
class SpiderServiceListSerializer(viewsets.ModelViewSet):
queryset = SpiderService.objects.all()
serializer_class = SpiderService
views:
class SpiderServiceList(APIView):
def get(self, request, format=None):
services = SpiderService.objects.all()
serializer = SpiderServiceSerializer(services, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SpiderServiceSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def perform_create(elf, serializer):
serializer.save(user=self.request.user)
when I post
{
"name": "2"
}
to the api url, got 500 error:
[01/Feb/2016 15:39:55] "POST /services/ HTTP/1.1" 500 17105
IntegrityError: NOT NULL constraint failed: spmanage_spiderservice.user_id
I have read Serializer relations and found I should not write code as above. The official example is equal to remove user field in SpiderServiceSerializer, and add a spiderservice field to a UserSerializer.Seems aim to achieve the syntax like user.spiderservice_set in django models, which do not fit my purpose.
I just want to create a spiderservice object with request.user.
How to solve it?
You need to add the current user as an extra parameter to the Serializer's save method:
serializer.save(user=request.user)
do you have any Account serializer for the user model ??
if so, in your SpiderServiceSerializer try this :
user = AccountSerializer(read_only=True, required=False)

Categories