Django Rest Ordering custom - python

I'm using django rest framework and I would like to order with a custom method.
So, when I call this url for example : http://127.0.0.1:8000/api/sets/?ordering=partMissing
It's possible to call a custom method because I would like to order with the number of part missing by snippet. I made count the sum of number of part in the many to many field.

I have very simple POC, that should allow you to implement more sophisticated solution.
views.py:
from rest_framework import viewsets
from ordering_test.models import Test
from ordering_test.ordering import MyCustomOrdering
from ordering_test.serializers import TestSerializer
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
filter_backends = (MyCustomOrdering,)
ordering.py:
from rest_framework.filters import OrderingFilter
class MyCustomOrdering(OrderingFilter):
allowed_custom_filters = ['testMethod']
def get_ordering(self, request, queryset, view):
"""
Ordering is set by a comma delimited ?ordering=... query parameter.
The `ordering` query parameter can be overridden by setting
the `ordering_param` value on the OrderingFilter or by
specifying an `ORDERING_PARAM` value in the API settings.
"""
params = request.query_params.get(self.ordering_param)
if params:
fields = [param.strip() for param in params.split(',')]
# care with that - this will alow only custom ordering!
ordering = [f for f in fields if f in self.allowed_custom_filters]
if ordering:
return ordering
# No ordering was included, or all the ordering fields were invalid
return self.get_default_ordering(view)
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request, queryset, view)
if ordering:
# implement a custom ordering here
ordering = ['-id']
if ordering:
return queryset.order_by(*ordering)
return queryset
The models.py and serializers.py are straightforward, but I will still include them here:
models.py:
from django.db import models
class Test(models.Model):
test = models.CharField(max_length=120)
test1 = models.CharField(max_length=120)
serializers.py:
from rest_framework import serializers
from ordering_test.models import Test
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = Test
fields = ('test', 'test1')
Happy coding!

I think a much easier approach to opalczynski's solution would be to introduce a new field for custom ordering:
from django import forms
import django_filters
from rest_framework import serializers
from .models import MyModel
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ('field1',)
class CustomOrderingFilter(django_filters.FilterSet):
order_by = django_filters.BooleanFilter(
widget=forms.HiddenInput(),
method='filter_order_by',
)
class Meta:
model = MyModel
fields = [
'order_by'
]
def filter_order_by(self, queryset, name, value):
if value:
return self.Meta.model.objects.filter(
id__in=queryset.values_list('id', flat=True)
).order_by(value)
return queryset
class TestViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_class = CustomOrderingFilter
Then you can easily order by any field you want like this: example.com/api/mymodel/?order_by=partMissing.
In my example, I used a fixed model field, but you can change the way you order in the filter_order_by method on the CustomOrderingFilter. Just change it to the logic you want, but make sure to use the .filter(=queryset.values_list('id', flat=True)) to ensure that other filters that are set, are being used.

You can use FilterSet to annotate and then OrderingFilter to order accordingly. As an advantage you may use the OrderingField syntax and can still order by multiple fields ad the same time.
/api/?annotate_related={id}&order=subscribed
/api/?annotate_related={id}&order=-subscribed
/api/?annotate_related={id}&order=-subscribed,-modified
FilterSet:
class YourFilterSet(FilterSet):
annotate_related = filters.NumberFilter(method="_annotate_related")
class Meta:
model = Model
def _annotate_related(self, queryset, key, value, *args, **kwargs):
# eg. annotate if user belongs to a certain category
return queryset.annotate(is_subscribed=Case(When(annotate_related__id=value, then=1), output_field=IntegerField(), default=0))
ViewSet:
class YourViewSet(ModelViewSet):
queryset = Model.objects.all()
filterset_class = YourFilterSet
filter_backends = [OrderingFilter, DjangoFilterBackend]
ordering_fields = [
"is_subscribed", # order by annotated field
]

Related

How to check if a relation exists? M2M

