ModelViewSet: "this field is required" when POST, field is FK - python

This is my current code
Models:
class Author(models.Model):
a_author= models.CharField( primary_key=True, unique=True, db_column='author_id')
a_name = models.CharField(db_column='author_name')
class Book(models.Model):
b_book = models.CharField( primary_key=True, unique=True, db_column='book_id')
b_name = models.CharField(db_column='book_name')
b_author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
db_column='book_author_name')
Serializers
class AuthorSerializer(serializers.ModelSerializer):
author = serializers.CharField(source='a_author')
name = serializers.CharField(source='a_name')
class Meta:
fields = ('author', 'name')
class BookSerializer(serializers.ModelSerializer):
book = serializers.CharField(source='b_book')
name = serializers.CharField(source='b_name')
author = serializers.CharField(source='b_author')
class Meta:
fields = ('book', 'name', 'author')
def create(self, validated_data):
author_id = validated_data.pop('author')
author = models.Author.objects.filter(a_author=author_id).first()
validated_data['b_author'] = author
return models.Book.objects.create(**validated_data)
Views
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
def get_queryset(self):
queryset = models.Book.objects
author = self.kwargs.get('author')
queryset = queryset.filter(b_author=author)
return queryset
Url
urlpatterns = [
path('library/<str:author_id>/', BookViewSet.as_view({'get': 'list', 'post':'create'}))
]
Currently if I post to /library/123abc/ with params: { 'name': 'test', 'author': '123abc' }, it will work - a Book record with name=test, author=123abc will be created.
But now I want to take author out of the params (since url already has athor id so I don't want to duplicate it again in params) and send just {'name': 'test'}, it will return 400 error with message {'author': 'this field is required'}. I have tried author = serializers.CharField(source='b_author', required=False) but it didn't work.
Is there any way to get around this? I wonder if there is any way to include additional value in django before params value are validated...

Since you want the author to be shown but not written, you need to set that field as read_only:
class BookSerializer(serializers.ModelSerializer):
book = serializers.CharField(source='b_book')
name = serializers.CharField(source='b_name')
author = serializers.CharField(source='b_author', read_only=True)
Next, you'll want the author to be saved when creating a Book.
For that, you'll have to explicitly provide it to the serializer:
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
def get_queryset(self):
queryset = models.Book.objects
author = self.kwargs.get('author')
queryset = queryset.filter(b_author=author)
return queryset
def perform_create(self, serializer):
author = get_object_or_404(Author, pk=self.kwargs.get('author'))
serializer.save(author= author)

Related

Django: NOT NULL constraint failed: appname_post.user_id

I have the following django rest framework serializer and view for a model Post, and the models defined for the app are as following.
Now I wanted to test the API, so tried to "create" a new post from the API page, but then I got an error IntegrityError at /api/posts/ NOT NULL constraint failed: appname_post.user_id.
So then I tried to debug and checked its request.data value, it says it only has title and content but not user_id, as I could expect from the error above.
But now I have no idea how to tell the API automatically relate the user_id field to request.user.id. Isn't not enough to just add the user_id = serializers.ReadOnlyField(source='user.id') line? I could do "retrieve", "update", "patch" etc but just cannot "post" (create).
Thanks for your help.
serializer and view
class DevPerm(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return True
def has_permission(self, request, view):
return True
class PostSerializer(serializers.HyperlinkedModelSerializer):
user_id = serializers.ReadOnlyField(source='user.id')
class Meta:
model = Post
fields = ('url',
'id',
'title',
'content',
'created_at',
'voters',
'comments',
'user_id',
)
read_only_fields = (
'id',
'created_at',
"voters",
"comments",
# "user_id",
)
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated, DevPerm,]
models
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts', default="")
created_at = models.DateTimeField(auto_now_add=True)
title = models.TextField(blank=True)
content = models.TextField(blank=True)
voters = models.ManyToManyField(User, related_name='votes', default="")
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='comments', default="")
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments', default="")
created_at = models.DateTimeField(auto_now_add=True)
content = models.TextField(blank=True)
You can override perform_create method inside you ModelViewSet.
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated, DevPerm,]
def perform_create(self, serializer):
serializer.save(user=self.request.user)

How to access atribute of instance I want to validate from inside serializer?

