Optionally retrieve related items in Django REST framework - python

Suppose I have these models:
class House(models.Model):
name = models.CharField(max_length=50)
# Other attributes
class Booking(models.Model):
house = models.ForeignKey(House, on_delete=models.CASCADE)
arrival_date = models.DateField()
departure_date = models.DateField()
Serializers:
class HouseSerializer(serializers.ModelSerializer):
class Meta:
model = House
fields = ('id','name')
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = ('id',
'arrival_date',
'departure_date',
'house')
As you can see, bookings are associated to houses.
Users can request information about a house through "/house/:houseId", and bookings through "/booking/:bookingId".
I'd like to return all bookings related to a house when a user requests "/house/bookings", but these should NOT be returned when simply requesting "/house/bookings" as this is a relatively expensive operation and not usually needed.
I know how to make them be returned with the house, but how to make this optional. How do I do that?

First, it makes more sense (to me at least) to have your endpoint for bookings for a given house exist at /house/:houseId/bookings/ since the namespace at /house/bookings/ will already be looking for an ID.
You can also serialize the relationship between a House and Booking to display bookings for a house at the house detail endpoint. Something like:
class HouseSerializer(serializers.ModelSerializer):
bookings = BookingSerializer(many=True)
class Meta:
model = House
fields = ('id','name', 'bookings',)
But if you want another endpoint, just create a view that takes the BookingSerializer and filters the queryset by the House ID in the kwargs:
(Again, assuming your endpoint is /house/<house_id>/bookings/)
class BookingsForHouseListView(ListAPIView):
serializer_class = BookingSerializer
def get_queryset(self):
return Bookings.objects.filter(house__id=self.kwargs['house_id'])

Maybe you can do two serializers, one to the List of houses without showing the books, and another for the individual house, showing the books.
In the views you define the list serializer for the list uri, and the individual serializer for the individual uri.

Turns out, I was trying with too high-level parts of the API that restricted me more than they were helping me, I needed to do some things manually.
This answer https://stackoverflow.com/a/17763864/1582024 shows how to implement a hierarchy of resources.

Related

Order queryset by the number of foreign key instances in a Django field

I am trying to return the objects relating to a through table which counts the number of reactions on a blog post.
I have an Article model, Sentiment model and Reactions model. The sentiment is simply a 1 or 2, 1 representing like and 2 for dislike. On the frontend users can react to an article and their reactions are stored in a Reactions table.
Reactions model
class Reaction(models.Model):
user_id = models.ForeignKey(User, related_name='user_id', on_delete=models.CASCADE)
article_id = models.ForeignKey(Article, related_name='article_id', on_delete=models.CASCADE)
sentiment = models.ForeignKey(Sentiment, related_name='sentiment', on_delete=models.CASCADE)
I'd like to find the 2 most liked articles so I have written a view to handle the GET request
views.py
class MostPopularView(generics.RetrieveAPIView):
queryset = Reaction.objects.annotate(num_likes = Count('sentiment_id')).order_by('num_likes')
serializer_class = MostPopularSerializer
and a serializer to transform the data
serializers.py
class MostPopularSerializer(serializers.Serializer):
class Meta:
fields = (
'id',
'title',
)
model = Article
As the code stands now, I'm getting a response
<QuerySet [<Reaction: d745e09b-5685-4592-ab43-766f47c73bef San Francisco Bay 1>, <Reaction: d745e09b-5685-4592-ab43-766f47c73bef The Golden Gate Bridge 1>, <Reaction: dd512e6d-5015-4a70-ac42-3afcb1747050 San Francisco Bay 1>, <Reaction: dd512e6d-5015-4a70-ac42-3afcb1747050 The Golden Gate Bridge 2>]>
Showing San Francisco Bay has 2 likes and The Golden Gate Bridge has 1 like and 1 dislike.
I've tried multiple methods to get the correct response including filtering by sentiment=1 but can't get any further than this.
What I'm looking for is a way to count the number of sentiment=1 fields which correspond to each article id and order them in descending order, so most liked at the top.
Edit
I've rethought my approach although I have not yet found a solution
Filter Reaction table by sentiment=1
Order by count of article_id
Serialize with MostPopularSerializer
I changed the View to be a ModelViewSet
class MostPopularView(viewsets.ModelViewSet):
articles = Reaction.objects.filter(sentiment=1).annotate(num_likes = Count('article_id')).order_by('num_likes')[:4]
# queryset = Article.objects.filter(id=articles['article_id'])
#Doesn't work by hypothetically what I'm thinking
for article in articles:
queryset = Article.objects.filter(id=article['article_id'])
serializer_class = MostPopularSerializer
And the serializer to be a ModelSerializer
class MostPopularSerializer(serializers.ModelSerializer):
class Meta:
fields = (
'id',
'title',
'tags',
)
model = Article
and an updated URL for good measure
path('popular', views.MostPopularView.as_view({'get': 'list'}))
Any tips on achieving these steps would be much appreciated, thank you
It makes no sense to use the Reaction as queryset, you use the Article as queryset, so:
from django.db.models import Case, Value, When
class MostPopularView(generics.RetrieveAPIView):
queryset = Article.objects.annotate(
sentiment=Sum(
Case(
When(article_id__sentiment_id=1, then=Value(1)),
When(article_id__sentiment_id=2, then=Value(-1)),
)
)
).order_by('-sentiment')
serializer_class = MostPopularSerializer
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
Note: Normally one does not add a suffix …_id to a ForeignKey field, since Django
will automatically add a "twin" field with an …_id suffix. Therefore it should
be user, instead of user_id.
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the User model to the Reaction
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the user relation to reactions.
I solved a simular Problem a different way.
For me I wanted to sort a queryset of Person by how often the Country was used.
I added a property to the Model
class Country(models.Model):
.
.
def _get_count(self):
count = len(Person.objects.filter(country=self.id))
return count or 0
count = property(_get_count)
In the View I have this queryset
qs = sorted(Country.objects.all(), key=lambda country: country.count*-1)
I needed to use python sorted because the django qs.order_by can not sort by property.
The *-1 is for descending order