I have models:
class User(models.Model):
...
group = models.ManyToManyField(
Group, related_name="users_group",
)
class Group(models.Model):
....
How to check in serializer is the Group empty (there is no relationship with User)
My version is :
class GroupSerializer(serializers.ModelSerializer):
empty = serializers.SerializerMethodField()
class Meta:
...
def get_empty(self, obj):
return not User.objects.filter(group=obj).exists()
But maybe there is an even better way.
If you define a ManyToManyModel [Django-doc], Django automatically defines one in reverse with the value for the related_name=… parameter [Django-doc] as name, so you can use:
class GroupSerializer(serializers.ModelSerializer):
empty = serializers.SerializerMethodField()
class Meta:
# …
def get_empty(self, obj):
return not group.users_group.exists()
But this is not very efficient if you want to serialize a large number of Groups: for each Group it will make an extra query. You can use a BooleanField:
class GroupSerializer(serializers.ModelSerializer):
empty = serializers.BooleanField()
class Meta:
# …
and then in the APIView or the ViewSet use as QuerySet a QuerySet where you make an annotation with an Exists subquery [Django-doc] to check if there exists a user for that Group:
from django.db.models import Exists, OuterRef
from rest_framework import viewsets
class MyModelViewSet(viewsets.ModelViewSet):
queryset = Group.objects.annotate(
empty=~Exists(
User.objects.filter(group=OuterRef('pk'))
)
)
# …

django-rest-framework- Filtering using 'or' on multiple values from one url parameter

I have a tagging system in place for a model that my API exposes. The models look something like this:
class TaggableModel(models.Model):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag, related_name="taggable_models")
class Tag(models.Model):
tag = models.CharField(max_length=32)
I've then set up a serializer and view that look like:
class TaggableModelSerializer(serializers.ModelSerializer):
class Meta:
model = TaggableModel
fields = ('id', 'name', 'tags',)
read_only_fields = ('id',)
class TaggableModelViewSet(viewsets.ModelViewSet):
queryset = TaggableModel.objects.all()
serializer_class = TaggableModelSerializer
permission_classes = (AllowAny,)
filter_backend = [DjangoFilterBackend]
filterset_fields = ['tags']
If I want to grab all TaggableModels that have tag ids 1, 2, or 3, I can do so via:
https://my-api-domain/api/taggable-models?tags=1&tags=2&tags=3
Is there a way to split on a delimiter, so I can pass it all as one parameter? e.g.:
https://my-api-domain/api/taggable-models?tags=1,2,3
It looks like I can write my own custom DjangoFilterBackend filters, but I am a bit unsure as to where to start. Or perhaps there is an easier way to accomplish this?
Sure you can do this by having custom filterset class with specific field 'widget' (that's how it is called in django-filters)
Here's a sample you can try:
# filters.py
from django_filters.rest_framework import FilterSet, filters
from django_filters.widgets import CSVWidget
from .your_models import Tag, TaggableModel
class TaggableModelFilterSet(FilterSet):
tags = filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(), widget=CSVWidget,
help_text=_("A list of ids, comma separated, identifying tags"),
method='filter_tags'
)
class Meta:
model = TaggableModel
fields = ['tags']
def filter_tags(self, queryset, name, value):
if value:
queryset = queryset.filter(tags__in=value)
return queryset
# views.py
class TaggableModelViewSet(viewsets.ModelViewSet):
queryset = TaggableModel.objects.all()
serializer_class = TaggableModelSerializer
permission_classes = (AllowAny,)
filter_backends = [DjangoFilterBackend]
filter_class = TaggableModelFilterSet
There is an even simpler way to achieve this using the django-filter package. Deep within the django-filter documentation, it mentions that you can use "a dictionary of field names mapped to a list of lookups".
Your code would be updated like so:
# views.py
from django_filters.rest_framework import DjangoFilterBackend
class TaggableModelViewSet(viewsets.ModelViewSet):
queryset = TaggableModel.objects.all()
serializer_class = TaggableModelSerializer
permission_classes = (AllowAny,)
filter_backend = [DjangoFilterBackend]
filterset_fields = {
'tags': ["in", "exact"] # note the 'in' field
}
Now in the URL you would add __in to the filter before supplying your list of parameters and it would work as you expect:
https://my-api-domain/api/taggable-models?tags__in=1,2,3
The django-filter documentation on what lookup filters are available is quite poor, but the in lookup filter is mentioned in the Django documentation itself.