In my view I do following:
class ReviewViewSet(viewsets.ModelViewSet):
#queryset, serializer_class and permission_classes defined here
def perform_create(self, serializer):
title_id = self.kwargs.get('title_id')
title = get_object_or_404(Title, pk=title_id)
serializer.save(author=self.request.user, title=title)
I want to validate that review doesn't exist yet. I'm trying to do this in serializer's validate():
class ReviewSerializer(serializers.ModelSerializer):
title = serializers.SlugRelatedField(slug_field='pk', read_only='True')
author = serializers.SlugRelatedField(slug_field='username', read_only='True')
def validate(self, data):
title = # <-- How to get title here?
author = self.context['request'].user
queryset = Review.objects.all().filter(title=title, author=author)
if queryset.exists():
raise serializers.ValidationError('Review alredy exists')
return(data)
class Meta:
fields = '__all__'
model = Review
Attempt to do title = self.title raises AttributeError: 'ReviewSerializer' object has no attribute 'title'
How to access title from inside validate() in my case?
Here's my Review model:
class Review(models.Model):
class Meta:
ordering = ['-pub_date']
unique_together = ['title', 'author']
title = models.ForeignKey(
Title,
on_delete=models.CASCADE,
related_name='reviews',
)
author = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='reviews',
)
text = models.TextField('Review text')
score = models.PositiveSmallIntegerField(
'Review score',
validators=[
MinValueValidator(1),
MaxValueValidator(10)
]
)
pub_date = models.DateTimeField(
'Date and time of review',
auto_now_add=True,
db_index=True
)
The response to your question lies in the documentation : https://www.django-rest-framework.org/api-guide/serializers/#object-level-validation
About the validate method:
This method takes a single argument, which is a dictionary of field values
If you look at the code sample in the doc, you'll see that all data that you might need to validate are in the data argument of the validate method.
Hence, the title is in data['title'].

Python Django saved posts computed method

