I have an author and books model. An author has many books with him
class Author(Model):
id = UUIDField(primary_key=True, default=uuid4, editable=False)
name = CharField(max_length=50)
email = CharField(max_length=50)
class Book(Model):
id = UUIDField(primary_key=True, default=uuid4, editable=False)
name = CharField(max_length=50)
author = ForeignKey(Author, on_delete=models.CASCADE)
In my urls.py
author_router = SimpleRouter()
author_router.register(
r"author", AuthorViewSet, basename=author
)
nested_author_router = NestedSimpleRouter(author_router, r"author", lookup="author")
nested_author_router.register(r"book", BookViewSet)
In my searlizers.py
class BookSerializer(ModelSerializer):
class Meta:
model = Book
fields = (
"id",
"name",
"author",
)
extra_kwargs = {
"id": {"required": False},
"author": {"required": False},
}
class AuthorSerialzer(ModelSerializer):
class Meta:
model = Author
fields = (
"id",
"name",
"email",
)
extra_kwargs = {
"id": {"required": False},
}
In views.py
class BookViewSet(GenericViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
def create(self, request, author_pk):
data = request.data
data["author"] = author_pk
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
Since books are related to the author and I am using nested routers the curl call would look like
curl --location --request POST 'localhost:8000/author/1/book' --data '{"name": "Book Name"}'
In my BookViewSet I end up manually adding the author_pk to the data object before calling serializer is_valid method. Is there a way to specify the source from URL route or any better way of doing this?
In this case you can pass the author_pk to save() to automatically set the author id of the newly created book, as explained here:
def create(self, request, author_pk):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(author_id=author_pk)
return Response(serializer.data)
Related
I have a Story and Post models, where a Post belongs to a Story. I want a URL to get all Posts associated with a given Story.
I was able to override the get_queryset of my PostViewSet in order to filter posts by story with URLs like http://localhost:8000/posts/?story=1/. This works beautifully if I type in the URL directly. Now I want to return this kind of url in my StorySerializer. I would like to be able to get Story responses that look like this
[
{
"url": "http://localhost:8000/stories/1/",
"title": "Hero's Journey",
"openings": 0,
"date_created": "2020-06-28T16:53:35.150630Z",
"posts": "http://localhost:8000/posts/?story=1/"
},
{
"url": "http://localhost:8000/stories/2/",
"title": "Halo 3",
"openings": 0,
"date_created": "2020-06-28T18:17:12.973586Z",
"posts": "http://localhost:8000/posts/?story=2/"
}
]
Is there DRF support for this kind of thing? I was trying to use a HyperlinkedIdentityField with 'post-list' View in my StorySerializer, but I couldn't find a combination of parameters that would work. The current exception I get is
AttributeError: 'Story' object has no attribute 'posts'
Serializers
class StorySerializer(serializers.HyperlinkedModelSerializer):
posts = serializers.HyperlinkedIdentityField(
view_name = 'post-list',
many=True,
lookup_field = 'pk',
lookup_url_kwarg = 'story',
)
class Meta:
model = models.Story
fields = ['url', 'title', 'openings', 'date_created', 'posts']
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = models.Post
fields = ['url', 'story', 'user', 'text', 'date_created']
Views
class StoryViewSet(viewsets.ModelViewSet):
queryset = models.Story.objects.all()
serializer_class = serializers.StorySerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = models.Post.objects.all()
serializer_class = serializers.PostSerializer
def get_queryset(self):
queryset = self.queryset
story_id = self.request.query_params.get('story', None)
if story_id is not None:
queryset = queryset.filter(story=story_id)
return queryset
Models
class Story(models.Model):
title = models.CharField(max_length=100)
date_created = models.DateTimeField(default=timezone.now)
openings = models.PositiveSmallIntegerField(default=0)
participant = models.ManyToManyField(User)
class Post(models.Model):
text = models.CharField(max_length=300)
user = models.ForeignKey(
User,
on_delete=models.PROTECT)
story = models.ForeignKey(
Story,
on_delete=models.PROTECT)
date_created = models.DateTimeField(default=timezone.now)
I was able to find a great solution here, overriding the get_url method to map 'pk' value to 'story' directly.
https://stackoverflow.com/a/27584761/7308261
from rest_framework.reverse import reverse
import urllib
class StoryPostsHyperlinkedIdentityField(serializers.HyperlinkedIdentityField):
def get_url(self, obj, view_name, request, format):
lookup_field_value = getattr(obj, self.lookup_field, None)
result = '{}?{}'.format(
reverse(view_name, kwargs={}, request=request, format=format),
urllib.parse.urlencode({'story': lookup_field_value})
)
return result
class StorySerializer(serializers.HyperlinkedModelSerializer):
posts = StoryPostsHyperlinkedIdentityField(
view_name='post-list',
)
class Meta:
model = models.Story
fields = ['url', 'title', 'openings', 'date_created', 'posts']
In my API, I have two models Question and Option as shown below
class Question(models.Model):
body = models.TextField()
class Options(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
option = models.CharField(max_length=100)
is_correct = models.SmallIntegerField()
While creating a question it would be nicer the options can be created at the same time. And already existed question should not be created but the options can be changed if the options are different from previous.
I am using ModelSerializer and ModelViewSet. I use different urls and views for Question and Option.
serializers.py
class QuestionSerializer(serializers.ModelSerializer):
class Meta:
model = Question
fields = '__all__'
class OptionReadSerializer(serializers.ModelSerializer):
question = QuestionSerializer(read_only=True)
class Meta:
model = Option
fields = ('question', 'option', 'is_correct')
class OptionWriteSerializer(serializer.ModelSerializer):
class Meta:
model = Option
fields = ('question', 'option', 'is_correct')
views.py
class QuestionViewSet(ModelViewSet):
seriaizer_class = QuestionSerializer
queryset = Question.objects.all()
class OptionViewSet(ModelViewSet):
queryset = Option.objects.all()
def get_serializer_class(self):
if self.request.method == 'POST':
return OptionWriteSerializer
return OptionReadSerializer
urls.py
from django.urls import include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('api/question', QuestionViewset, base_name='question')
router.register('api/option', OptionViewSet, base_name='option')
urlpatterns = [
path('', include(router.urls))
]
In this way, I always have to create questions first and then I can individually add the option for that question. I think this may not be a practical approach.
It would be nicer that question and option can be added at the same time and similar to all CRUD operations.
The expected result and posting data in JSON format are as shown below:
{
"body": "Which country won the FIFA world cup 2018",
"options": [
{
"option": "England",
"is_correct": 0
},
{
"option": "Germany",
"is_correct": 0
},
{
"option": "France",
"is_correct": 1
}
]
}
We can use PrimaryKeyRelatedField.
tldr;
I believe a Question can have multiple Options attached to it. Rather than having an Option hooked to a Question.
Something like this:
class Question(models.Model):
body = models.TextField()
options = models.ManyToManyField(Option)
class Options(models.Model):
text = models.CharField(max_length=100)
is_correct = models.BooleanField()
Then we can use PrimaryKeyRelatedField something like this:
class QuestionSerializer(serializers.ModelSerializer):
options = serializers.PrimaryKeyRelatedField(queryset=Options.objects.all(), many=True, read_only=False)
class Meta:
model = Question
fields = '__all__'
Reference : https://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
In models I added related_name='options' in foreign key field of Option model
models.py
class Question(models.Model):
body = models.TextField()
class Options(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='options')
option = models.CharField(max_length=100)
is_correct = models.SmallIntegerField()
In QuestionWriteSerializer I override the update() and create() method. For creating and updating the logic was handled from QuestionWriteSerialzer.
serializers.py
class OptionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
class Meta:
model = Option
fields = ('id', 'question', 'option', 'is_correct')
class QuestionReadSerializer(serializers.ModelSerializer):
options = OptionSerializer(many=True, read_only=True)
class Meta:
model = Question
fields = ('id', 'body', 'options')
class QuestionWriteSerializers(serializers.ModelSerializer):
options = OptionSerializer(many=True)
class Meta:
model = Question
fields = ('id', 'body', 'options')
def create(self, validated_data):
options_data = validated_data.pop('options')
question_created = Questions.objects.update_or_create(**validated_data)
option_query = Options.objects.filter(question=question_created[0])
if len(option_query) > 1:
for existeding_option in option_query:
option_query.delete()
for option_data in options_data:
Options.objects.create(question=question_created[0], **option_data)
return question_created[0]
def update(self, instance, validated_data):
options = validated_data.pop('options')
instance.body = validated_data.get('body', instance.body)
instance.save()
keep_options = []
for option_data in options:
if 'id' in option_data.keys():
if Options.objects.filter(id=option_data['id'], question_id=instance.id).exists():
o = Options.objects.get(id=option_data['id'])
o.option = option_data.get('option', o.option)
o.is_correct = option_data.get('is_correct', o.is_correct)
o.save()
keep_options.append(o.id)
else:
continue
else:
o = Options.objects.create(**option_data, question=instance)
keep_options.append(o.id)
for option_data in instance.options.all():
if option_data.id not in keep_options:
Options.objects.filter(id=option_data.id).delete()
return instance
The QuestionViewSet is almost the same and I removed the OptionViewSet and controlled all things from QuestionViewSet
views.py
class QuestionViewSet(ModelViewSet):
queryset = Question.objects.all()
def get_serializer_class(self) or self.request.method == 'PUT' or self.request.method == 'PATCH':
if self.request.method == 'POST':
return QuestionWriteSerializer
return QuestionReadSerializer
def create(self, request, *args, **kwargs):
"""
Overriding create() method to change response format
"""
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response({
'message': 'Successfully created question',
'data': serializer.data,
'status': 'HTTP_201_CREATED',
}, status=status.HTTP_201_CREATED, headers=headers)
else:
return Response({
'message': 'Can not create',
'data': serializer.errors,
'status': 'HT',
}, status=status.HTTP_400_BAD_REQUEST)
I can't figure out how to pass user object to the following serializer:
class ReviewSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = Review
fields = ('pk', 'title', 'user', 'movie', 'timestamp', 'review_text',)
I have this viewset:
class ReviewsViewSet(viewsets.ModelViewSet):
queryset = Review.objects.all()
serializer_class = ReviewSerializer
and this model:
class Review(models.Model):
title = models.CharField(max_length=255)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='reviews')
movie = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='reviews')
review_text = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return '{movie} review by {user}'.format(user=self.user, movie=self.movie)
My javascript request looks like this:
return axios({
method: 'post',
url: 'http://localhost:8000/api/reviews/',
data: { // Using data from Vue
title: this.review_title,
movie: this.id,
review_text: this.review_text,
user: JSON.stringify(this.user)
},
headers: {
'Content-Type': 'application/json',
Authorization: `JWT ${token}`
}
})
It gives me this traceback.
How should I pass the user object to the request?
Thanks in advance.
Remove read_only=True from serializer
class ReviewSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Review
fields = ('pk', 'title', 'user', 'movie', 'timestamp', 'review_text',)
If you set read_only=True, the DRF will not takes the value from input source even if it's there
From the doc,
Read-only fields are included in the API output, but should not be
included in the input during create or update operations. Any
'read_only' fields that are incorrectly included in the serializer
input will be ignored.
Set this to True to ensure that the field is used when serializing a
representation, but is not used when creating or updating an instance
during deserialization.
Defaults to False
UPDATE
You should override the create() method of ReviewSerializer as
class ReviewSerializer(serializers.ModelSerializer):
user = UserSerializer()
def create(self, validated_data):
user_dict = validated_data.pop('user')
user_obj, created = User.objects.get_or_create(**user_dict)
return Review.objects.create(user=user_obj, **validated_data)
class Meta:
model = Review
fields = ('pk', 'title', 'user', 'movie', 'timestamp', 'review_text',)
for debug purpose only
class ReviewsViewSet(viewsets.ModelViewSet):
queryset = Review.objects.all()
serializer_class = ReviewSerializer
def create(self, request, *args, **kwargs):
print(request.data) # print here <<<<
return super(ReviewsViewSet, self).create(request, *args, **kwargs)
This post has an update below.
I currently have these two models. I am trying to create a job using CreateAPIView. Before I show the view here are my models
class modelJobCategory(models.Model):
description = models.CharField(max_length=200, unique=True)
other = models.CharField(max_length=200, unique=False , blank=True , null=True)
class modelJob(models.Model):
category = models.ManyToManyField(modelJobCategory,null=True,default=None,blank=True)
description = models.CharField(max_length=200, unique=False)
These two are my serializers
class Serializer_CreateJobCategory(ModelSerializer):
class Meta:
model = modelJobCategory
fields = [
'description',
]
class Serializer_CreateJob(ModelSerializer):
class Meta:
model = modelJob
category = Serializer_CreateJobCategory
fields = [
'category',
'description',
]
def create(self, validated_data):
job = modelJob.objects.create(user=user,category=?,...) #How to get category ?
return job
Now this is my view
class CreateJob_CreateAPIView(CreateAPIView):
serializer_class = Serializer_CreateJob
queryset = modelJob.objects.all()
def post(self, request, format=None):
serializer = Serializer_CreateJob(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)
Now I am passing the following JSON
{
"category" :{
"description": "Foo"
},
"description" : "World"
}
However I get the exception
{
"category": [
"Incorrect type. Expected pk value, received str."
]
}
I came across the same question here and it mentions i need to define a slug field which I am not sure where. Any suggestion on how I can fix this ?
Update:
So my create Job serializer looks like this now however it returns back the error
Got AttributeError when attempting to get a value for field category
on serializer Serializer_CreateJob. The serializer field might be
named incorrectly and not match any attribute or key on the modelJob
instance. Original exception text was: 'ManyRelatedManager' object has
no attribute 'description'.
class Serializer_CreateJob(ModelSerializer):
category = serializers.CharField(source='category.description')
class Meta:
model = modelJob
category = Serializer_CreateJobCategory()
fields = [
'category',
'description',
]
def create(self, validated_data):
category_data = validated_data.pop('category')
category = modelJobCategory.objects.get(description=category_data['description'])
job = modelJob.objects.create(description=validated_data["description"])
job.category.add(category)
job.save()
return job
Any suggestions on how I can fix this now ?
Can you try this?
class Serializer_CreateJob(ModelSerializer):
category = serializers.SlugRelatedField(
many=True,
queryset=modelJobCategory.objects.all(),
slug_field='description'
)
class Meta:
model = modelJob
fields = [
'category',
'description',
]
Try to explicitly define category field and use source=category.description like this:
from rest_framework import serializers
class Serializer_CreateJob(ModelSerializer):
category = serializers.CharField(source='category.description')
class Meta:
model = modelJob
category = Serializer_CreateJobCategory
fields = [
'category',
'description',
]
def create(self, validated_data):
category_data = validated_data.pop('category')
category = Category.objects.get(description=category_data['description'])
job = modelJob.objects.create(description=validated_data['description'],category=category,...) #categy object found by it's description
return job
When posting this:
curl -X POST -H "Authorization: Token sometoken" -d "url=someurl" 127.0.0.1:8000/create/
I get the error:
{"user":["This field is required."] with the ItemSerializer,
I have seen other posts on SO talking about using perform_create, which I am trying to use to save the user object, but it doesn´t work for some reason. Perform_create works when user is defined like this:
user = serializers.CharField(
default=serializers.CurrentUserDefault()
)
But I want to use the user object, not only CharField storing the username
Serializers:
class UserDetailsSerializer(serializers.ModelSerializer):
class Meta:
model = UserModel
fields = ('pk', 'username', 'email', 'first_name', 'last_name')
read_only_fields = ('email', )
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['cat']
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = [
'comment',
]
class ItemSerializer(serializers.HyperlinkedModelSerializer):
user = UserDetailsSerializer()
category = CategorySerializer(many=True)
thecomments = CommentSerializer(many=True)
timestamp = serializers.SerializerMethodField('get_mytimestamp')
def get_mytimestamp(self, obj):
return time.mktime(datetime.datetime.now().timetuple())
class Meta:
model = Item
fields = [
"url",
"user",
"timestamp",
"categories",
"thecomments",
]
Model:
class Item(models.Model):
url = models.CharField(max_length=1000)
user = models.ForeignKey('auth.User', unique=False)
timestamp = models.DateTimeField(auto_now_add=True)
url = models.CharField(max_length=1000)
categories = models.ManyToManyField(Category)
View:
class ItemCreateAPIView(generics.CreateAPIView):
serializer_class = ItemSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Solution:
Serializer to create Item:
class CreateSerializer(serializers.HyperlinkedModelSerializer):
timestamp = serializers.SerializerMethodField('get_mytimestamp')
def get_mytimestamp(self, obj):
return time.mktime(datetime.datetime.now().timetuple())
class Meta:
model = Item
fields = [
"url",
"timestamp",
]
views.py
class ItemCreateAPIView(generics.CreateAPIView):
serializer_class = CreateSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
post:
curl -X POST -H "Authorization: Token sometoken" -d "url='someurl'" 127.0.0.1:8000/createitem/
class ItemSerializer(serializers.HyperlinkedModelSerializer):
user = UserDetailsSerializer()
category = CategorySerializer(many=True)
thecomments = CommentSerializer(many=True)
timestamp = serializers.SerializerMethodField('get_mytimestamp')
def get_mytimestamp(self, obj):
return time.mktime(datetime.datetime.now().timetuple())
class Meta:
model = Item
fields = [
"url",
"user",
"timestamp",
"categories",
"thecomments",
]
extra_kwargs = {'user': {'required': False}}
add extra_kwargs = {'user': {'required': False}} to your serializer Meta
i had the same issue.
added extra_kwargs = {'user': {'required': False}} to serializer Meta class and it worked