DJango rest framework - API list using filter field from related models

Hi I'm new to Django and the Django rest framework so my terminology may be off.
I'm trying to build an API that gives back a list of items from a model but filtered based on fields in another related model.
I'll provide my current view and serializer classes and models
class service(models.Model):
name = models.CharField(max_length=50)
vendor = models.CharField(max_length=50)
version = models.CharField(max_length=10)
registration_status = models.BooleanField(default=False)
class service_network(models.Model):
service = models.OneToOneField(
service,
related_name='network',
on_delete=models.CASCADE,
primary_key=True,
)
forwarded_port = models.CharField(max_length=50)
class ServiceNetworkSerializer(serializers.ModelSerializer):
class Meta:
model = service_network
fields = '__all__'
class ServiceSerializer(serializers.ModelSerializer):
network = ServiceNetworkSerializer()
class Meta:
model = service
fields = [
'id',
'name',
'vendor',
'version',
'registration_status',
'network',
]
class ServiceAPI(ModelViewSet):
queryset = service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'
Currently I can get back lists using a URL query string
{{baseUrl}}/engine/service?registration_status=true
What I want to do is something like this
{{baseUrl}}/engine/service/network?forwarded_port=8080
Which I would expect to give back a list of services where the related network field "forwarded_port" is equal to 8080.
Is there another way to query this API? Maybe using a POST with a body containing the query? If there something in the DOCS that I can read, I've tried to look through filtering and querysets but I wasn't able to find anything that would do this out of the box
I'm also new to stackoverflow and I've tried to keep my question short with as much relevant information so if there anything missing I'd be happy to edit my question
I was able to solve this using the following queryset override
def get_queryset(self):
if len(self.request.GET) > 0:
query_set = {}
for query in self.request.GET:
query_set[query] = self.request.GET.get(query)
return service.objects.filter(**query_set)
else:
return service.objects.all()
What this does is lets you filter fields without explicitly specifying what they are, in cases when you have many fields that need filtering. I also have to say as I'm not experienced with Django, I'm not sure what kind of errors this may bring up but its a hack that's worked for me. If I find this is really bad I'll come back and remove this.
Try this:
{{baseUrl}}/engine/service?network__forwarded_port=8080
Probably it works.
Doc: https://docs.djangoproject.com/en/dev/topics/db/queries/#lookups-that-span-relationships-1
EDIT:
If the above answer doesn't work, you can change the ServiceApi class and filter by yourself:
class ServiceAPI(ModelViewSet):
def get_queryset(self):
if self.request.GET.get(network__forwarded_port)
return service.objects.filter(network__forwarded_port = self.request.GET.get(network__forwarded_port))
else:
return service.objects.all()
serializer_class = ServiceSerializer
filterset_fields = '__all__'

Django Rest Framework creating child objects in parent serializer using child serializer

Supposing some standard Django relational setup like this:
models.py
class Book(models.Model):
title = models.CharField(max_length=30)
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
I'd like to create a book and all its pages with one request. If we start with serializers like this:
serializers.py
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = '__all__'
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'pages')
pages = PageSerializer(many=True)
Then the problem is that the PageSerializer now requires a book foreign key. But I don't know the key of the book until I've created the book, which is only after I've sent the POST request. So I cannot include the book pk in the POST data that the client sends.
An obvious solution is to override the create function on the Book serializer. But then I am still faced with the problem that the validators will say that the book field is required and the POST data will fail to validate.
I could make book a not-required field on the PageSerialzer. But this seems very bad. The book field IS required. And the BookSerializer create method will be able to supply it. It's just the client that doesn't know it.
So my suspicion is that the best way to do this is to leave book as required on the PageSerializer, but somehow make it so that the validators on the BookSerializer don't check for whether that is in the POST data when I post to BookSerializer.
Is this the correct way to achieve what I want? And if so, how do I do it? Thank you.
Why not try handling it in the create viewset. You can validate the data for the Book object first, before creating it. Then validate the data for the Page object using the created Book object and the other data sent from the request to the page.
I'd link your ViewSet to a BookCreateSerializer, and from this specific serializer I'd then add a function to not only verify the received data but make sure you link the parent's id to the child's one during creation.
IMPORTANT NOTE
This works if a parent only has one child, not sure about when passing multiple children.
Here is what is could look like.
BookCreateSerializer:
class BookCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Book model in DB
"""
pages = PageCreateSerializer()
class Meta:
model = Book
fields = [
'title',
'pages'
]
def create(self, validated_data):
page_data = validated_data.pop('page')
book = Book.objects.create(**validated_data)
Page.objects.create(book=book, **page_data)
return book
PageCreateSerializer
class PageCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Page model in DB
"""
class Meta:
model = Page
fields = [
'book',
'text'
]
To make sure that your Book instance understands what a page field is in the serializer, you have to define a related_name in its child's Model (Page). The name you choose is up to you. It could look like:
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='page')
text = models.CharField(max_length=100)