Django filter by serializer method field

I cant create some logic(for me it interesting). For example i have View like this:
class DucktList(generics.ListAPIView):
serializer_class = DuckSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('test_field',) // i want to create some custom field and filter by it if needed.
serializer:
class DuckSerializer(serializers.ModelSerializer):
test_field = SerializerMethodField() // i want filter by this field!
def get_test_field(self, obj):
return True
class Meta:
......
How i can filter filter_fields with test_field ?
Perhaps you could define your own filter with custom methods and using this library django-filters:
from django_filters import rest_framework as filters
class EventFilter(filters.FilterSet):
finish_on = filters.BooleanFilter(name='finish_on', method='filter_manifestation')
begin_on = filters.BooleanFilter(name='begin_on', method='filter_manifestation')
def filter_manifestation(self, queryset, name, value):
if value is False:
lookup = '__'.join([name, 'gte'])
else:
lookup = '__'.join([name, 'lte'])
qs = queryset.filter(**{lookup: timezone.now()})
return qs
class Meta:
model = Event
fields = [
'finished', 'has_begun'
]
And add this filter to your view:
class ManifestationViewSet(viewsets.ReadOnlyModelViewSet):
...
filter_class = EventFilter
...
You could then adapt your filter's customs methods depending on what you want to do in the related function of your serializer custom field.
You have some snippets on the django-filter library doc about various filters types.

Filter queryset for nested serializer in django rest framework

