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']
Related
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)
I am creating a cart with a list of products. I am trying to turn my product ids into titles while still being able to use my POST/ PUT methods on the products to be able to add/ remove the products.
Previously my products in the cart was displayed as IDs, this was my serializer:
class CartSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='cart-api:cart-detail',
read_only=True,
lookup_field='id',
)
class Meta:
model = Cart
fields = [
"id",
"url",
"products",
"sub_total",
"shipping",
"total",
]
After making some changes, I was able to turn the product ids into titles and several other fields related to each product. Here is my serializer code after the changes:
But my products disappeared from my put method and I am no longer able to add/ remove products.
(snippet 2)
How can we keep the the POST/ PUT methods on my product while displaying them as the way I want in snippet 2? Thanks so much in advance!
class MyPaintingSerializer(serializers.ModelSerializer):
painting_url = serializers.HyperlinkedIdentityField(
view_name='paintings-api:detail',
read_only=True,
lookup_field='slug'
)
class Meta:
model = Painting
fields = [
'id',
'title',
'painting_url',
'price',
]
class CartSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='cart-api:cart-detail',
read_only=True,
lookup_field='id',
)
products = MyPaintingSerializer(many=True, read_only=True)
class Meta:
model = Cart
fields = [
"id",
"url",
"products",
"sub_total",
"shipping",
"total",
]
Here is my code for views.py
class CartListCreateAPIView(generics.ListCreateAPIView):
queryset = Cart.objects.all()
serializer_class = CartSerializer
permission_classes = [permissions.AllowAny]
def perform_create(self, serializer):
serializer.save()
def get_queryset(self):
queryList = Cart.objects.all()
cart_id = self.request.session.get("cart_id", None)
return queryList
class CartDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Cart.objects.all()
serializer_class = CartSerializer
lookup_field = 'id'
permission_classes = [permissions.AllowAny]
I'm designing a mailbox with Django. My code is as follows:
#models.py
class Post(models.Model):
text = models.CharField(max_length=256)
sender = models.ForeignKey(User)
receiver = models.ForeignKey(User)
class Comment(models.Model):
post = models.ForeignKey(Post)
text = models.CharField(max_length=256)
#serializers.py
class CommentSerializer(serializers.ModelSerializer):
post = serializers.PrimaryKeyRelatedField()
class Meta:
model = Comment
fields = [
'id',
'text',
'post'
]
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = [
'id',
'text',
'sender',
'receiver',
]
class MainUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email']
I tried to customize serializer and have a serializer as follows:
class PostSerializer(serializers.Field):
def to_representation(self, value):
return PostSerializer(value, context={'request': self.context['request']}).data
def to_internal_value(self, id):
try:
id = int(id)
except ValueError:
raise serializers.ValidationError("Id should be int.")
try:
post = Post.objects.get(pk=id)
except User.DoesNotExist:
raise serializers.ValidationError("Such a post does not exist")
return user
I want to represent comment objects like this
{
"post":{
"text" = "Hello"
"sender" = 1
"receiver" = 2
}
"text": "Greate"
}
My code works great but The problem is it doesn't show the Combo Box for selecting the post. I also tried to customize the PrimaryKeyRelatedField's to_represent method in this way:
class PostSerializer(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
post_id = super(PostSerializer, self).to_representation(value)
post = Post.objects.get(pk=user_id)
return PostSerializer(
user, {"context":self.context['request']}
).data
but it says the unhashable type: 'ReturnDict' and as I understand we could return anything but simple things such as int or string. Is there a way to do this?
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)
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