How to limit the queryset of related fields of serializer based on some request parameters in Django Rest Framework

I have three models Transaction, Business, and Location. They are defined as follows:
class Business(models.Model):
# Can have zero or more locations. A user can have many businesses.
name = models.CharField(max_length=200, validators=[MinLengthValidator(1)])
# ... and some other fields ...
class Location(models.Model):
# Must have one business. Locations cannot exist without a business
suburb = models.CharField(max_length=150, validators=[MinLengthValidator(1)])
business = models.ForeignKey(Business, related_name='locations')
# ... and some other fields ...
class Transaction(models.Model):
# Can have zero or one business
# Can have zero or one location and the location must belong to the business. If business is empty, location must be empty
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
# ... and some other fields ...
And the serializers:
class BusinessRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
owner = get_owner_from_context(self.context)
return Business.objects.filter(owner=owner)
class LocationRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
params = self.context['request'].query_params
business_params = params.get('business')
if business_params is not None:
owner = get_owner_from_context(self.context)
return Location.objects.filter(owner=owner, business=business_params)
else:
return None
class TransactionSerializer(serializers.ModelSerializer):
business = BusinessRelatedField(required=False, allow_null=True)
location = LocationRelatedField(required=False, allow_null=True)
The problem I was facing was that I didn't know how to restrict the value of Location based on the value of Business. I was manually performing this check inside TransactionSerializer's validate method until it occurred to me to create a PrimaryKeyRelatedField subclass and override the get_queryset method. This seemed like a better approach to me (and it's actually working) but I'd like to know if this is the 'normal' way of doing it.
The other problem I'm now facing is that the 'browsable API' no longer shows any choices for Location which I feel is a hint that I might be doing something wrong.
Any help would be appreciated.
You can override the get_fields() method of the serializer and modify the queryset for business and location fields to the desired values.
get_fields() method is used by the serializer to generate the field names -> field instances mapping when .fields property is accessed on it.
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transaction
fields = (.., business, transaction)
def get_fields(self):
# get the original field names to field instances mapping
fields = super(TransactionSerializer, self).get_fields()
# get the required parameters
owner = get_owner_from_context(self.context)
business_params = self.context['request'].query_params.get('business')
# modify the queryset
fields['business'].queryset = Business.objects.filter(owner=owner)
fields['location'].queryset = Location.objects.filter(owner=owner, business=business_params)
# return the modified fields mapping
return fields
This is a very late answer, however it would not be different back then.
With the information you provided (in the comments as well) and AFAIK there is no way of doing this unless you manipulate the javascript code of the browsable API's templates and add ajax calling methods to it.
DRF browsable API and DRF HTML and forms may help.

Query Django model using name from another model

I'm building a project that involves a list of people and the transactions they've made. Each person will have their own profile with x amount of transactions.
My database, so far, consists of two models:
One to define a person
class Person (models.Model):
"""
A person
"""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100, blank=True)
def __unicode__(self):
return self.name
One to associate a transaction each person made.
class Purchase (models.Model):
"""
A purchase for each person
"""
person_id = models.ForeignKey(Person)
purchase_description = models.CharField(max_length=1000, blank=True)
I determined this would be a many-to-one database relationship because some people might make the same transaction.
Now, I can make pages for each person, showing their name in the header. What I'd like to do is show the transactions made to that particular person. How can this be done? Here's what my view.py file looks like:
from django.shortcuts import render
from .models import Person, Purchase
def load_person(request, name):
person = Person.objects.get(name=name)
# name = Purchase.objects.filter(purchase)
context = {
'name': name
# 'purchase': purchase
}
return render(request, 'pages/person.html', context)
Here's the url associated to the query in my url.py file.
url(r'^project/(?P<name>[-\w]+)/$', views.load_person),
person = Person.objects.get(name=name)
purchases = Purchase.objects.filter(person_id=person)
Sounds like you just started using django, there are several things that are not best practice, they are:
You don't need to define id in Person, django will define it for you.
Try not to use person_id as field name, use person instead. Django model is not relational database design, think of the models as separate entities.

Categories

Resources