Here is my view:
class SectorListAPI(generics.ListAPIView):
queryset = SectorModel.objects.all()
serializer_class = SectorSerializer
Here is my serializers:
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = GroupProfile
fields = ('title','slug',)
class DepartmentSerializer(serializers.ModelSerializer):
organizations = OrganizationSerializer(many=True, read_only=True)
class Meta:
model = DepartmentModel
fields = ('title', 'organizations',)
class SectorSerializer(serializers.ModelSerializer):
# title = serializers.CharField()
departments = DepartmentSerializer(many=True, read_only=True)
class Meta:
model = SectorModel
fields = ('title','departments',)
Look, here 'SectorSerializer' is parent 'DepartmentSerializer' is children and 'OrganizationSerializer' is grand children serializer. Now in my view I can easily filter my queryset for 'SectorModel'. But how can i filter on 'GroupProfile' model.
You might want to filter the queryset to ensure that only results relevant to the currently authenticated user making the request are returned.
You can do so by filtering based on the value of request.user.
For example:
from myapp.models import Purchase
from myapp.serializers import PurchaseSerializer
from rest_framework import generics
class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases
for the currently authenticated user.
"""
user = self.request.user
return Purchase.objects.filter(purchaser=user)
EDIT
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.
1- subclass ListSerializer, overwriting to_representation and then calling super
2- Add subclassed ListSerializer as the meta list_serializer_class on the nested Serializer.
Code relevant to yours:
class FilteredListSerializer(serializers.ListSerializer):
def to_representation(self, data):
data = data.filter(user=self.request.user, edition__hide=False)
return super(FilteredListSerializer, self).to_representation(data)
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
list_serializer_class = FilteredListSerializer
model = GroupProfile
fields = ('title','slug',)
class DepartmentSerializer(serializers.ModelSerializer):
organizations = OrganizationSerializer(many=True, read_only=True)
class Meta:
model = DepartmentModel
fields = ('title', 'organizations',)
class SectorSerializer(serializers.ModelSerializer):
# title = serializers.CharField()
departments = DepartmentSerializer(many=True, read_only=True)
class Meta:
model = SectorModel
fields = ('title','departments',)
Thanks to #ans2human for the inspiration behind this answer.
Here's a new approach that is working great for me. I have several Models with is_active = BooleanField(...) that I need to filter out in nested relationships.
NOTE: this solution does not filter out results on non-list fields. for that, you should look to the primary queryset on your View
The core of the work is done by overloading the to_representation() function on a custom ListSerializer, and the many_init on an accompanying custom ModelSerializer:
class FilteredListSerializer(serializers.ListSerializer):
filter_params:dict
def __init__(self, *args, filter_params:dict={"is_active":True}, **kwargs):
super().__init__(*args, **kwargs)
self.filter_params = filter_params
def set_filter(self, **kwargs):
self.filter_params = kwargs
def to_representation(self, data):
data = data.filter(**self.filter_params)
return super().to_representation(data)
class FilteredModelSerializer(serializers.ModelSerializer):
LIST_SERIALIZER_KWARGS = serializers.LIST_SERIALIZER_KWARGS + ("filter_params",)
LIST_ONLY_KWARGS = ('allow_empty', 'filter_params')
#classmethod
def many_init(cls, *args, **kwargs):
list_kwargs = dict()
for arg in cls.LIST_ONLY_KWARGS:
value = kwargs.pop(arg, None)
if value is not None:
list_kwargs[arg] = value
child_serializer = cls(*args, **kwargs, **({"read_only":True} if "read_only" not in kwargs else dict()))
list_kwargs['child'] = child_serializer
list_kwargs.update({
key: value for key, value in kwargs.items()
if key in cls.LIST_SERIALIZER_KWARGS
})
meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', FilteredListSerializer)
return list_serializer_class(*args, **list_kwargs)
Then, your custom ModelSerializer for whatever view would instead just extend FilteredModelSerializer instead.
class ChildSerializer(FilteredModelSerializer):
is_active = BooleanField() # not strictly necessary, just for visibilty
... # the rest of your serializer
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True)
...# the rest of your parent serializer
Now, the children field on the ParentSerializer will filter for is_active = True.
If you have a custom query that you wanted to apply, you can do so by providing a dict of filter params in the standard queryset format:
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True, filter_params={"my_field":my_value, "my_datetime__gte": timezone.now()})
...# the rest of your parent serializer
Alternatively, one could also utilize the set_filter(...) method on the FilteredListSerializer after instantiating the field, like so. This will yield a more familiar format to the original QuerySet.filter(...) style:
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True)
children.set_filter(my_field=my_value, my_datetime__gte=timezone.now())
...# the rest of your parent serializer

Using custom methods in filter with django-rest-framework

I would like to filter against query params in my REST API - see django docs on this.
However, one parameter I wish to filter by is only available via a model #property
example models.py:
class Listing(models.Model):
product = models.OneToOneField(Product, related_name='listing')
...
#property
def category(self):
return self.product.assets[0].category.name
Here is the setup for my Listing API in accordance with django-filter docs
class ListingFilter(django_filters.FilterSet):
product = django_filters.CharFilter(name='product__name')
category = django_filters.CharFilter(name='category') #DOES NOT WORK!!
class Meta:
model = Listing
fields = ['product','category']
class ListingList(generics.ListCreateAPIView):
queryset = Listing.objects.all()
serializer_class = ListingSerializer
filter_class = ListingFilter
How can I appropriately filter by listing.category? It is not available on the listing model directly.
Use the 'action' parameter to specify a custom method - see django-filter docs
First define a method that filters a queryset using the value of the category parameter:
def filter_category(queryset, value):
if not value:
return queryset
queryset = ...custom filtering on queryset using 'value'...
return queryset
Listing filter should look like this:
class ListingFilter(django_filters.FilterSet):
...
category = django_filters.CharFilter(action=filter_category)
...
For sake of database speed, you should just add the category to your listing model
class Listing(models.Model):
product = models.OneToOneField(Product, related_name='listing')
category = models.ForeignKey(Category)
Then use a post_save signal to keep the field updated
from django.dispatch import receiver
from django.db.models.signals import post_save
#receiver(post_save, sender=Product)
def updateCategory(sender, instance, created, update_fields, **kwargs):
product = instance
product.listing.category = product.assets[0].category.name
product.listing.save()
Then filter by it's name as you would any other field:
class ListingFilter(django_filters.FilterSet):
...
category = django_filters.CharFilter(name='category__name')
...

Categories