I'm trying to fetch related objects from below two models.
Following django models with ManyToManyField relationship.
Book
class Book(models.Model):
authors = models.ManyToManyField(
to=Author, verbose_name="Authors", related_name="books_author"
)
bookshelves = models.ManyToManyField(
to=Bookshelf, verbose_name="Bookshelf", related_name="books_shelves"
)
copyright = models.NullBooleanField()
download_count = models.PositiveIntegerField(blank=True, null=True)
book_id = models.PositiveIntegerField(unique=True, null=True)
languages = models.ManyToManyField(
to=Language, verbose_name=_("Languages"), related_name="books_languages"
)
Author
class Author(models.Model):
birth_year = models.SmallIntegerField(blank=True, null=True)
death_year = models.SmallIntegerField(blank=True, null=True)
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Meta:
verbose_name = _("Author")
verbose_name_plural = _("Author")
I have to fetch all the Auhtors with their related books. I have tried a lot of different ways none is working for me.
First way : using prefetch_related
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorSerializer
queryset = Author.objects.exclude(name__isnull=True)
def get_queryset(self):
auths = queryset.prefetch_related(Prefetch("books_author"))
Second way using related_name 'books_auhtor'
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorSerializer
queryset = Author.objects.exclude(name__isnull=True)
def get_queryset(self):
auths = queryset.books_author.all()
None of the above ways worked for me. I want to prepare a list of Authors and their associated books.
For ex:-
[{'Author1':['Book1','Book2'],... }]
Prefetching is not necessary, but can be used to boost efficiency, you can work with:
class AuthorListAPIView(APIErrorsMixin, generics.ListAPIView):
serializer_class = AuthorWithBooksSerializer
queryset = Author.objects.exclude(name=None).prefetch_related('books_author')
In the AuthorWithBooksSerializer, you can then add the data of the books, for example:
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('book_id', 'copyright')
class AuthorWithBooksSerializer(serializers.ModelSerializer):
books = BookSerializer(source='books_author', many=True)
class Meta:
model = Author
fields = ('name', 'books')
Here the books will use the BookSerializer and thus encode a list of dictionaries.
While you can use the name of the author as object key, I strongly advise against this: it makes the object less accessible since the keys are no longer fixed and if these contain spaces, it can also result in more trouble obtaining the value(s) associated with a given attribute name.
Related
I'm trying to use my api to create and update products in a bundle. I did so:
model.py
class Business(models.Model):
name = models.CharField(max_length=155)
class Product(models.Model):
business = models.ForeignKey(
Business,
on_delete=models.CASCADE,
blank=True,
null=True,
)
name = models.CharField(max_length=200)
description = models.TextField(null=True, blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return self.name
class Meta:
verbose_name = "Product"
class Bundle(models.Model):
business = models.ForeignKey(
Business,
on_delete=models.CASCADE,
blank=True,
null=True,
)
name = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
products = models.ManyToManyField(Product, related_name="bundles",blank=True, null=True, through="BundleProduct")
class Meta:
verbose_name = "Bundle"
def __str__(self):
return self.name
class BundleProduct(models.Model):
bundle = models.ForeignKey(Bundle, on_delete=models.CASCADE, related_name="bundleproducts")
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="bundleproducts")
number = models.IntegerField(default=1)
class Meta:
verbose_name = "Bundle of Product"
def __str__(self):
return str(self.product.name) + " do " + self.bundle.name
def get_absolute_url(self):
return reverse("BundleProduct_detail", kwargs={"pk": self.pk})
And here is my serializers.py:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = "__all__"
class BundleProductSerializer(serializers.ModelSerializer):
class Meta:
model = BundleProduct
fields = "__all__"
class BundleSerializer(serializers.ModelSerializer):
class Meta:
model = Bundle
fields = "__all__"
My viewset.py
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
model = Product
class BundleProductViewSet(viewsets.ModelViewSet):
queryset = BundleProduct.objects.all()
serializer_class = BundleProductSerializer
model = BundleProduct
class BundleViewSet(viewsets.ModelViewSet):
queryset = Bundle.objects.all()
serializer_class = BundleSerializer
model = Bundle
When I try to post some products in bundleproducts I receive "Incorrect type. Expected pk value, received list."
Reading about this error, I found some issues relating to PrimaryKeyRelatedField and SlugRelatedField. I know I need to override but I have no idea how to do it.
It's an example of how to post would works:
{
"number": 1,
"bundle": 2,
"product":
[
1,
2
]
}
After watching the video commented by Neil, I created the following method:
class BundleSerializer(
serializers.ModelSerializer
):
products = ProductSerializer(many=True)
def create(self, validated_data):
products = validated_data.pop('products')
bundle = BundleProduct.objects.create(**validated_data)
for product in products:
BundleProduct.objects.create(**product, bundle=bundle)
return Bundle
class Meta:
model = Bundle
fields = "__all__"
But doesn't work. I receive this error: "TypeError at /api/v1/bundle/
'name' is an invalid keyword argument for this function"
If you are making post via BundleSerializer you need to pass products with list of ProductSerializer data not just id since products in BundleSerializer is accepting productsSerializer data. You are getting type error 'name' is an invalid keyword argument for this function" because your validated_data contain name and BundleProduct object Does not have name field.And you are creating BundleProduct objects with validated_data.
Create bundle object and pass id of bundle object to BundleProduct object.
If you do not want to create product and just pass existing product id you need to make ListField
You need to Override get_fields and check the requests
override to_representation to return always List of ProdutSerializer Data
Override create for POST request
Override update for PUT and PATCH Request
Below is solution for POST Request
For PATCH AND PUT Request you need to override update method of ModelSerializer and handle the products accordingly.
class BundleSerializer(serializers.ModelSerializer):
def create(self, validated_data):
products = validated_data.pop('products')
bundle = Bundle.objects.create(**validated_data)
for product_id in products:
product = get_object_or_404(Product, pk=product_id)
BundleProduct.objects.create(product=product, bundle=bundle)
return bundle
class Meta:
model = Bundle
fields = "__all__"
def to_representation(self, instance):
repr = super().to_representation(instance)
repr['products'] = ProductSerializer(instance.products.all(), many=True).data
return repr
def get_fields(self):
fields = super().get_fields()
if self.context['request'].method in ['POST', "PATCH","PUT"]:
fields['products'] = serializers.ListField(
write_only=True,
child=serializers.IntegerField()
)
return fields
sample POST data to BundleSerializer
{
"products":[1,2],
"name":"Offer One",
"description":"description",
"price":1212,
"business":1
}
In my experience, if you want to update a model and a related model in one request, with DRF, the easiest way to do this is to override the "create" method of a serializer. There's a good video on this here which I used as my reference: https://www.youtube.com/watch?v=EyMFf9O6E60
The issue here is that you are posting a list to BundleProduct's product field yet it is an ForeignKey. To join Bundle to a Product, simply POST:
{
"bundle": 2,
"product" 1,
"number": 1
}
You can repeat this:
{
"bundle": 2,
"product" 4,
"number": 1
}
to add yet another product 4 to the same bundle and so on. Just make sure you do them one by one and not in a list as you had done earlier.
I have a simple DRF REST API that I want to use to create blog articles. I want to be able to add tags to those blog articles so users can search tags and see related articles. However, the tags may not exist yet. I have created an Article Model with a ForeignKey field to a Tag Model like this:
class Tag(models.Model):
name = models.CharField(max_length=32)
def _str__(self):
return self.name
class Meta:
ordering = ('name',)
class Article(models.Model):
title = models.CharField(max_length=256)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
date = models.DateTimeField(auto_now_add=True)
tags = models.ForeignKey(Tag, on_delete=models.CASCADE, blank=True, default=None)
def __str__(self):
return self.title
class Meta:
ordering = ('date', 'id')
Ideally what I want is to be able to POST a new Article with a set of tags, and if any of the tags don't exist, create them in the DB. However, as it is currently, the tags need to already exist to be added to the Article. Visually, DRF shows this as a dropdown that is populated with pre-existing tags:
How can I add or create multiple Tags from my Article API endpoint?
EDIT: As requested, I've added my views.py
views.py:
from api.blog.serializers import ArticleSerializer, TagSerializer
from rest_framework import viewsets
# /api/blog/articles
class ArticleView(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# /api/blog/tags
class TagView(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
For completeness, here are my serializers from my REST API's serializers.py.
serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = '__all__'
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
urls.py:
from rest_framework import routers
router = routers.DefaultRouter()
router.register('articles', views.ArticleView)
router.register('tags', views.TagView)
urlpatterns = [
path('', include(router.urls)),
]
Overriding the create() method of the serializer as
class ArticleSerializer(serializers.ModelSerializer):
tags = serializers.CharField()
class Meta:
model = Article
fields = '__all__'
def create(self, validated_data):
tag = validated_data.pop('tags')
tag_instance, created = Tag.objects.get_or_create(name=tag)
article_instance = Article.objects.create(**validated_data, tags=tag_instance)
return article_instance
Okay, thanks to #JPG for their help. This is what I've ended up with. It allows users to add space delimited tags into a CharField on the /api/blog/article endpoint. When a POST request is performed, the tags are split on spaces, get_or_create()d (for this to work I needed to make Tag.name the primary key), and then added to the Article with article.tags.set(tag_list). As #JPG and #Martins suggested, a ManyToManyField() was the best way to do this.
Here is my full code:
serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class TagsField(serializers.CharField):
def to_representation(self, tags):
tags = tags.all()
return "".join([(tag.name + " ") for tag in tags]).rstrip(' ')
tags = TagsField()
class Meta:
model = Article
fields = '__all__'
def create(self, validated_data):
tags = validated_data.pop('tags') # Removes the 'tags' entry
tag_list = []
for tag in tags.split(' '):
tag_instance, created = Tag.objects.get_or_create(name=tag)
tag_list += [tag_instance]
article = Article.objects.create(**validated_data)
print(tag_list)
article.tags.set(tag_list)
article.save()
return article
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
Note that I had to create a custom TagField() and override to_representation(). This is because if I used a regular serializer.CharField() tags were displayed as: "Blog.tag.None" instead of the tag values, like this:
models.py:
class Tag(models.Model):
name = models.CharField(max_length=32, primary_key=True)
def __str__(self):
return self.name
class Meta:
ordering = ('name',)
class Article(models.Model):
title = models.CharField(max_length=256)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
date = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField(Tag)
def __str__(self):
return self.title
class Meta:
ordering = ('date', 'id')
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 = ...
I am writing a simple database for the condo I live in which has a list of people, units, unit type (home vs parking space), and unitholder (join table for many-to-many relationship between a person and a unit) - one person can be the owner of a unit type of "home" while renting a parking space.
This is my model:
class Person(models.Model):
first_name = models.CharField(max_length=30, null=False)
last_name = models.CharField(max_length=30, null=False)
phone = models.CharField(max_length=20)
email = models.EmailField(max_length=20)
class UnitType(models.Model):
description = models.CharField(max_length=30)
class Unit(models.Model):
unit_number = models.IntegerField(null=False, unique=True)
unit_type = models.ForeignKey(UnitType, null=False)
unitholders = models.ManyToManyField(Person, through='UnitHolder')
class UnitHolderType(models.Model):
description = models.CharField(max_length=30)
class UnitHolder(models.Model):
person = models.ForeignKey(Person)
unit = models.ForeignKey(Unit)
unitholder_type = models.ForeignKey(UnitHolderType)
This is my view:
class PersonViewSet(viewsets.ModelViewSet):
queryset = Person.objects.all()
serializer_class = PersonSerializer
class UnitHolderTypeViewSet(viewsets.ModelViewSet):
queryset = UnitHolderType.objects.all()
serializer_class = UnitHolderTypeSerializer
class UnitViewSet(viewsets.ModelViewSet):
queryset = Unit.objects.all()
serializer_class = UnitSerializer
class UnitHolderViewSet(viewsets.ModelViewSet):
queryset = UnitHolder.objects.all()
serializer_class = UnitHolderSerializer
class UnitTypeViewSet(viewsets.ModelViewSet):
queryset = UnitType.objects.all()
serializer_class = UnitTypeSerializer
This is my serializer:
class UnitSerializer(serializers.ModelSerializer):
unit_type = serializers.SlugRelatedField(
queryset=UnitType.objects.all(), slug_field='description'
)
class Meta:
model = Unit
fields = ('unit_number', 'unit_type', 'unitholders')
class UnitTypeSerializer(serializers.ModelSerializer):
class Meta:
model = UnitType
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
class UnitHolderSerializer(serializers.ModelSerializer):
person = serializers.PrimaryKeyRelatedField(many=False, read_only=True)
unit = serializers.PrimaryKeyRelatedField(many=False, read_only=True)
class Meta:
model = UnitHolder
fields = ('person', 'unit', 'unitholder_type')
class UnitHolderTypeSerializer(serializers.ModelSerializer):
class Meta:
model = UnitHolderType
The problem:
When I query the /units endpoint like the following:
u = requests.get('http://localhost:8000/units').json()
My response looks like this:
[{'unit_type': 'Home', 'unit_number': 614, 'unitholders': [1]}]
What I want back is something like this:
[
{
'unit_type': 'Home',
'unit_number': 614,
'unitholders': [
{
'id: 1,
'first_name': 'myfirstname',
'last_name': 'mylastname',
'unitholder_type': 'renter'
}
]
}
]
I'm pretty sure my problem is in my UnitSerializer but I am brand new to DRF and read the through the documentation but still can't seem to figure it out.
An easy solution would be using depth option:
class UnitSerializer(serializers.ModelSerializer):
unit_type = serializers.SlugRelatedField(
queryset=UnitType.objects.all(), slug_field='description'
)
class Meta:
model = Unit
fields = ('unit_number', 'unit_type', 'unitholders')
depth = 1
This will serialize all nested relations 1 level deep. If you want to have fine control over how each nested field gets serialized, you can list their serializers explicitly:
class UnitSerializer(serializers.ModelSerializer):
unit_type = serializers.SlugRelatedField(
queryset=UnitType.objects.all(), slug_field='description'
)
unitholders = UnitHolderSerializer(many=True)
class Meta:
model = Unit
fields = ('unit_number', 'unit_type', 'unitholders')
Also as a side note, you need to look into modifying your querysets inside views to prefetch related objects, otherwise you will destroy the app performance very quickly (using something like django-debug-toolbar for monitoring generated queries is very convenient):
class UnitViewSet(viewsets.ModelViewSet):
queryset = Unit.objects.all().select_related('unit_type').prefetch_related('unitholders')
serializer_class = UnitSerializer
Perhaps you must doing somethings so:
class UnitHolderViewSet(viewsets.ModelViewSet):
queryset = UnitHolder.objects.all()
unitholders = UnitHolderSerializer(read_only=True, many=True)
Django rest framework serializing many to many field
I have combined these two answers: one and two In the attempt to select only certain fields from nested objects without any success at all, the result is returning ALL fields from all tables.
serializers:
class NameTestTypeSerializer(serializers.ModelSerializer):
class Meta:
model = TestTypeModel
fields = 'name'
class ExecutedTestSerializer(serializers.ModelSerializer):
test_type = NameTestTypeSerializer
class Meta:
model = ExecutedTestModel
fields = ('id', 'result', 'test_type')
depth = 1
models:
class TestTypeModel(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(null=False, max_length=255, unique=True)
........
class Meta:
db_table = 'TestType'
class ExecutedTestModel(models.Model):
id = models.AutoField(primary_key=True)
test_type = models.ForeignKey(TestTypeModel, to_field='id')
result = models.IntegerField(null=False)
class Meta:
db_table = 'ExecutedTest'
viewset:
class ExecutedTestViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticatedOrReadOnly,)
serializer_class = ExecutedTestSerializer
def get_queryset(self):
queryset = ExecutedTestModel.objects.all().select_related('test_type').defer('test_type__executable' )
return queryset
How did you check that executable is fetched? In django you can access deferred fields, they are loaded from db on demand.
I believe the problem isn't in underscore notation, instead it is in the definition of the serializers.