sorting search result in django rest framework - django

I have written a search api for my website in django rest framework.
when you search a name (e.g. "jumanji") there might be more than one result for the search for many reasons. what I want is for the result to be ordered by the "rating" field or "releaseDate" field of the Film model.
here are my codes.
# models.py
class Film(models.Model):
filmID = models.AutoField(primary_key=True)
title = models.CharField(max_length=150)
duration = models.PositiveIntegerField()
typeOf = models.IntegerField(validators=[MaxValueValidator(3), MinValueValidator(1),])
rating = models.FloatField(default=0, validators=[MaxValueValidator(10), MinValueValidator(0),])
releaseDate = models.DateTimeField(null=True)
# serializers.py
class FilmSerializer(serializers.ModelSerializer):
class Meta:
model = Film
fields = [
"filmID", "title", "price", "duration", "typeOf", "numberOfFilminoRatings", "filminoRating", "rating",
"releaseDate", "detailsEn", "salePercentage", "saleExpiration", "posterURL", "posterDirectory",
]
# views.py
'''Override get_search_fields method of SearchFilter'''
class DynamicSearch(filters.SearchFilter,):
def get_search_fields(self,view, request):
return request.GET.getlist('search_fields',[])
'''Override page_size_query_param attribute of PageNumberPagination'''
class CustomizePagination(PageNumberPagination):
page_size_query_param = 'limit'
"""Pagination Handler"""
class PaginationHanlerMixin(object):
#property
def paginator(self):
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator =None
else :
self._paginator = self.pagination_class()
else :
pass
return self._paginator
def paginate_queryset(self,queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self,data):
if self.paginator is None:
raise "Paginator is None"
return self.paginator.get_paginated_response(data)
class SearchFilm(APIView,PaginationHanlerMixin):
authentication_classes = ()
permission_classes = (AllowAny,)
def __init__(self,):
APIView.__init__(self)
self.search_class=DynamicSearch
self.pagination_class=CustomizePagination
def filter_queryset(self,queryset):
filterd_queryset=self.search_class().filter_queryset(self.request, queryset ,self)
return filterd_queryset
def get(self, request):
films= Film.objects.all()
filtered_queryset=self.filter_queryset(films)
#Get appropriate results for each page
results=self.paginate_queryset(filtered_queryset)
if(results is not None):
serializer=FilmSerializer(results,many=True)
serializer=self.get_paginated_response(serializer.data)
else :
serializer=FilmSerializer(filtered_queryset,many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

It as simple as using function order_by to QuerySet of your films. If you give few options, it will be firstfully ordered by leftmost, then second to the left etc.
films = Film.objects.all().order_by('rating', 'releaseDate')

Related

How can I pass multiple values for a single parameter into the API url in Django Rest Framework?

I have made a filter api like this.
localhost/api/allpackages?price_min=700&price_max=900&destination=Spain&new_activity=Swimming&tour_type=Group%20Tour&featured=true&fix_departure=true
But according to new changes, I should be able to filter like this
localhost/api/allpackages?destination=Spain&destination=Japan&destination=Thailand....featured=true...
There can be multiple values for a single parameter, beacause user can now clik the chekboxes on the frontend. How can I achieve this?
My models:
class Package(models.Model):
operator = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
destination = models.ForeignKey(Destination, on_delete=models.CASCADE)
package_name = models.CharField(max_length=255)
featured = models.BooleanField(default=False)
price = models.IntegerField()
duration = models.IntegerField(default=5)
discount = models.CharField(max_length=255, default="15% OFF")
discounted_price = models.IntegerField(default=230)
savings = models.IntegerField(default=230)
tour_type = models.CharField(max_length=100, choices=TOUR_TYPE, default='Group Tour')
new_activity = models.ManyToManyField(NewActivity)
accommodation = models.CharField(max_length=255,default='Guest House & Hotel')
transport = models.CharField(max_length=150, default='Flight')
age_range = models.CharField(max_length=100, default='6 to 79 years old')
fix_departure = models.BooleanField(default=False)
....
...
My views:
class AllPackageAPIView(ListAPIView):
queryset = Package.objects.all()
serializer_class = PackageSerializer
filterset_class = PackageFilter
def get_queryset(self):
new_activity = self.request.query_params.get('new_activity', None)
destination = self.request.query_params.get('destination', None)
if new_activity is not None:
if destination is not None:
return Package.objects.filter(destination__name=destination, new_activity__title=new_activity)
else:
return Package.objects.filter(new_activity__title=new_activity)
elif destination is not None:
if new_activity is not None:
return Package.objects.filter(destination__name=destination, new_activity__title=new_activity)
else:
return Package.objects.filter(destination__name=destination)
else:
return Package.objects.all()
My filter:
class PackageFilter(filters.FilterSet):
price = filters.RangeFilter()
class Meta:
model = Package
fields = ['price','featured', 'fix_departure',
'tour_type',]
My serializers:
class PackageSerializer(serializers.ModelSerializer):
class Meta:
model = Package
fields = ['id', 'operator','destination', 'package_name', 'duration', 'featured', 'price', 'discount', 'discounted_price',
'tour_type','new_activity', 'accommodation', 'transport', 'age_range',
'savings', 'fix_departure', 'rating', 'image', 'date_created', ]
# fields = '__all__'
depth = 1
I have done this but now no data are shown. The get list is empty. I used get_queryset(self) as a function and now self.request.GET.get for querying.
MY updated view:
class AllPackageAPIView(ListAPIView):
queryset = Package.objects.all()
serializer_class = PackageSerializer
filterset_class = PackageFilter
def get_queryset(self):
new_activity = self.request.GET.get('new_activity', None)
destination = self.request.GET.get("destination", "")
destination_values = destination.split(",")
if new_activity is not None:
if destination is not None:
return Package.objects.filter(destination__name=destination_values, new_activity__title=new_activity)
else:
return Package.objects.filter(new_activity__title=new_activity)
elif destination is not None:
if new_activity is not None:
return Package.objects.filter(destination__name=destination_values, new_activity__title=new_activity)
else:
return Package.objects.filter(destination__name=destination_values)
else:
return Package.objects.all()
My solution:
def get_queryset(self):
# def get(self, request, format=None, *args, **kwargs):
new_activity = self.request.GET.get('new_activity',None)
destination = self.request.GET.get("destination",None)
tour_type = self.request.GET.get("tour_type",None)
if new_activity is not None:
new_activity = self.request.GET.get('new_activity', "")
new_activity_values = new_activity.split(",")
if destination is not None:
destination = self.request.GET.get("destination", "")
destination_values = destination.split(",")
if tour_type is not None:
tour_type = self.request.GET.get("tour_type", "")
tour_type_values = tour_type.split(",")
return Package.objects.filter(destination__name__in=destination_values,new_activity__title__in=new_activity_values,
tour_type__in=tour_type_values)
else:
return Package.objects.filter(destination__name__in=destination_values,
new_activity__title__in=new_activity_values)
else:
return Package.objects.filter(new_activity__title__in=new_activity_values)
elif destination is not None:
destination = self.request.GET.get("destination", "")
destination_values = destination.split(",")
if new_activity is not None:
new_activity = self.request.GET.get('new_activity', "")
new_activity_values = new_activity.split(",")
if tour_type is not None:
tour_type = self.request.GET.get("tour_type", "")
tour_type_values = tour_type.split(",")
return Package.objects.filter(destination__name__in=destination_values,
new_activity__title__in=new_activity_values,
tour_type__in=tour_type_values)
else:
return Package.objects.filter(destination__name__in=destination_values,
new_activity__title__in=new_activity_values
)
else:
return Package.objects.filter(destination__name__in=destination_values)
elif tour_type is not None:
tour_type = self.request.GET.get("tour_type", "")
tour_type_values = tour_type.split(",")
if destination is not None:
destination = self.request.GET.get("destination", "")
destination_values = destination.split(",")
if new_activity is not None:
new_activity = self.request.GET.get('new_activity', "")
new_activity_values = new_activity.split(",")
return Package.objects.filter(destination__name__in=destination_values,
new_activity__title__in=new_activity_values,
tour_type__in=tour_type_values)
else:
return Package.objects.filter(destination__name__in=destination_values,
tour_type__in=tour_type_values)
else:
return Package.objects.filter(tour_type__in=tour_type_values)
else:
return Package.objects.all()
This works as a filter for checkbox searches in ecommerce website. But it has a problem. When calling api, it repeats some of the objects ie same package object in my case. If anyone can solve it, let me know.
I have solved this question before, I've decided to get multiple values in URL by using split , character.
Example: URL: localhost/api/allpackages?destination=Spain,Japan,Thailand....featured=true...
destination = self.request.GET.get("destination", "")
destination_values = destination.split(",")
Sample code about filtering first_name, last_name, and multiple values of username in User model.
model.py
class User(AbstractUser):
#property
def full_name(self):
"""Custom full name method as a property"""
return str(self.first_name) + ' ' + str(self.last_name)
def __str__(self):
return self.email
view.py
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ['first_name', 'last_name']
class ListCreateUser(ListCreateAPIView):
"""
List and Create User Generic contains create and list user APIs.
"""
serializer_class = UserSerializer
queryset = User.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = UserFilter
def get_queryset(self):
username = self.request.GET.get('username', '')
if username:
username_values = username.split(',')
return User.objects.filter(username__in=username_values)
return User.objects.all()
Results:
Filter by username
I found that Django supports multi-value parameters with its QueryDict since at least 3.0 . So when you have the situation like:
https://www.example.com/foo?a=1&a=2
you can get all values of a with:
def my_function(request):
a_list = request.query_params.getlist('a')
# [1, 2]
This is not intuitive, since request.query_params.get('a') only returns the last element in the list (see documentation).
django-rest-framework does not provide multi-value filter support, you have to write it yourself if you want OR you can use djangorestframework-jsonapi it provides the multi-value filter and many other pluggable features
Membership in a list of values: ?filter[name.in]=abc,123,zzz (name in ['abc','123','zzz'])
You can configure the filter backends either by setting the REST_FRAMEWORK['DEFAULT_FILTER_BACKENDS'] or individually add them as .filter_backends
'DEFAULT_FILTER_BACKENDS': (
'rest_framework_json_api.filters.QueryParameterValidationFilter',
'rest_framework_json_api.filters.OrderingFilter',
'rest_framework_json_api.django_filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
),
See this example
https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#configuring-filter-backends
Your code with changes you don't need to write PackageFilter and get_queryset
from rest_framework_json_api import django_filters
class AllPackageAPIView(ListAPIView):
queryset = Package.objects.all()
serializer_class = PackageSerializer
filter_backends = (django_filters.DjangoFilterBackend)
filterset_fields = {'destination': ('exact', 'in'), ...}

How can we pass another field to Create Model Mixin?

Model:
class ShopItem(models.Model):
id = models.AutoField(db_column='ID', primary_key=True)
name = models.CharField(db_column='Name', max_length=255)
price = models.IntegerField(db_column='Price', default=0)
description = models.CharField(db_column='Description', max_length=63)
seller_id = models.ForeignKey(Seller, models.DO_NOTHING, db_column='SellerID')
View:
class SellerItemAPIView(GenericAPIView, ListModelMixin, CreateModelMixin, UpdateModelMixin):
serializer_class = ShopItemSerializer
permission_classes = [AllowAny]
def get_seller(self, *args, **kwargs):
phone_number = self.kwargs.get('phone_number')
seller = Seller.objects.filter(Q(user_id__phone_number=phone_number))[0]
return seller
def post(self, request):
seller = self.get_seller()
return self.create(request, seller_id=seller.id)
Is there any way to use this Creat method with another field?
It now give me this error:
{
"seller_id": [
"This field is required."
]
}
you can add the seller_id to your request.data before calling self.create like this:
seller = Seller.objects.get(user_id=self.request.user.id)
if request.data = {}:
request.data.update(seller_id=seller)
else:
try:
if not request.data._mutable:
request.data._mutable = True
request.data.update(seller_id=seller)
except:
request.data.update(seller_id=seller)

Nested JSON and HyperlinkedModelSerializer problem

I'm working on a tweet App. I have 2 main models : Tweets and Comments. I'm using HyperlinkedModelSerializer to get absolute url for my comments instances with the "url" added in the field. But when It comes to display the comments inside my tweet JSON format, i have this error :
`HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.
This error is gone when I remove the url from the field.
Here is my Comment Model :
class CommentManager(models.Manager):
def filter_by_instance(self, instance):
content_type = ContentType.objects.get_for_model(instance.__class__)
obj_id = instance.id
qs = super(CommentManager, self).filter(content_type=content_type, object_id=obj_id)
return qs
class Comment(models.Model):
content = models.TextField(max_length=150)
author = models.ForeignKey(
User,
on_delete = models.CASCADE
)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
parent = models.ForeignKey(
"self",
on_delete = models.CASCADE,
blank=True,
null=True
)
datestamp = models.DateTimeField(auto_now_add=True)
objects = CommentManager()
def __str__(self):
return str(self.content[:30])
def save(self):
self.object_id = self.parent.id
super(Comment, self).save()
def children(self):
return Comment.objects.filter(parent=self)
#property
def is_parent(self):
if self.parent is None:
return False
return True
Here is my comment serializer :
class CommentSerializer(serializers.HyperlinkedModelSerializer):
children = serializers.SerializerMethodField()
datestamp = serializers.SerializerMethodField()
def get_datestamp(self, obj):
return obj.datestamp.date()
def get_children(self, obj):
qs = obj.children()
return ChildrenCommentSerializer(qs, many=True).data
class Meta:
model = Comment
fields = [
"url",
"datestamp",
"content",
"is_parent",
"object_id",
"children"
]
class ChildrenCommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = [
"content"
]
And finally my tweet serializer :
class TweetSerializer(serializers.ModelSerializer):
author = serializers.SlugRelatedField(slug_field="username", queryset=User.objects.all())
comments = serializers.SerializerMethodField()
datestamp = serializers.SerializerMethodField()
def get_datestamp(self, obj):
return obj.datestamp.date()
def get_comments(self, obj):
qs = Comment.objects.filter_by_instance(obj)
print()
print()
print(CommentSerializer(qs, many=True))
print()
print()
return CommentSerializer(qs, many=True).data
class Meta:
model = Tweet
fields = ["datestamp", "id", "content", "author", "nb_likes", "nb_comments", "slug", "comments" ]
def to_representation(self, obj):
representation = super().to_representation(obj)
if obj.nb_likes > 50:
representation['super_popular'] = True
return representation
I do not understand how and where in the code i should add the "context={'request': request}"
place it when instantiating the serializer
class ViewExample(APIView):
def get(self, request, pk, format=None):
listexample = Example.objects.all()
serializer = ExampleSerializer(listexample, many=True, context={'request':request})
return Response(serializer.data, status=status.HTTP_200_OK)

DRF: How to validate a nested serializer's fields?

I have models as below:
model: Business
name
model: Employee
business=FK(Business),
user = FK(User)
model: Item
title
units
model: PurchaseInvoice
business=FK(Business)
employee = FK(Employee)
invoice_no
date
model: PurchasedItems
parent_invoice=FK(PurchaseInvoice, related_name="purchased_items")
item = FK(Item)
quantity
rate
and serializers.py:
class PurchasedItemPOSTSerializer(serializers.ModelSerializer):
class Meta:
model = PurchasedItem
fields = ('item', 'quantity', 'rate')
def validate_quantity(self, value):
if value==0 or value is None:
raise serializers.ValidationError("Invalid quantity")
return value
def validate_rate(self, value):
if value==0 or value is None:
raise serializers.ValidationError("rate?? Did you get it for free?")
return value
class PurchaseInvoicePOSTSerializer(serializers.ModelSerializer):
purchased_items = PurchasedItemPOSTSerializer(many=True)
class Meta:
model = PurchaseInvoice
fields = ('invoice_no','date','purchased_items')
def validate_purchased_items(self, value):
if len(value)==0:
raise serializers.ValidationError("atleast one item is required.")
return value
#transaction.atomic
def create(self, validated_data):
purchased_items_data = validated_data.pop('purchased_items')
purchase_invoice = self.Meta.model.objects.create(**validated_data)
for purchased_item in purchased_items_data:
PurchasedItem.objects.create(purchase_invoice=purchase_invoice, **purchased_item)
return invoice
and views.py
class PurchaseInvoiceViewSet(viewsets.ModelViewSet):
serializer_class = PurchaseInvoicePOSTSerializer
def get_serializer_context(self):
return {'user': self.request.user, 'business_id': self.kwargs['business_id']}
def get_queryset(self, **kwargs):
return PurchaseInvoice.objects.filter(business__id=self.kwargs['business_id'])
def perform_create(self, serializer):
business = Business.objects.get(pk=self.kwargs['business_id'])
employee = get_object_or_404(Employee, business=business, user=self.request.user)
serializer.save(business=business, employee=employee)
The data sent through POST is:
{
"invoice_no": "123",
"date": "2018-07-13",
"purchased_items": [
{"item":1, "quantity":0, "rate":0}],
}
As you can see from the data above no error is raised by validate_quantity when quantity=0 and neither by validate_rate when rate=0 for the item in purchased_items.
You can add custom validate in your serializer. For example:
from rest_framework import serializers
class PurchasedItemPOSTSerializer(serializers.ModelSerializer):
class Meta:
model = PurchasedItem
fields = ('item', 'quantity', 'rate')
def validate(self, data):
# custom anything validate
quantity = data.get('quantity')
# recheck validate in this
if not quantity:
raise serializers.ValidationError('not valid')
return data

Two model forms in one view/template in Django using CBV

Can someone show the example of usage of two model forms in one view/template? The task is to process more than one form in one CBV, where model of the second form has FK to first form model, so value from select widget from first form must be processed as value for object, created in second form.
My forms looks like this:
class CompanyEditForm(ModelForm):
name = CharField(label="Наименование", required=True)
type = ModelChoiceField(queryset=CompanyTypes.objects.all(), label="Тип компании", empty_label=None, initial=3)
description = CharField(label="Описание компании", required=False, widget=forms.Textarea(attrs={'cols': 40, 'rows':3}))
www = CharField(label="WEB сайт", required=False)
class Meta:
model = Companies
fields = ('type', 'name', 'description', 'www')
class BranchEditForm(ModelForm):
name = CharField(label="Наименование офиса", required=True)
type = ModelChoiceField(queryset=BranchTypes.objects.all(), label="Тип отделения", empty_label=None, initial=1)
class Meta:
model = Branches
exclude = ('company', 'address')
class AddressEditForm(ModelForm):
postalcode = IntegerField(label="Почтовый код", required=False)
city = CharField(label="Город", required=True)
street = CharField(label="Улица", required=True)
app = CharField(label="Дом", required=True)
app_extra = CharField(label="Корпус / Строение", required=False)
comment = CharField(label="Примечание к адресу", required=False)
exclude = ('company',)
class Meta:
model = Addresses
fields = ('postalcode', 'city', 'street', 'app', 'app_extra', 'comment')
UPDATE
I wrote this mixin:
class MultiFormCreate(FormMixin, TemplateResponseMixin, View):
formconf = None
def get_form_classes(self):
form_classes = {}
for key, params in self.formconf.items():
form_classes[key] = params.formclass
return self.form_classes
def get_initial(self, classname):
inicial = {}
if 'inicial' in self.formconf[classname]:
inicial = self.formconf[classname]['inicial'].copy()
return inicial
def get_form_kwargs(self, classname):
kwargs = {'initial': self.get_initial(classname), 'prefix': classname}
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
return kwargs
def get_forms(self):
for classname, params in self.formconf.items():
log.info("Name: %s, Params: %s" % (classname, params))
return dict(
[(classname, params['formclass'](**self.get_form_kwargs(classname))) for classname, params in self.formconf.items()])
def get(self, request, *args, **kwargs):
forms = self.get_forms()
return self.render_to_response(self.get_context_data(forms=forms))
def get_success_url(self):
if self.success_url:
url = force_text(self.success_url)
else:
raise ImproperlyConfigured(
"No URL to redirect to. Provide a success_url.")
return url
then in a view i only need to write processing in post:
class CompanyCreate(MultiFormCreate):
template_name = 'company/edit.html'
success_url = '/forbidden/'
formconf = {
'company': {'formclass': CompanyEditForm, 'inicial': {'name': "TESTNAME"}},
'branch': {'formclass': BranchEditForm},
'address': {'formclass': AddressEditForm}
}
def post(self, request, *args, **kwargs):
forms = self.get_forms()
cform = forms['company']
aform = forms['address']
bform = forms['branch']
if cform.is_valid() and aform.is_valid() and bform.is_valid():
''' Creating main form form object (by saving tthe form) '''
company_object = cform.save()
''' Creating dependant object '''
address_object = aform.save(commit=False)
branch_object = bform.save(commit=False)
''' assigning dependent fields '''
address_object.company = company_object
''' saving dependent _object_ '''
address_object.save()
''' managing another dependent fields '''
branch_object.company = company_object
branch_object.address = address_object
''' saving last object '''
branch_object.save()
return HttpResponseRedirect(self.get_success_url())
else:
forms = self.get_forms()
return self.render_to_response(self.get_context_data(forms=forms))
Code for view file :
if request.method == "POST":
company_form = CompanyEditForm(request.POST)
if company_form.is_valid():
company_object = company_form.save()
post_data = request.POST.copy()
branch_form = BranchEditForm(post_data)
branch_form.data['company'] = company_object
if branch_form.is_valid():
branch_object.save()
rest implement your business logic ..