Django REST Framework caching programmatically - django

From the official docs it looks like the only way of caching with DRF is to use decorators.
As far as I know, there is also a more flexible way to use caching directly querying the cache, like in the example below (source):
from django.core.cache import cache
from django.conf import settings
from django.core.cache.backends.base import DEFAULT_TIMEOUT
CACHE_TTL = getattr(settings, 'CACHE_TTL', DEFAULT_TIMEOUT)
def cached_sample(request):
if 'sample' in cache:
json = cache.get('sample')
return JsonResponse(json, safe=False)
else:
objs = SampleModel.objects.all()
json = serializers.serialize('json', objs)
# store data in cache
cache.set('sample', json, timeout=CACHE_TTL)
return JsonResponse(json, safe=False)
This approach gives use more control over what and how long we store in the cache.
So, my question is the following: is there a way to adapt this way of caching to a simple view defined in DRF?
Example:
# MODEL
class Item(models.Model):
code = models.CharField(max_length=8, primary_key=True)
name = models.CharField(max_length=32)
# SERIALIZER
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Pair
fields = '__all__'
# VIEW
class ItemsList(generics.ListAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
def list(self, request):
queryset = self.get_queryset()
serializer = ItemSerializer(queryset, many=True)
return Response(serializer.data)

to complete my comment
try to cache serializer.data if already present in cache return directly Response(cached_data) before building the queryset
try something like this :
# VIEW
class ItemsList(generics.ListAPIView):
if 'my_key' in cache:
return Response(cache.get('my_key'))
else:
queryset = self.get_queryset()
serializer = ItemSerializer(queryset, many=True)
# store data in cache
cache.set('my_key', serializer.data, timeout=CACHE_TTL)
return Response(serializer.data)

Related

How to change a model after creating a ModelViewset?

I am creating an API with django rest framework.
But I have a problem every time I want to modify a model when I have already created an associated ModelViewset.
For example, I have this model:
class Machine(models.Model):
name = models.CharField(_('Name'), max_length=150, unique=True)
provider = models.CharField(_("provider"),max_length=150)
build_date = models.DateField(_('build date'))
category = models.ForeignKey("machine.CategoryMachine",
related_name="machine_category",
verbose_name=_('category'),
on_delete=models.CASCADE
)
site = models.ForeignKey(
"client.Site",
verbose_name=_("site"),
related_name="machine_site",
on_delete=models.CASCADE
)
class Meta:
verbose_name = _("Machine")
verbose_name_plural = _("Machines")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("Fridge_detail", kwargs={"pk": self.pk})
And this viewset:
class MachineViewSet(viewsets.ModelViewSet):
"""
A simple ViewSet for listing or retrieving machine.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = MachineSerializer
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
#swagger_auto_schema(responses={200: MachineSerializer})
def list(self, request):
serializer_context = {
'request': request,
}
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
serializer = MachineSerializer(queryset, many=True, context=serializer_context)
return Response(serializer.data)
#swagger_auto_schema(responses={404: 'data not found', 200: MachineSerializer})
def retrieve(self, request, pk=None):
serializer_context = {
'request': request,
}
queryset = Machine.objects.all()
machine = get_object_or_404(queryset, pk=pk)
serializer = MachineSerializer(machine, context=serializer_context)
return Response(serializer.data)
If I want to add a field to my model, like for example a description. When I run the command python manage.py makemigrations
I get the error :
django.db.utils.ProgrammingError: column machine_machine.description does not exist
I have to comment my viewset to be able to run the makemigrations and migrate.
How can I avoid having to comment each time the viewsets when I want to modify a model?
In your view you have the following line in your class:
queryset = cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
Since this line is present in the class declaration and not in a method of the class it is executed when the class is created / interpreted. This causes it to fire a query, since a queryset will need to be evaluated (pickling Querysets will force evaluation) to cache it. The problem here is that your models haven't been migrated yet and your queries fail. You can write that code in the get_queryset method by overriding it if you want:
class MachineViewSet(viewsets.ModelViewSet):
"""
A simple ViewSet for listing or retrieving machine.
"""
permission_classes = [permissions.IsAuthenticated]
serializer_class = MachineSerializer
# Remove below lines
# queryset = cache.get_or_set(
# 'machine_machines_list',
# Machine.objects.all(),
# 60*60*24*7
# )
def get_queryset(self):
return cache.get_or_set(
'machine_machines_list',
Machine.objects.all(),
60*60*24*7
)
...
But this implementation is critically flawed! You are caching the queryset for a week. Lot's of new data can come in within that time period and you would be essentially serving stale data to your users. I would advise you to forego this sort of caching. Also you seem to be returning all the data from your view, consider some sort of filtering on this so that less memory is needed for such stuff.

Product matching query does not exist Django

i am trying to insert product in my database using django custom fields, but it's showing me error that Product matching query does not exist.
it would be great if anybody could figure me out where should i make changes in my code. thank you so much in advance.
views.py
class ProductAdd(APIView):
def post(self, request, format=None):
data = request.data
title = data['title']
slug = data['slug']
description = data['description']
# created_on = data['created_on']
# status = data['status']
queryset = Product.objects.filter(title__contains=title,slug__contains=slug,description__contains=description)
query_slug = Product.objects.get(slug__exact=slug).first()
try:
if query_slug == None:
# queryset.annotate(Count(title,slug,description,created_on,status))
queryset.annotate()
Response({"msg":"product added succesfully!"}, status=HTTP_201_CREATED)
else:
print("query already exist!")
except ValueError:
return Response(status=HTTP_400_BAD_REQUEST)
As the error said, it could not find a product based on given slug. To prevent this error from raising, you can use .filter instead of .get:
query_slug = Product.objects.filter(slug__exact=slug).first()
Also, I would recommend using a serializer to serialize and store data in DB. More information can be found in documentation. Here is an example:
# serializer
from rest_framework import serializers
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = "__all__"
def validate_slug(self, value):
if Product.objects.filter(slug=slug).exists():
raise serializers.ValidationError("Product Exists")
return value
# view
from rest_framework import generics
class ProductAdd(generics.CreateAPIView):
serializer_class = ProductSerializer

how to save foreign keys using django-rest-framework

I'm new to Django and I'm trying to save Unit and current_user foreign keys in the database based pk obtained but every time I try doing so but run into two types of error serializers.is_valid() raises an exception error or the serializer returns "Invalid data. Expected a dictionary, but got Unit."
I have tried a very very ugly way of bypassing serializers by getting rid but i get ee8452a4-2a82-4804-a010-cf2f5a41e006 must be an instance of SavedUnit.unit.I have also tried saving the foreign key directly using SavedUnit.objects.create() without luck
model.py
class SavedUnit(models.Model):
"""
Saving units for later models
relationship with units and users
"""
id = models.UUIDField(primary_key=True, default=hex_uuid, editable=False)
unit = models.ForeignKey(Unit, on_delete=models.CASCADE)
user = models.ForeignKey('accounts.User', on_delete=models.CASCADE, related_name='user')
published_at = models.DateTimeField(auto_now_add=True)
serializers.py
class SavedSerializer(serializers.ModelSerializer):
unit = UnitSerializer()
class Meta:
model = SavedUnit
fields = [
'id',
'unit'
]
views.py
class SavedUnitView(APIView):
"""
Query all the unites saved
"""
#staticmethod
def get_unit(request, pk):
try:
return Unit.objects.get(pk=pk)
except Unit.DoesNotExist:
return Response(status=status.HTTP_400_BAD_REQUEST)
#staticmethod
def post(request, pk):
if request.user.is_authenticated:
unit = get_object_or_404(Unit, id=pk)
serializers = SavedSerializer(data=unit)
if serializers.is_valid(raise_exception=True):
created = SavedUnit.objects.get_or_create(
user=request.user,
unit=unit)
return Response(status=status.HTTP_201_CREATED)
return Response(status=status.HTTP_401_UNAUTHORIZED)
def get(self, request):
units = SavedUnit.objects.filter(user=self.request.user.id)
try:
serializers = SavedSerializer(units, many=True)
return Response(serializers.data, status=status.HTTP_200_OK)
except Unit.DoesNotExist:
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Your code is using serializer only for validation, but it is possible use it to insert or update new objects on database calling serializer.save().
To save foreign keys using django-rest-framework you must put a related field on serializer to deal with it. Use PrimaryKeyRelatedField.
serializers.py
class SavedSerializer(serializers.ModelSerializer):
unit_id = serializers.PrimaryKeyRelatedField(
source='unit',
queryset=Unit.objects.all()
)
unit = UnitSerializer(read_only=True)
class Meta:
model = SavedUnit
fields = [
'id',
'unit_id',
'unit'
]
views.py
class SavedUnitView(APIView):
permission_classes = (permissions.IsAuthenticated,) # For not handling authorization mannually
def post(request):
serializer = SavedSerializer(data=request.data)
serializer.is_valid(raise_exception=True) # Trigger Bad Request if errors exist
serializer.save(user=request.user) # Passing the current user
return Response(serializer.data, status=status.HTTP_201_CREATED)
Now, the id of the unit will be passed in the request body like this
POST /saved-units/
Accept: application/json
Content-Type: application/json
Authorization: Token your-api-token
{
"unit_id": 5 # Id of an existing Unit
}
Actually the problem is here:
def post(request, pk):
if request.user.is_authenticated:
unit = get_object_or_404(Unit, id=pk)
serializers = SavedSerializer(data=unit) <-- Here
You are passing a unit instance, but it should be request.data, like this:
serializers = SavedSerializer(data=request.data)
( I am unsure about what you are doing, if you already have PK, then why are you even using serializer? because you don't need it as you already have the unit, and you can access current user from request.user, which you are already doing. And I don't think you should use #staticmethod, you can declare the post method like this: def post(self, request, pk) and remove static method decorator)

Django filter not working

my filter isn't working Whenever I access http://localhost:8080/payables/invoices/?status=NOT_PAID It just returns all the invoices. I have no runtime error, the parameter I enter simply seems to be ignored. I really don't understand, other than that, it works well.
views.py
class InvoiceViewSet(viewsets.ViewSet):
serializer_class = InvoiceSerializer
filter_backend = filters.DjangoFilterBackend
filter_fields = ('status','supplier',)
def list(self,request,):
queryset = Invoice.objects.filter()
serializer = InvoiceSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = Invoice.objects.filter()
invoice = get_object_or_404(queryset, pk=pk)
serializer = InvoiceSerializer(invoice)
return Response(serializer.data)
class InvoiceItemViewSet(viewsets.ViewSet):
serializer_class = InvoiceItemSerializer
def list(self,request,invoice_pk=None):
queryset = InvoiceItem.objects.filter(invoice=invoice_pk)
serializer = InvoiceItemSerializer(queryset,many=True)
return Response(serializer.data)
def retrieve(self,request,pk,invoice_pk):
queryset = InvoiceItem.objects.filter(pk=pk,invoice=invoice_pk)
invoice_item = get_object_or_404(queryset,pk=pk)
serializer = InvoiceItemSerializer(invoice_item)
return Response(serializer.data)
url.py
from django.conf.urls import url, include
#viewset
from rest_framework_nested import routers
from payables.views import InvoiceViewSet,InvoiceItemViewSet
router = routers.SimpleRouter()
router.register(r'invoices', InvoiceViewSet,base_name='invoices')
invoice_item_router = routers.NestedSimpleRouter(router,r'invoices',lookup='invoice')
invoice_item_router.register(r'items', InvoiceItemViewSet, base_name='invoice_items')
urlpatterns = [
url(r'^',include(router.urls)),
url(r'^',include(invoice_item_router.urls))
]
It is because you are explicitly creating the queryset and hence the filter backend is never used:
queryset = Invoice.objects.filter()
I suggest looking at ModelViewSet. In that case you just have to pass queryset at the view level and rest will be taken care of.
instead of queryset = Invoice.objects.filter()
with queryset = self.filter_queryset(self.get_queryset()).filter()
instead of queryset = Invoice.objects.filter()
use queryset = self.get_queryset()
self.get_queryset() returns the filtered object list

Pagination in Django-Rest-Framework using API-View

I currently have an API view setup as follows:
class CartView(APIView):
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [IsAuthenticated, ]
api_view = ['GET', 'POST']
def get(self, request, format=None):
try:
cart = request.user.cart
except Cart.DoesNotExist:
cart = Cart.objects.create(user=request.user)
cart_details = cart.cart_details.all()
serializer = CartDetailSerializer(cart_details, many=True, fields=['id', 'item', 'quantity', 'product_type'])
return Response(serializer.data)
Here CartDetailSerializer is a normal ModelSerializer.
I want to paginate this API. However, in the docs of DRF, I found this:
If you're using a regular APIView, you'll need to call into the pagination API yourself to ensure you return a paginated response.
There is no example provided on how to paginate a regular APIView API.
Can anyone post an example which I can use in above scenario.
Thanks.
While the way rayy mentions is a possibility, django-rest-framework can handle this internally with some additional features that make working with your API much easier. (*note django-rest-framework's pagination is built from the Django paginator from django.core.paginator)
Right after what you quoted is the key information to solving this problem:
Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular APIView, you'll need to call into the pagination API yourself to ensure you return a paginated response. See the source code for the mixins.ListMixin and generics.GenericAPIView classes for an example.
Slight correction to what is stated there: look at the ListModelMixin.
If you go to these two links you can see the source code for the above files:
generics.py
mixins.py
What you need to do is include something like the following to get pagination to work in the APIView (**note: this code is untested but the idea is correct. There is also a better way of writing this rather than having to include the code in every view but I will leave that up to you to keep my answer short and understandable):
from __future__ import absolute_import
# if this is where you store your django-rest-framework settings
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Cart
class CartView(APIView):
pagination_class = settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
#assuming every other field in the model has a default value
cart = Cart.objects.get_or_create(user=request.user)
#for a clear example
cart_details = Cart.objects.all()
page = self.paginate_queryset(cart_details)
if page is not None:
serializer = CartDetailSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = CartDetailSerializer(cart_details, many=True)
return Response(serializer.data)
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
I hope this was of more help to you and others who come across this post.
When using regular APIView, you need to use Django's own Paginator class.
Django Pagination in Views
In your case you can paginate queryset before sending it to serializer.
Something like this:
def get(self, request, format=None):
try:
cart = request.user.cart
except Cart.DoesNotExist:
cart = Cart.objects.create(user=request.user)
cart_details = cart.cart_details.all()
paginator = Paginator(cart_details, 10)
page = request.GET.get('page')
try:
cart_details = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
cart_details = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
cart_details = paginator.page(paginator.num_pages)
serializer = CartDetailSerializer(cart_details, many=True, fields=['id', 'item', 'quantity', 'product_type'])
return Response(serializer.data)
Hope this helps.
I prefer extending the Paginator class, here is how it would look:
from rest_framework import status
from rest_framework.exceptions import NotFound as NotFoundError
from rest_framework.pagination import PageNumberPagination # Any other type works as well
from rest_framework.response import Response
from rest_framework.views import APIView
class CustomPaginator(PageNumberPagination):
page_size = 10 # Number of objects to return in one page
def generate_response(self, query_set, serializer_obj, request):
try:
page_data = self.paginate_queryset(query_set, request)
except NotFoundError:
return Response({"error": "No results found for the requested page"}, status=status.HTTP_400_BAD_REQUEST)
serialized_page = serializer_obj(page_data, many=True)
return self.get_paginated_response(serialized_page.data)
class CartView(APIView):
def get(self, request, format=None):
cart_details = Cart.objects.filter(user=request.user) # or any other query
paginator = CustomPaginator()
response = paginator.generate_response(cart_details, CartDetailSerializer, request)
return response
I am using DRF version 3.6.2.
You don't need to code so much. Just use this simple steps.
class ProductPagination(PageNumberPagination):
page_size = 5
class product_api(generics.ListCreateAPIView):
queryset = Products.objects.all()
serializer_class = product_serilizer
pagination_class = ProductPagination
if you want search functionality by getting method, you can write below code
class ProductPagination(PageNumberPagination):
page_size = 5
class product_api(generics.ListCreateAPIView):
queryset = Products.objects.all()
serializer_class = product_serilizer
pagination_class = SearchProductPagination
def get_queryset(self):
qs = super(product_search_api,self).get_queryset()
searched_product = self.request.query_params.get('searched_product',None)
if search:
qs = Products.objects.filter(Q(product_name__icontains= searched_product))
return qs