I am new to Tastypie (and Django) and am running into a problem with circular many-to-many relationships in my app's api.
I have 3 models RiderProfile, Ride, and RideMemebership. RiderProfilea can belong to multiple Rides, and Rides can have multiple RiderProfile. The many-to-many relationship is mediated by RideMembership. My models look like:
class RiderProfile(models.Model):
user = models.OneToOneField(User)
age = models.IntegerField(max_length=2)
rides = models.ManyToManyField('riderapp.Ride', through="RideMembership")
def __unicode__(self):
return self.user.get_username()
class Ride(models.Model):
name = models.CharField(max_length=64)
riders = models.ManyToManyField(RiderProfile, through="RideMembership")
def __unicode__(self):
return self.name
class RideMembership(models.Model):
rider = models.ForeignKey(RiderProfile)
ride = models.ForeignKey(Ride)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
def __unicode__(self):
return self.rider.user.get_username() + ' to ' + self.ride.name()
My TastyPie resources look like:
class UserResource(ModelResource):
...
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileResource', 'riders', full=True)
class RiderProfileResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders'
user = fields.ForeignKey(UserResource, 'user', full=True)
rides = fields.ToManyField('riderapp.api.RideResource', 'rides', full=True)
When I GET either a RiderProfile or Ride (list or detail), I get a recursion error because the models are fetching themselves infinitely. I have tried using the RelationalField.use_in parameter, which is very close to what I am trying to accomplish - as it prevents a field from being included based whether the request is for a list or a detail. However, I am trying to remove a resource field based on which endpoint is called.
For instance, a request for /rides:
I would like to have a list of all the RiderProfile items involved, but without their Ride list.
Likewise, a request for /riders:
I would like to have a list of all the Ride items for the RiderProfile, but without their Rider list.
What is the recommended solution for this? I have been playing the with the dehyrdate cycle, but am struggling to modify the set of related resources. I have also read answers about using multiple ModelResources for Rides and Riders. Is there a recommended way to accomplish this?
Thanks in advance for your advice!
Update
I added extra ModelResources for use with each endpoint (RiderProfileForRideResource and RideForRiderProfileResource), and it is working. I am just not sure this is the best approach. It creates additional endpoints that I don't really want to expose. Any thoughts on a better way?
class UserResource(ModelResource):
...
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileForRideResource', 'riders', full=True)
class RideForRiderProfileResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides_for_riders'
class RiderProfileResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders'
user = fields.ForeignKey(UserResource, 'user', full=True)
rides = fields.ToManyField('riderapp.api.RideForRiderProfileResource', 'rides', full=True)
class RiderProfileForRideResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders_for_ride'
user = fields.ForeignKey(UserResource, 'user', full=True)
class RideMembershipResource(ModelResource):
class Meta:
queryset = RideMembership.objects.all()
resource_name = 'rider_membership'
This might not be the cleanest one way to do it but you could try to remove riders or rides in the dehydrate cycle by checking the resource uri path of the api call you have made
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileResource', 'riders', full=True)
def dehydrate(self, bundle):
# You make api call to 'riders' and are dehydrating related source RideResource. Path should be of the form API/app_name/riders
# When call made directly to this resource then uri path will be API/app_name/rides and if statement will not be entered
if 'riders' in bundle.request.path:
del bundle.data['riders']
and vice versa for the opposite relation.
You can use a callable for the use_in attribute of your resource field instead of overriding dehydrate.
def riders_check(bundle):
return 'riders' in bundle.request.path
Something like,
riders = fields.ToManyField('riderapp.api.RiderProfileForRideResource', 'riders', full=True, use_in=riders_check)
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.
Given these models:
class Product(models.Model):
name = CharField(max_length=255)
class Order(models.Model):
product = models.ForeignKey(Product)
user = models.ForeignKey(User)
quantity = models.PositiveIntegerField()
A user can only have a single Order object per product. I would like an API call to show a list of products, with the user's order where available. Is there a way to do that?
The default serialisation lists ALL orders under order_set. I did get a bit ahead with this to filter by user:
class FilteredOrderSerializer(serialisers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.context['request'].user)
return super(FilteredOrderSerializer, self).to_representation(data)
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
list_serializer_class = FilteredOrderSerializer
class ProductSerializer(serializers.ModelSerializer):
order_set = OrderSerializer(many=True, read_only=True)
class Meta:
model = Product
So now it only shows the authenticated user's order, but the resulting JSON looks something like
[
{
"name": "prod1",
"order_set": [
{
"quantity": 4
}
],
}
]
i.e. the field is still called order_set and it's a list, while I would like it to be called order and be either an object or null. So I'm not sure where this filtering should take place, nor how to define the field.
Edit: I'm using a simple viewset
class ProductViewSet(view sets.ReadOnlyModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
You need to add related_name field in your orders so you can
product.orders.all()
Then this should do.
class Order(models.Model):
product = models.ForeignKey(Product, related_name='orders')
user = models.ForeignKey(User)
quantity = models.PositiveIntegerField()
class ProductSerializer(serializers.ModelSerializer):
order = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ('name', 'order')
def get_order(self, object):
try:
order = object.orders.get(user=self.context['request'].user)
return SceneDetailSerializer(order).data
except Order.DoesNotExist:
return None
Update: You can try out serializer method field. Not sure if self contains context and user references in it. You need to remove listfield from your order serializer. Let me know if it works.
I have a Cart model and a CartItem model. The CartItem model has a ForeignKey to the Cart model.
Using Django Rest Framework I have a view where the API user can display the Cart, and obviously then I want to include the CartItem in the respone.
I set up my Serializer like this:
class CartSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
cartitem_set = CartItemSerializer(read_only=True)
class Meta:
model = Cart
depth = 1
fields = (
'id',
'user',
'date_created',
'voucher',
'carrier',
'currency',
'cartitem_set',
)
My problem is the second line, cartitem_set = CartItemSerializer(read_only=True).
I get AttributeErrors saying 'RelatedManager' object has no attribute 'product'. ('product' is a field in the CartItem model. If I exclude product from the CartItemSerializer I just get a new AttributeError with the next field and so on. No matter if I only leave 1 or all fields in the Serializer, I will get a error.
My guess is that for some reason Django REST Framework does not support adding Serializers to reverse relationships like this. Am I wrong? How should I do this?
PS
The reason why I want to use the CartItemSerializer() is because I want to have control of what is displayed in the response.
Ahmed Hosny was correct in his answer. It required the many parameter to be set to True to work.
So final version of the CartSerializer looked like this:
class CartSerializer(serializers.ModelSerializer):
cartitem_set = CartItemSerializer(read_only=True, many=True) # many=True is required
class Meta:
model = Cart
depth = 1
fields = (
'id',
'date_created',
'voucher',
'carrier',
'currency',
'cartitem_set',
)
It's important to define a related name in your models, and to use that related name in the serializer relationship:
class Cart(models.Model):
name = models.CharField(max_length=500)
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='cart_items')
items = models.IntegerField()
Then in your serializer definition you use those exact names:
class CartSerializer(serializers.ModelSerializer):
cart_items = CartItemSerializer(read_only=True)
class Meta:
model = Cart
fields = ('name', 'cart_items',)
It would be wise to share your whole code, that is model and serializers classes. However, perhaps this can help debug your error,
My serializer classes
class CartItemSerializer(serializers.ModelSerializer):
class Meta:
model = CartItem
fields = ('id')
class CartSerializer(serializers.ModelSerializer):
#take note of the spelling of the defined var
_cartItems = CartItemSerializer()
class Meta:
model = Cart
fields = ('id','_cartItems')
Now for the Models
class CartItem(models.Model):
_cartItems = models.ForeignKey(Subject, on_delete=models.PROTECT)
#Protect Forbids the deletion of the referenced object. To delete it you will have to delete all objects that reference it manually. SQL equivalent: RESTRICT.
class Meta:
ordering = ('id',)
class Cart(models.Model):
class Meta:
ordering = ('id',)
For a detailed overview of relationships in django-rest-framework, please refer their official documentation
In Django Rest Framework, how do you filter a serializer when it's nested in another serializer?
My filters are imposed in the DRF viewsets, but when you call a serializer from inside another serializer, the viewset of the nested serializer never gets called, so the nested results appear unfiltered.
I have tried adding a filter on originating viewset, but it doesn't seem to filter the nested results because the nested results get called as a separate pre-fretched query. (The nested serializer is a reverse lookup, you see.)
Is it possible to add a get_queryset() override in the nested serializer itself (moving it out of the viewset), to add the filter there? I've tried that, too, with no luck.
This is what I tried, but it doesn't even seem to get called:
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
def get_queryset(self):
query = super(QuestionnaireSerializer, self).get_queryset(instance)
if not self.request.user.is_staff:
query = query.filter(user=self.request.user, edition__hide=False)
return query
You can subclass the ListSerializer and overwrite the to_representation method.
By default the to_representation method calls data.all() on the nested queryset. So you effectively need to make data = data.filter(**your_filters) before the method is called. Then you need to add your subclassed ListSerializer as the list_serializer_class on the meta of the nested serializer.
subclass ListSerializer, overwriting to_representation and then calling super
add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer
Here is the relevant code for your sample.
class FilteredListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.context['request'].user, edition__hide=False)
return super(FilteredListSerializer, self).to_representation(data)
class EditionSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredListSerializer
model = Edition
class QuestionnaireSerializer(serializers.ModelSerializer):
edition = EditionSerializer(read_only=True)
company = serializers.StringRelatedField(read_only=True)
class Meta:
model = Questionnaire
While all the above answers work, I find the use of Django's Prefetch object the easiest way of all.
Say a Restaurant obj has a lot of MenuItems, some of which are is_removed == True, and you only want those that are not removed.
In RestaurantViewSet, do something like
from django.db.models import Prefetch
queryset = Restaurant.objects.prefetch_related(
Prefetch('menu_items', queryset=MenuItem.objects.filter(is_removed=False), to_attr='filtered_menu_items')
)
In RestaurantSerializer, do something like
class RestaurantSerializer(serializers.ModelSerializer):
menu_items = MenuItemSerializer(source='filtered_menu_items', many=True, read_only=True)
Tested many solutions from SO and other places.
Found only one working solution for Django 2.0 + DRF 3.7.7.
Define a method in model which has nested class. Craft a filter that will fit your needs.
class Channel(models.Model):
name = models.CharField(max_length=40)
number = models.IntegerField(unique=True)
active = models.BooleanField(default=True)
def current_epg(self):
return Epg.objects.filter(channel=self, end__gt=datetime.now()).order_by("end")[:6]
class Epg(models.Model):
start = models.DateTimeField()
end = models.DateTimeField(db_index=True)
title = models.CharField(max_length=300)
description = models.CharField(max_length=800)
channel = models.ForeignKey(Channel, related_name='onair', on_delete=models.CASCADE)
.
class EpgSerializer(serializers.ModelSerializer):
class Meta:
model = Epg
fields = ('channel', 'start', 'end', 'title', 'description',)
class ChannelSerializer(serializers.ModelSerializer):
onair = EpgSerializer(many=True, read_only=True, source="current_epg")
class Meta:
model = Channel
fields = ('number', 'name', 'onair',)
Pay attention to source="current_epg" and you'll get the point.
I find it easier, and more straight forward, to use a SerializerMethodField on the serializer field you want to filter.
So you would do something like this.
class CarTypesSerializer(serializers.ModelSerializer):
class Meta:
model = CarType
fields = '__all__'
class CarSerializer(serializers.ModelSerializer):
car_types = serializers.SerializerMethodField()
class Meta:
model = Car
fields = '__all__'
def get_car_types(self, instance):
# Filter using the Car model instance and the CarType's related_name
# (which in this case defaults to car_types_set)
car_types_instances = instance.car_types_set.filter(brand="Toyota")
return CarTypesSerializer(car_types_instances, many=True).data
This saves you from having to create many overrides of the serializers.ListSerializer if you need different filtering criteria for different serializers.
It also has the extra benefit of seeing exactly what the filter does within the serializer instead of diving into a subclass definition.
Of course the downside is if you have a serializer with many nested objects that all need to be filtered in some way. It could cause the serializer code to greatly increase. It's up to you how you would like to filter.
Hope this helps!
When a serializer is instantiated and many=True is passed, a
ListSerializer instance will be created. The serializer class then
becomes a child of the parent ListSerializer
This method takes the target of the field as the value argument, and
should return the representation that should be used to serialize the
target. The value argument will typically be a model instance.
Below is the example of the nested serializer
class UserSerializer(serializers.ModelSerializer):
""" Here many=True is passed, So a ListSerializer instance will be
created"""
system = SystemSerializer(many=True, read_only=True)
class Meta:
model = UserProfile
fields = ('system', 'name')
class FilteredListSerializer(serializers.ListSerializer):
"""Serializer to filter the active system, which is a boolen field in
System Model. The value argument to to_representation() method is
the model instance"""
def to_representation(self, data):
data = data.filter(system_active=True)
return super(FilteredListSerializer, self).to_representation(data)
class SystemSerializer(serializers.ModelSerializer):
mac_id = serializers.CharField(source='id')
system_name = serializers.CharField(source='name')
serial_number = serializers.CharField(source='serial')
class Meta:
model = System
list_serializer_class = FilteredListSerializer
fields = (
'mac_id', 'serial_number', 'system_name', 'system_active',
)
In view:
class SystemView(viewsets.GenericViewSet, viewsets.ViewSet):
def retrieve(self, request, email=None):
data = get_object_or_404(UserProfile.objects.all(), email=email)
serializer = UserSerializer(data)
return Response(serializer.data)
The following worked for me, from self.context['view'], You can get the filter params inside the serializer and use it however you want.
class ShipmentDocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ['id', 'created_date', 'consignment', 'document', 'org', 'title' ]
class ShipmentDocumentTypeSerializer(serializers.ModelSerializer):
documents = serializers.SerializerMethodField()
class Meta:
model = DocumentType
fields = ['id', 'type', 'documents']
def get_documents(self, instance):
consignment_id=self.context['view'].kwargs['consignment_id']
queryset = Document.objects.filter(consignment__id=consignment_id)
return ShipmentDocumentSerializer(queryset, many=True).data
Lets say I have three models Submission, Contact and SubmissionContact.
class Submission(models.Model):
title = models.CharField(max_length=255, verbose_name='Title')
...
class Contact(models.Model):
name = models.CharField(max_length=200, verbose_name='Contact Name')
email = models.CharField(max_length=80, verbose_name='Contact Email')
...
class SubmissionContact(models.Model):
submission = models.ForeignKey(Submission)
contact = models.Foreign(Contact, verbose_name='Contact(s)')
Can I read / write to all these three tables using a single ModelResource using tastypie. (basically get and put actions on list and detail in tastypie)
Thanks in advance for any help.
You can nest one model into the other or use the dehydrate cycle to add extra resources to your output, for example consider a Foo and Bar model
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all()
resource_name = 'foo'
serializer = Serializer(formats=['xml', 'json'])
excludes = ['date_created']
class BarResource(ModelResource):
foo = fields.ForeignKey(FooResource, attribute='foo', full=True, null=True)
class Meta:
queryset = Bar.objects.all()
resource_name = 'bar'
serializer = Serializer(formats=['xml', 'json'])
If there's no relationship you could also do something like (with large datasets this would cause a lot of database overhead, you might have to rethink your model definitions):
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all()
resource_name = 'foo'
serializer = Serializer(formats=['xml', 'json'])
excludes = ['date_created']
def dehydrate(self, bundle):
obj = self.obj_get(id=bundle.data['id'])
bundle.data['bar'] = Bar.objects.get(id=1).name
return bundle