I'd like to post to my Django server using post so I can add a todo item. Here is the model:
class Todo(models.Model):
title = models.CharField(max_length=200);
text = models.TextField()
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(default=datetime.now, blank = True )
def __str__(self):
return self.title
And serializers:
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ("id", 'title','text', 'completed', 'created_at')
And view:
class TodoList(APIView):
def get(self,request):
todo=Todo.objects.all()
serializer=TodoSerializer(todo,many=True)
return Response(serializer.data)
def post(self,request):
Todo.objects.create(
title=request.POST.get('title'),
text=request.POST.get('text'))
return HttpResponse(status=201)
My post request is
{ "title": "new title",
"text": "a test text"}
And it told me
IntegrityError at /todos/
(1048, "Column 'title' cannot be null")
As a newbie at Django, I don't understand this error. Any ideas?
You need to access request.data instead of request.POST,
def post(self,request):
serializer = TodoSerializer(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)
Since you've asked about other methods besides post in the comments, I'll show an example of a ModelViewSet that will allow you to post to add a Todo, as well as provide support for retrieving, updating, and deleting your Todo's.
Recommended reading:
http://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
from rest_framework.viewsets import ModelViewSet
from todo.models import Todo
from todo.serializers import TodoSerializer
class TodoViewSet(ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
The ModelViewSet class will provide you with a default implementation of view methods to list, create, retrieve, update (whole or partial update), and delete Todo's. These actions are mapped to certain methods for different urls, get is mapped to list and retrieve, post is mapped to create, put and patch are mapped to update and partial_update, and delete is mapped to destroy.
Then in your urls.py, include the TodoViewSet using TodoViewSet.as_view(...):
from django.conf.urls import url
from todo.views import TodoViewSet
urlpatterns = [
url(
r'^todos/$',
TodoViewSet.as_view({'get': 'list', 'post': 'create'}),
name='todo-list',
),
url(
r'^todos/(?P<pk>\d+)/$',
TodoViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}),
name='todo-detail',
)
]
Here we are explicitly stating the mapping of request methods to view actions that I mentioned before.
Instead of creating like this, You can always use serializers for the same
data_serializer = TodoSerializer(data=request.data)
if data_Serializer.is_valid():
data_Serializer.save()
for put request :
todo_item = Todo.objects.get(id=id) // Need to get that element
data_serializer = TodoSerializer(instance=todo_item,data=request.data, partial=True)
if data_Serializer.is_valid():
data_Serializer.save()
else:
print data_Serializer.errors
for delete:
todo_item = Todo.objects.get(id=id) // Need to get that element
todo_item.delete()
Related
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.
I was remaking a social media site as a revision of Django and the rest framework, I didn't want to use the django default linear id count and didn't like how long the uuid library's ids was, so I used the shortuuid library. I've used them on the posts and the comments just to keep the anonymity of the count of both posts and comments. On the posts side everything works for the CRUD stuff (which should be proof that the issue isn't from the shortuuid library, as far as I know), although with the comments the Create Retrieve works perfectly but the Update Destroy doesn't. so here is the code we are working with:
starting with the models to know what kind of data we are working with (models.py):
from shortuuid.django_fields import ShortUUIDField
... # posts likes etc
class Comment(models.Model):
id = ShortUUIDField(primary_key=True, length=8, max_length=10)
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
body = models.TextField(max_length=350)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
class Meta:
ordering = ['created']
def __str__(self):
return f'on {self.post} by {self.user}'
objects = models.Manager()
serializers.py:
class CommentSerializer(ModelSerializer):
username = SerializerMethodField()
def get_username(self, comment):
return str(comment.user)
class Meta:
model = Comment
fields = ['id', 'user', 'post', 'username', 'body', 'created', 'updated']
read_only_fields = ['id', 'post', 'user', 'username']
now with the routing (urls.py):
from django.urls import path
from .views import *
urlpatterns = [
...
path('<str:pk>/comments/' , Comments),
path('<str:pk>/comments/create/', CreateComment),
path('<str:pk>/comments/<str:cm>/', ModifyComment),
# pk = post ID
# cm = comment ID
]
views.py:
class ModifyComment(generics.RetrieveUpdateDestroyAPIView):
serializer_class = CommentSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
post = Post.objects.get(pk=self.kwargs['pk'])
comment = Comment.objects.get(post=post, pk=self.kwargs['cm'])
return comment
def perform_update(self, serializer):
print(Post.objects.all())
post = Post.objects.get(pk=self.kwargs['pk'])
comment = Comment.objects.filter(pk=self.kwargs['cm'], post=post)
if self.request.user != comment.user:
raise ValidationError('you can\'t edit another user\'s post')
if comment.exists():
serializer.save(user=self.request.user, comment=comment)
else:
raise ValidationError('the comment doesnt exist lol')
def delete(self, request, *args, **kwargs):
comment = Comment.objects.filter(user=self.request.user, pk=self.kwargs['cm'])
if comment.exists():
return self.destroy(request, *args, **kwargs)
else:
raise ValidationError("you can\'t delete another user\'s post")
ModifyComment = ModifyComment.as_view()
and the response to going to the url '<str:pk>/comments/<str:cm>/' comment of some post we get this:
side note, the perform_update function doesn't seem to be called ever, even putting a print statement at the beginning of the function doesn't get printed so the issue may have to do with the get_queryset even though I've tried using the normal queryset=Comment.object.all() and making the get_queryset function return the comment with the correct params but I couldn't make it work
For individual objects you need to overwrite the get_object method.
You are performing the request GET /str:pk/comments/str:cm/, this calls the retrieve method on the view, which in turn calls get_object. The default behaviour is trying to find a Comment object with id equal to pk since it's the first argument, since you need to filter through a different model you need to overwrite it.
classy drf is a good website for seing how the internals of the clases work.
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
I need to update my user in REST framework
views.py
class UserUpdate(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self,request):
user=User.objects.get(id=request.user.id)
try:
user_serializer=UserSerializer(request.user,data=request.data, partial=True)
if user_serializer.is_valid():
user_serializer.save()
return Response(user_serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(user_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except:
return JsonResponse({'status':0,'message':'Error on user update'})
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'first_name', 'last_name', 'avatar']
models.py
class User(AbstractUser):
fb_userid = models.CharField(max_length=256)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
response:
DETAIL: Key (username)=() already exists.
def post(self,request):
user_serializer=UserSerializer(request.user, data=request.data, partial=True)
if user_serializer.is_valid():
user_serializer.save()
return Response(user_serializer.data, status=status.HTTP_200_OK)
else:
return Response(user_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#YKH is right this code is good there may error in your POST data.
In your image, you are passing two parameters in Header. It could possible you are passing the wrong header.
Content-Type should not be for form-data
I found this post where someone has a similar problem as you: Django Rest Framework unable to parse multipart/form data
It seems on your picture that you are putting something into Headers tab. Postman is taking care of that for you, so you shouldn't define anything there. Could you try again without setting anything in the headers?
I have a simple subclass of viewsets.ViewSet which looks like:
from rest_framework import viewsets
from rest_framework.response import Response
from ..models import Entry, Sitting, Source, Venue
from .serializers import (
SittingSerializer, SittingWithEntriesSerializer,
)
class SittingViewSet(viewsets.ViewSet):
def list(self, request, version=None):
queryset = Sitting.objects.order_by('id')
serializer = SittingSerializer(
queryset, many=True, context={'request': request}
)
return Response(serializer.data)
def retrieve(self, request, pk=None, version=None):
prefetch_qs = Entry.objects.select_related('speaker')
queryset = Sitting.objects.order_by('id') \
.prefetch_related(Prefetch('entry_set', queryset=prefetch_qs))
sitting = get_object_or_404(queryset, pk=pk)
serializer = SittingWithEntriesSerializer(
sitting, context={'request': request}
)
return Response(serializer.data)
However, the list view isn't paginated, as it is if you use a subclass of ModelViewSet. The settings I'm using are:
# Django Rest Framework settings:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('pombola.api.permissions.ReadOnly',),
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
'PAGE_SIZE': 10,
}
The documentation suggests looking at the source code for the mixins.ListModelMixin and generics.GenericAPIView classes, but I can't easily see how to reapply what they do to paginate results to these ViewSet methods.
Could anyone suggest what the simplest way would be to change this example to get pagination for the list view?
You overrided the list method, so it doesnt paginate your data.
If you check ListModelMixins I think this might be your answer:
class SittingViewSet(
viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.RetrieveModelMixin):
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(
Sitting.objects.order_by('id')
)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = SittingSerializer(
page,
many=True,
context={'request': request}
)
return self.get_paginated_response(serializer.data)
serializer = SittingSerializer(
queryset,
many=True,
context={'request': request}
)
return Response(serializer.data)
Simple solution. Let's use the auth model as an example.
from django.contrib.auth.models import User
from rest_framework.pagination import PageNumberPagination
from rest_framework import viewsets
from .serializer import UserSerializer
class UserViewSet(viewsets.ViewSet):
def list(self, request):
queryset = User.objects.all()
pagination = PageNumberPagination()
qs = pagination.paginate_queryset(queryset, request)
serializer = UserSerializer(qs, many=True)
return pagination.get_paginated_response(serializer.data)
Although this comes late as an answer, I wrote a Q&A style example for Django Rest Framework which enables non-generic viewsets to have pagination.
By default, only the viewsets.GenericViewSet has automatic pagination (if you enable pagination in your settings of course), because it inherits from generics.GenericAPIView.
That leaves you with 2 options:
The easy way:
mixins.ListModelMixin provides pagination, so you can simply declare your viewset as follows:
class SittingViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
and you now have a list() method with pagination.
The harder way: *
You can follow the example given above and create a pagination method inside your viewset.
The list() code must be derived from the source code of the mixins.ListModelMixin provided above.
The paginator(), paginate_queryset() and get_paginated_response() methods can be copied from the Documentation or the source code of generics.GenericApiView
I didn't add code example for the second option, because it would be just a copy/paste of the above mentioned links.
I have made a related answers in this post: Pagination not working in DRF APIView
**Pagination in DRF using APIView and if you don't have serializer class **
Here I have used the django paginator, it is very simple
In view
from rest_framework.views import APIView
class SittingView(APIView):
"""This is a API code so when at e.g. (url /sitting-record/?page=2) is entered it will show 10 records for page 2 and if page is empty it will show empty"""
def get(self, request, format=None):
"""Here 10 is the page size, that means 10 record per page"""
sitting = Sitting.objects.all().order_by('id')
sitting_paginator = Paginator(sitting, 10)
"""this gets the page page from url and if not given it set default page is 1"""
page = request.GET.get('page')
page = 1 if not page else page
try:
paginated_setting = sitting_paginator.page(page)
except Exception as e:
paginated_setting = []
"""you can retrieve or iterate the records fields from paginator_setting and then send the data in response"""
data = {
"setting records" : paginated_setting,
}
return Response({
"status" : status.HTTP_200_OK,
"messgae" : "Sitting content",
"data" : data
})
Pagination in DRF using viewsets and list
Here I have handled a exception If page is empty it will show empty records
In setting define the page size, this page size is global and it is used by paginator_queryset in view
REST_FRAMEWORK = {
'PAGE_SIZE': 10,
}
In view
from rest_framework import mixins, viewsets
class SittingViewSet(viewsets.GenericViewSet,
mixins.ListModelMixin):
serializer_class = SittingSerializer
queryset = Sitting.objects.all()
serializer = serializer_class(queryset, many=True)
def list(self, request, *args, **kwargs):
queryset =self.filter_queryset(Sitting.objects.all().order_by('id'))
page = request.GET.get('page')
try:
page = self.paginate_queryset(queryset)
except Exception as e:
page = []
data = page
return Response({
"status": status.HTTP_404_NOT_FOUND,
"message": 'No more record.',
"data" : data
})
if page is not None:
serializer = self.get_serializer(page, many=True)
data = serializer.data
return self.get_paginated_response(data)
# serializer = self.get_serializer(queryset, many=True)
return Response({
"status": status.HTTP_200_OK,
"message": 'Sitting records.',
"data" : data
})
**> Note: If you not use Order_by it will show exception because this list
gives unordered list.**