So I have 3 models of interest:
models.py
class Author(models.Model): #Author of books
name = models.CharField(max_length = 100)
#property # This is code to get the books linked to an author
def books(self):
book = Book.objects.filter(authors = self.id)
return ', '.join(map(str,book)) #This makes the book list in this format "A, B, C"
def __str__(self):
return self.name.title()
class Genre(models.Model): #Genre of books
name = models.CharField(max_length = 50, unique = True)
#property
def books(self):
book = Book.objects.filter(genre = self.id)
return ', '.join(map(str,book))
def __str__(self):
return self.name.title()
class Book(models.Model):
name = models.CharField(max_length = 150,) #Name of books
authors = models.ManyToManyField(Author,) #Many to many because multiple books can have multiple authors
genre = models.ManyToManyField(Genre)
def __str__(self):
return self.name.title()
Those are the models, and both genre and author have a many to many relation with books
serializers.py
class AuthorListSerializer(serializers.ModelSerializer):
class Meta:
model = models.Author
fields = ('name',)
class GenreListSerializer(serializers.ModelSerializer):
class Meta:
model = models.Genre
fields = ('name',)
class BookListSerializer(serializers.ModelSerializer):
authors = AuthorListSerializer(many = True,) #To represent the relationship as a string instead of id
genre = serializers.SlugRelatedField(many = True, queryset = models.Genre.objects.all(),slug_field = 'name')
class Meta:
model = models.Book
fields = ('name','authors','rating', 'genre')
class BookDetailSerializer(serializers.ModelSerializer):
publisher = serializers.SlugRelatedField(queryset = models.Publisher.objects.all(), slug_field= 'name') #To display publisher name instead of id
authors = serializers.StringRelatedField(many = True,) #To represent the relationship as a string instead of id
genre = serializers.StringRelatedField(many = True,)
class Meta:
model = models.Book
fields = ('name', 'authors', 'rating','genre',
'publisher', 'total_qty', 'avail_qty',
'pub_date','isbn','price',)
So in my Django-Rest-Framework Browsable api, Normal ForeignKey fields have their options displayed to create a new object instance.
For example The publishers in the "BookDetailSerializer" have all the publishers displayed as options for a POST or PUT or PATCH request.
but for the many to many fields that include Genre and Author it is not only blank, but it appears that I cannot put any input whatsoever.
I have tried using the DRF-writable-nested third party package but nothing changed:
class BookListSerializer(WritableNestedModelSerializer):
authors = AuthorListSerializer(many = True,) #To represent the relationship as a string instead of id
genre = serializers.SlugRelatedField(many = True,
queryset = models.Genre.objects.all(),slug_field = 'name')
My question is how can I be able to make POST request with my list of Authors and Genres available through the DRF browsable api?
Thanks in advance!!
This image shows the options available for making a POST request involving publishers
This image shows that you can't add any input for POST request involving both Genre and Authors as many-to many relations.
Update: So I have added a create method and it still doesn't work, as shown below:
class BookListSerializer(serializers.ModelSerializer):
authors = AuthorListSerializer(many = True,) #To represent the relationship as a string instead of id
genre = serializers.SlugRelatedField(many = True,
queryset = models.Genre.objects.all(),slug_field = 'name')
class Meta:
model = models.Book
fields = ('name','authors','rating', 'genre')
def create(self,validated_data):
authors_data = models.Author.objects.all()
book = Book.objects.create(authors = authors_data,**validated_data,)
return book
What can I do?
After struggling for 4 days I figured out the answer
When handling many to many relationships and you want to want the code to be as follows.
class BookListSerializer(serializers.ModelSerializer):
class Meta:
model = models.Book
fields = ('name','authors','rating', 'genre')
depth = 1
Adding the depth allows the nested relationships to fully serialize.
Because the relationship is nested, and is a many to many relationship you would have to create your own create function in the views.py as follows:
class BookViewSet(GetSerializerClassMixin, viewsets.ModelViewSet):
queryset = models.Book.objects.all()
serializer_class = serializers.BookDetailSerializer
serializer_action_classes = {'list': serializers.BookListSerializer}
permission_classes = [IsAdminOrReadOnly, permissions.IsAuthenticated,]
def create(self,request, *args, **kwargs):
data = request.data
new_book = models.Book.objects.create(name = data["name"], publisher = models.Publisher.objects.get(id = data["publisher"]),
pub_date = data["pub_date"],
price = data["price"],
isbn = data['isbn'],)
new_book.save()
for author in data['authors']:
author_obj = models.Author.objects.get(name = author['name'])
new_book.authors.add(author_obj)
for gen in data['genre']:
gen_obj = models.Genre.objects.get(name = gen['name'])
new_book.genre.add(gen_obj)
serializer = serializers.BookListSerializer(new_book)
return Response(serializer.data)
For the many to many relationships you have to create them after saving the object and add them to the object manually.
There is a foriegn Key relationship that's present there "Publishers"
For that relationship you have to manually point to its location in the database hence the code below.
name = data["name"], publisher = models.Publisher.objects.get(id = data["publisher"]),
For the Detail book serializer in the question its the same thing with the BookListSerializer
That's how I was able to handle POST requests in the Many-to-Many relations
Related
I want to calculate the average rating by using SerializerMethodField().
The error in the following code is AttributeError: 'FeedbackModel' object has no attribute 'aggregate'
I think _set is missing but I don't know where to put it..!
class FeedbackSerializer(serializers.ModelSerializer):
feedback_by_user_profile_pic = serializers.ImageField(source='feedback_by.profile_pic')
average_rating = serializers.SerializerMethodField()
def get_average_rating(self,instance):
return instance.aggregate(average_rating=Avg('rating'))['average_rating']
class Meta:
model = FeedbackModel
fields = ['feedback_text','rating','date','feedback_by_user_profile_pic','average_rating']
Feedback Model
class FeedbackModel(models.Model):
feedback_text = models.CharField(max_length=1000)
rating = models.IntegerField()
date = models.DateField(auto_now=True)
feedback_by = models.ForeignKey(UserModel,on_delete=models.CASCADE)
business_account = models.ForeignKey(BusinessAccountModel,on_delete=models.CASCADE)
class Meta:
db_table = 'feedback'
BusinessAccountModel
class BusinessAccountModel(models.Model):
business_title = models.CharField(max_length=70)
business_description = models.CharField(max_length=500)
status = models.CharField(max_length=100)
note = models.CharField(max_length=200)
user = models.OneToOneField(UserModel,on_delete=models.CASCADE)
class Meta:
db_table = 'wp_business_acc'
BusiAccSerializer
class BusiAccSerializer(serializers.ModelSerializer):
class Meta:
model = BusinessAccountModel
fields = '__all__'
I think you need to add the average_rating field in the BusiAccSerializer, not in the FeedbackSerializer.
First you have to set the related_name attribute in the FeedbackModel.
class FeedbackModel(models.Model):
...
# here I added the `related_name` attribute
business_account = models.ForeignKey(BusinessAccountModel,on_delete=models.CASCADE, related_name="feedbacks")
And then in the BusiAccSerializer,
class BusiAccSerializer(serializers.ModelSerializer):
average_rating = serializers.SerializerMethodField(read_only = True)
def get_average_rating(self, obj):
return obj.feedbacks.aggregate(average_rating = Avg('rating'))['average_rating']
class Meta:
model = BusinessAccountModel
fields = (
'business_title', 'business_description', 'status', 'note', 'user', 'average_rating',
)
First of all, you've indicated that you need an average rating for a business account, but you can not get an average rating for an account without having a concrete business account, so you need to do it in the business account serializer.
David Lu has already answered how to do it in the BusiAccSerializer, but I have something to add:
What you've trying to do is to use a serializer method field to add some aggregated data to the output. This way of solving your problem has a major drawback: when you will try to serialize a list of BusinessAccountModels, the serializer will do a separate database call for each business account and it could be slow. You better need to specify an annotated queryset in your view like this:
BusinessAccountModel.objects.all().annotate(average_rating=Avg('feedbacks__rating'))
Then you will be able to use the result of calculation as a regular field in your serializer:
class BusiAccSerializer(serializers.ModelSerializer):
...
average_rating = serializers.FloatField(read_only=True)
This way there will be no additional database queries done by the serializer.
Let us imagine that I have two models.
First model contains curse details and user that created this course
class Course(models.Model):
course_name = models.CharField(max_length=100, null=False)
description = models.CharField(max_length=255)
user_profile = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
and my second model is:
class Lesson(models.Model):
course = models.OneToOneField(Course, on_delete=models.CASCADE) #
# inside the course I want my APIVIEW to list only the courses that current user created.
# OnetoOne relationship does not solve the problem.
status = models.CharField(choices=STATUS, null=False, default=GOZLEMEDE,max_length=20)
tariffs = models.FloatField(max_length=5,null=False,default=0.00)
continues_off = models.CharField(max_length=2)
user_profile = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
My serializers for both Models:
class LessonSerializer(serializers.ModelSerializer):
class Meta:
model = models.Lesson
fields = ('course', 'status', 'tariffs', 'continues_off', 'user_profile')
def create(self, validated_data):
lesson = models.Lesson.objects.create(
course = validated_data['course'],
status = validated_data['status'],
tariffs=validated_data['tariffs'],
continues_off=validated_data['continues_off'],
user_profile=validated_data['user_profile']
)
return lesson
class CourseSerializer(serializers.ModelSerializer):
"""Serializers Course content"""
class Meta:
model = models.Course
fields = '__all__'
def create(self,validated_data):
course = models.Course.objects.create(
course_name = validated_data['course_name'],
description=validated_data['description'],
user_profile=validated_data['user_profile']
)
return course
My Viewset:
class LessonViewset(viewsets.ModelViewSet):
model = models.Lesson
serializer_class = serializers.LessonSerializer
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,BasePermission,)
def get_queryset(self):
user_current = self.request.user.id
return models.Lesson.objects.filter(user_profile=user_current)
How can I get the desired result. I want to get the courses for the current user and show them as a dropdown list in my API view. Just only the courses that user created should be in the dropdown list not all.
OnetoOne relationship gives all results of course table.
i think change your view code to :
def get_queryset(self,id):
return model.objects.filter(user_profile=id)
#You do not need to call it again when you put the Lesson on the model
\
On a project with Django / DRF; I have the following model structure:
class City(models.Model):
name = models.CharField(max_length=100)
class Company(models.Model):
city = models.ForeignKey(City)
.
.
And following serializer structure for Company model:
class CompanySerializer(serializers.ModelSerializer):
city_name = serializers.CharField(write_only=True)
.
.
class Meta:
model = Company
fields = ('city_name',)
def create(self, validated_data):
# Get city
city_name = validated_data.pop('city_name')
try:
city = City.objects.get(name__iexact=city_name)
except City.DoesNotExist:
city = City.objects.create(name=city_name.title())
company = Company.objects.create(city=city, **validated_data)
return company
While creating a company through the serializer, user provides a city_name, I create a new city with that name if not exists or use an existing entry if exists. In this structure, I want to be able to return city_name field while returning companies. It is not a field on the model, so I could use SerializerMethodField normally, but I also want this field to be writable as well. Do I have any options here?
i think, your solution is simple to add source and remove write_only:
city_name = serializers.CharField(source='city.name')
After changing to this approach, you can get city name in create or update methods as follows:
city_data = validated_data.pop('city')
city_name = city_data.get('name')
A better approach is to create two serializer
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = ('name')
class CompanySerializer(serializers.ModelSerializer):
city = CitySerializer(write_only=True)
class Meta:
model = Company
fields = ('city',)
def create(self, validated_data):
# Get city
city = validated_data.pop('city')
try:
city = City.objects.get(name__iexact=city)
except City.DoesNotExist:
city = City.objects.create(name=city.title())
company = Company.objects.create(city=city, **validated_data)
return company
models.py
class DailyRecordManager(models.Manager):
def get_query_set(self):
qs = super(DailyRecordManager, self).get_query_set().order_by('date_organised')
return qs
class DailyRecord(models.Model):
date_organised = models.DateField('Ogransied Date', help_text=('Enter Date when the program is organised: CCYY-MM-DD'))
program_name = models.TextField('program name',)
venue = models.CharField('venue', max_length = 255, blank=True)
organiser = models.ForeignKey(Organiser, verbose_name = 'Organiser', related_name = 'organisers')
objects = models.Manager()
public = DailyRecordManager()
class Meta:
verbose_name = 'dailyrecord'
verbose_name_plural = 'dailyrecords'
ordering = ['-date_organised']
def __str__(self):
return self.program_name
class Participant(models.Model):
participant = models.CharField(max_length= 50, unique = True)
daily_record = models.ForeignKey(DailyRecord, verbose_name = 'program_name')
class Meta:
verbose_name = 'participant'
verbose_name_plural = 'participants'
def __str__(self):
return self.participant
views.py
class DailyActivityPageView(SingleTableView):
table_class = DailyRecordTable
queryset = DailyRecord.public.all()
# queryset = Participant(DailyRecord.public.all()) is not working
template_name = 'dailyrecord/daily-activity-record.html'
tables.py
class DailyRecordTable(tables.Table):
date_organised = tables.Column('Date')
program_name = tables.Column( 'Program Name')
venue = tables.Column( 'Venue')
organiser = tables.Column( 'Organiser')
participant = tables.Column( 'dailyrecord.participant')
# daily = tables.RelatedLinkColumn()
class Meta:
model = DailyRecord
Now what I need is to display the data from participant table too, corresponding to the daily_record foreign key.
Click this link to view the snapshot. see the last column of the table. I need the data of Participant.partcipant column here
Sorry for poor English.
Thank You
There are two problems here.
First is, that a daily record can have multiple participants. Thus, in order to fill last column you have to aggregate all possible participants into that column (for example as list).
Second is, that you should make Participant backwards related to DailyRecord by adding attribute "related_name" to daily_record in your Participant model, like this:
daily_record = models.ForeignKey(DailyRecord, verbose_name = 'program_name', related_name="participants")
Now, you should simply get participants from daily_record like this:
participants = DailyRecord.public.first().participants.all()
If you had OneToOneField instead of ForeignKey you could add (single) participant to table like this:
participant = tables.Column( "Participant", accessor='participant')
The following problem is driving me real crazy ...
I have three models:
Order
Target
ConstructionSite
An Order has a "date" and "users" and a lot of "targets". The Target itself consists of some information and a key to a "ConstructionSite".
I'm trying to build a RESTful API with Angular. So far so good. I also know that writable nested serializers have to be build manually.
My Models:
class ConstructionSite(models.Model):
cost_unit_nr = models.CharField(primary_key=True)
class Order(models.Model):
date = models.DateField("Datum")
users = models.ManyToManyField(User)
class Target(models.Model):
cost_unit_nr = models.OneToOneField(ConstructionSite)
order = models.ForeignKey(Order, related_name="targets")
My Serializers*
class ConstructionSiteIDSerializer(serializers.HyperlinkedModelSerializer):
cost_unit_nr = serializers.CharField()
class Meta:
model = ConstructionSite
fields = ('cost_unit_nr',)
class TargetSerializer(serializers.ModelSerializer):
cost_unit_nr = ConstructionSiteIDSerializer()
id = serializers.IntegerField()
class Meta:
model = Target
fields = ('id', 'cost_unit_nr', 'services', 'sequence_number', 'published')
class OrderSerializer(serializers.ModelSerializer):
users = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True)
targets = TargetSerializer(many=True, required=True, read_only=False)
class Meta:
model = Order
fields = ('id', 'date', 'targets', 'users')
def update(self, instance, validated_data):
targets_data = validated_data.pop('targets')
users_data = validated_data.pop('users')
instance.date = validated_data.get('date', instance.date)
# User handling ... (not shown here)
# Targets
instance.targets = []
for target in targets_data:
cs_data = target.get('cost_unit_nr')
new_target, created = Target.objects.update_or_create(id=target.get('id', False))
cs_instance, created = ConstructionSite.objects.get_or_create(cost_unit_nr=cs_data.get('cost_unit_nr'))
new_target.cost_unit_nr = cs_instance
new_target.services = target.get('services')
new_target.published = target.get('published', False)
new_target.sequence_number = target.get('sequence_number', 0)
instance.targets.add(new_target)
instance.save()
return instance
Data
I'm trying to update an existing Order. It already has a Target and I want to add another one:
[{"id":13,
"date":"2015-12-03",
"targets":[
{
"id":15,
"cost_unit_nr": "cost_unit_nr":"3"},
"services":"whatever",
"sequence_number":0,
"published":false},
{
"cost_unit_nr": "cost_unit_nr":"4"},
"services":"still whatever",
"sequence_number":0,
"published":false}],
"users":[1]}]
cost_unit_nr is the id of the ConstructionSite object.
The 2nd entry of targets has no ID of course, it should be created when I call update_or_create ... But an IntegrityError is thrown:
('weekplans_target.cost_unit_nr_id may not be NULL',)
Where "weekplans" is the name of my app.
First: why?
Second: why target.cost_unit_nr_id? I don't have this field anywhere. Why is _id added to cost_unit_nr
Thanks guys!