Assuming im using the default django model, a Post model (code below) and a SavedPost model that links a User to a Post (if the certain user with the certain post exists then that post is saved for that user) and a Follower model that links 2 user (similar to SavedPost).
What im trying to do: An API that for a user, they get all posts for the users they follow, in addition each of these posts has an extra 'field' to say if that post is saved or not.
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post_type = models.CharField(max_length=1, choices=[('B', 'Blog'), ('V', 'Video')], default='B')
file_path = models.URLField(null=True)
title = models.CharField(max_length=255)
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SavedPost(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# A user can save a post only once.
unique_together = ('user', 'post')
class Follower(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user")
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name="follower")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# A user can follow another user only once
unique_together = ('user', 'follower')
Post serilializer:
class PostSerializer(serializers.ModelSerializer):
"""
Nested serializer for post using SimpleUser and Kingdom.
"""
class Meta:
model = Post
fields = ('id', 'user', 'post_type', 'file_path',
'title', 'description', 'created_at', 'updated_at')
def to_representation(self, instance):
data = super().to_representation(instance)
data['user'] = UserSerializer(
User.objects.get(pk=data['user'])).data
return data
API View:
#permission_classes([IsAuthenticated,])
#api_view(['GET'])
def get_following(request):
user = request.user
following = Follower.objects.filter(follower=user).values('user')
# saved_posts = SavedPost.objects.filter(user=user, post__user__in=following).order_by('-post__created_at')
posts = Post.objects.filter(user__in=following).order_by('-created_at')
serializer = PostSerializer(posts, many=True, context={'request': request})
return JsonResponse(serializer.data, safe=False)
So far with the view I made I can get all the posts that the request.user follows but it doesnt say if they are saved or not. I am looking for say 'is_saved' boolean on post to say if that post is saved for that user or not.
Any help/method to do this appreciated. Thank you.
Use serializers.SerializerMethodField as
class PostSerializer(serializers.ModelSerializer):
is_saved = serializers.SerializerMethodField()
def get_is_saved(self, post_instance):
return SavedPost.objects.filter(user=post_instance.user, post=post_instance).exists()
class Meta:
model = Post
fields = ['id', 'user', 'post_type', 'file_path',
'title', 'description', 'created_at', 'updated_at', 'is_saved']
def to_representation(self, instance):
data = super().to_representation(instance)
data['user'] = UserSerializer(
User.objects.get(pk=data['user'])).data
return data
First of all, just to be clear, I will be defining the related_name option for the ForeignKeys in SavedPost - it's up to you to decide whether to implement this or not:
class SavedPost(models.Model):
user = models.ForeignKey(User, related_name="saved", on_delete=models.CASCADE)
post = models.ForeignKey(Post, related_name="saved", on_delete=models.CASCADE)
...
Now, in your PostSerializer, you could add this field (remember to add it to the fields variable in the Meta inner class - that is if you're using ModelSerializer):
class PostSerializer(serializers.ModelSerializer):
saved = SavedPostSerializer(many=True)
...
To finish it off, define your SavedPostSerializer - above PostSerializer, if in the same file/module:
class SavedPostSerializer(serializers.ModelSerializer):
class Meta:
model = SavedPost
fields = "__all__"
With this, your json should have a nested field with the saved key containing an array of SavedPosts, if there are any related to the Posts retrieved.

django rest framework how to add entries for multiple tables with one request?

I'm trying to add genres to the Genre model at the same time as adding a Movie to the movie model, however I get the following response object when trying to add a Genre entry that doesn't already exist (works fine if it exists in the table already):
{'genres': ['Object with genre=Mystery does not exist.']}
I thought it should work using the object.get_or_create() in the create() method of the MovieSerializer but it doesn't seem to work.
Also I'm sending data by POST request in the format:
{'tmdb_id': 14,
'title': 'some movie',
'release_date': '2011-12-12',
'genres': ['Action', 'Mystery']}
not sure if that matters.
Here's the code:
Views.py
class CreateMovieView(generics.ListCreateAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
def perform_create(self, serializer):
"""Save the post data when creating a new movie."""
serializer.save()
class MovieDetailsView(generics.RetrieveUpdateDestroyAPIView):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
Models.py
class Genre(models.Model):
genre = models.CharField(max_length=65)
def __str__(self):
return "{}".format(self.genre)
class Movie(models.Model):
tmdb_id = models.IntegerField(primary_key=True)
title = models.CharField(max_length=255)
release_date = models.DateField()
imdb_id = models.CharField(max_length=255, blank=True)
img_path = models.CharField(max_length=255, blank=True)
runtime = models.CharField(max_length=65, blank=True)
synopsis = models.TextField(blank=True)
imdb_rating = models.DecimalField(max_digits=3, decimal_places=1, blank=True, null=True)
metascore = models.IntegerField(blank=True, null=True)
genres = models.ManyToManyField(Genre, related_name='genres', blank=True)
def __str__(self):
return "{}".format(self.title)
serializers.py
class MovieSerializer(serializers.ModelSerializer):
"""Serializer to map the Model instance into JSON format."""
genres = serializers.SlugRelatedField(slug_field='genre', many=True, queryset=Genre.objects.all())
class Meta:
"""Meta class to map serializer's fields with the model fields."""
model = Movie
fields = ('tmdb_id',
'imdb_id',
'title',
'release_date',
'img_path',
'runtime',
'synopsis',
'imdb_rating',
'metascore',
'genres')
def create(self, validated_data):
genres_data = validated_data.pop('genres')
movie = Movie.objects.create(**validated_data)
for genre_name in genres_data:
genre, created = Genre.objects.get_or_create(genre=genre_name)
movie.genres.add(genre)
return movie
I'm not sure why this is the error you get, but seems like your'e not using the Views correctly to allow creation of Movie instances.
the ListCreateAPIView lets you create lists of movies
you should, instead, add a CreateModelMixin to your other view:
class MovieDetailsView(generics.RetrieveUpdateDestroyAPIView,
mixins.CreateModelMixin):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
or, even better, use the ModelViewSet:
from restframework import viewsets
class MovieViewSet(viewsets.ModelViewSet):
queryset = ...

Django REST Framework relationship serialization

I've been banging my head against this issue and know I have to be missing something simple.
I'm trying to learn Django REST Framework and having issues setting the foreign keys of a new object to existing other objects when POSTing JSON to the server.
models.py
class Genre(models.Model):
name = models.CharField(max_length=200)
class Author(models.Model):
first_name = models.CharField(max_length=200)
last_name = models.CharField(max_length=200)
def full_name(self):
return self.first_name + ' ' + self.last_name
class Book(models.Model):
title = models.CharField(max_length=200)
genre = models.ForeignKey(Genre)
isbn = models.CharField(max_length=15, default='')
summary = models.CharField(max_length=500, null=True)
author = models.ForeignKey(Author)
serializers.py
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ('id', 'first_name', 'last_name',)
class GenreSerializer(serializers.ModelSerializer):
class Meta:
model = Genre
fields = ('id', 'name',)
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('id','url', 'author', 'genre', 'title', 'isbn', 'summary',)
What I'm trying to is create a new book related to an existing Author and Genre. So, given some JSON like
{"title": "Title",
"author": {"id":1}
"genre" : {"id":2}
...
}
I want to create a new book and have its Genre and Author set to the appropriate entities that are already in the database.
I've tried to change the author and genre fields on BookSerializer to serializers.PrimaryKeyRelatedField() and scoured the docs and SO for answers, including this one. I've tried to change the fields in the JSON to "author": 1 or "genre_id": 2 but I can't seem to get it working. I keep getting django.db.utils.IntegrityError: books_book.genre_id may not be NULL.
I am using a DRF ModelViewSet for the views if that makes a difference.
What am I missing here?
You are getting Integrity error because it's expecting the author instance but you are sending the pk related to author. Try this
serializers.py
class BookSerializer(serializers.ModelSerializer):
author = AuthorSerializer(read_only=True)
genre = GenreSerializer(read_only=True)
class Meta:
model = Book
fields = ('id','url', 'author', 'genre', 'title', 'isbn', 'summary',)
def create(self, validated_data):
author_id = self.initial_data.pop("author")
genre_id = self.initial_data.pop("genre")
author = Author.objects.get(id=author_id)
genre = Genre.objects.get(id=genre_id)
book = Book.objects.create(author=author, genre=genre, **validated_data)
return book

Categories

Resources