Bulk set properties to django queryset - python

I have model like this:
class MyModel(models.Model):
#fields
def __init__(self, *args, **kwargs):
self._issue_status = None
super(...).__init__(...)
#property
def issue_status(self):
if self._issue_status is None:
self.get_issue_status()
return self._issue_status
where get_issue_status goes to api and take status. But api can bulk return statuses and i want to use it when get queryset of MyModel.
So i write own manager:
class MyModelManager(models.Manager):
def get_queryset(self):
queryset = super(...).get_queryset()
issues = self.bulk_get_issues()
set_issue_statuses(queryset)
return queryset
It works nice for getting all objects. But when i try to filter queryset - status property for every object is None. How can i fix that? Thank.

I think you want to override your get_queryset function,
something like:
def get_queryset(self, request):
qs = super(...).get_queryset(request)
the_filter = ["None", "", "SKIPPED", ]
return qs.exclude(status__in=the_filter)

Related

Django filter by two parameters

I want to filter by two parameters. I have this:
def get_queryset(self):
queryset = Product.objects.filter(
Q(category__in=self.request.GET.getlist("category")) &
Q(brand__in=self.request.GET.getlist("brand")))
return queryset
But this works only if two filters are chosen(otherwise it returns nothing). I want my program to filter by one filter if only one is chosen and filter by two parameters if two are chosen. How can I do that?
def get_queryset(self):
queryset = Product.objects.all()
if "category" in self.request.GET:
queryset = queryset.filter(category__in=self.request.GET.getlist("category"))
if "brand" in self.request.GET:
queryset = queryset.filter(brand__in=self.request.GET.getlist("brand"))
return queryset
Most methods applied to a queryset will return a new queryset. This allows you to add more parameters as required. As the queryset is lazy, it doesn't actually query the database until you start consuming data so there's no performance overhead to the above either.
You can do this.
def get_queryset(self):
kwargs = {}
if self.request.GET.getlist("category"):
kwargs["category__in"] = self.request.GET.getlist("category")
if self.request.GET.getlist("brand"):
kwargs["brand__in"] = self.request.GET.getlist("brand")
return Product.objects.filter(**kwargs)
You can actually write model manager for custom query_sets. For more information you can read the documentation: https://docs.djangoproject.com/en/3.0/topics/db/managers/#calling-custom-queryset-methods-from-the-manager
class ProductQuerySet(models.query.QuerySet):
def active(self):
return self.filter(active=True)
def search(self, query):
lookups = (Q(title__icontains=query) |
Q(description__icontains=query) |
Q(price__icontains=query) |
Q(tag__title__icontains=query)) # (model_name)__(model_field)__(filter)
return self.filter(lookups).distinct()
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def search(self, query):
return self.get_queryset().active().search(query)
In Views.py:
def get_queryset(self, *args, **kwargs):
request = self.request
query = request.GET.get('q', None)
if query is not None:
query = query.strip()
return Product.objects.search(query)
return Product.objects.featured()
I hope that will help you. Good Luck

LIstSerializer AssertionError

I am trying to use a ListSerializer so that I can create/de-serialize multiple objects in a list on a POST. I followed the guide at https://www.django-rest-framework.org/api-guide/serializers/#listserializer but seem to be running into this error when i visit the endpoint.
assert self.child is not None, '``child`` is a required argument.'
python3.7/site-packages/rest_framework/serializers.py in __init__, line 592
My serializers are as follows:
class PredictionListSerializer(serializers.ListSerializer):
def update(self, instance, validated_data):
pass
def create(self, validated_data):
predictions = [Prediction(**item) for item in validated_data]
return Prediction.objects.bulk_create(predictions)
class NestedPredictionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
# Nested Serializer
data = DataSerializer()
class Meta:
model = Prediction
list_serializer_class = PredictionListSerializer
fields = ('id', 'specimen_id', 'data' 'date_added',)
extra_kwargs = {
'slug': {'validators': []},
}
datatables_always_serialize = ('id',)
The assertion error is being thrown in the initialization of the ListSerializer, however, the serializer is being initialized in a ViewSet like so.
class BulkPredictionViewSet(viewsets.ModelViewSet):
queryset = Prediction.objects.all()
serializer_class = PredictionListSerializer
Anyone familiar with this issue? I am wondering if my lack o control around initialization of the serializer (because I am using a ViewSet) is affecting this. If I try to pass the NestedPredictionSerializer into the ViewSet I get "Invalid data. Expected a dictionary, but got list."
EDIT:
I was able to overwrite the get_serializer method in my ViewSet to set many=True and pass the serializer as my NestedPredictionSerializer which recognizes the list on a POST. Howeber, on a get now I receive the error When a serializer is passed a ``data`` keyword argument you must call ``.is_valid()`` before attempting to access the serialized ``.data`` representation.
You should either call ``.is_valid()`` first, or access ``.initial_data`` instead.
I was able to get both the POST and GET working when the following the advice on this read combined with this answer: https://stackoverflow.com/a/45651309/3439441
My overridden ViewSet.get_serializer now looks like this:
def get_serializer(self, *args, **kwargs):
if self.request.method.lower() == 'post':
data = kwargs.get('data')
kwargs['many'] = isinstance(data, list)
return super(BulkPredictionViewSet, self).get_serializer(*args, **kwargs)
Just use many=True only for create action:
class BulkPredictionViewSet(viewsets.ModelViewSet):
queryset = Prediction.objects.all()
serializer_class = NestedPredictionSerializer
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
if self.action == 'create':
kwargs['many'] = True
return serializer_class(*args, **kwargs)
You don't need to use PredictionListSerializer since many=True will create ListSerializer automatically.

DeleteView marking inactive instead deleting?

Is there some elegant solution to using Django's DeleteView but instead actually deleting the objects, marking them inactive? I like the simplicity of the DeleteView but would want to keep the data at the backend, instead of removing it.
Elegant solution would be overriding Model & Manager to update a field on delete. This is an implementation as Abstract Model, so that it can be extended by any other Model. You can modify it as per your need, if you already have delete fields in your model.
Soft Deletion Abstract Model
class SoftDeletionModel(models.Model):
deleted_at = models.DateTimeField(blank=True, null=True)
objects = SoftDeletionManager()
all_objects = SoftDeletionManager(alive_only=False)
class Meta:
abstract = True
def delete(self):
self.deleted_at = timezone.now()
self.save()
def hard_delete(self):
super(SoftDeletionModel, self).delete()
Object Manager
class SoftDeletionManager(models.Manager):
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop('alive_only', True)
super(SoftDeletionManager, self).__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return SoftDeletionQuerySet(self.model).filter(deleted_at=None)
return SoftDeletionQuerySet(self.model)
def hard_delete(self):
return self.get_queryset().hard_delete()
QuerySet
class SoftDeletionQuerySet(QuerySet):
def delete(self):
return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now())
def hard_delete(self):
return super(SoftDeletionQuerySet, self).delete()
def alive(self):
return self.filter(deleted_at=None)
def dead(self):
return self.exclude(deleted_at=None)
For explanation, see Soft Deletion in Django
The DeleteView inherits DeletionMixin so you can just predefine the delete method.
DeletionMixin
Override delete method in DeleteView as follows
class Example(DeleteView):
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
"""
self.object = self.get_object()
self.object.is_deleted = True # Declare a boolean field is_deleted in your model. Default value is Flase.
return HttpResponseRedirect(self.get_success_url())

Get max value from django rest

I am using django rest framework and instead of getting the complete list of an object, I only want to get a specific value, like max(date) for example. Here is the code I am using:
My Serializer
class MoodSerializer(serializers.ModelSerializer):
class Meta:
model = Mood
fields = ('date', 'rating')
def create(self, validated_data):
return Mood.objects.create(**validated_data)
My View
class MoodList(generics.ListCreateAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
class MoodDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
My URLS
url(r'^mood/$', views.MoodList.as_view()),
url(r'^mood/(?P<pk>[0-9]+)/$', views.MoodDetail.as_view()),
So if fire a GET on "max_mood" I want the latest Mood entry from the db.
Instead of Mood.objects.all(), why not sort Mood objects by some_criteria and get the first one (which will be the max one). Like this:
mood = Mood.objects.order_by('-some_criteria').first()
There is another way but it requires additional queries:
from django.db.models import Max
maximum = Mood.objects.aggregate(m=Max('some_criteria')).get('m')
max_mood = Mood.objects.get(some_criteria=maximum)
view
class MaxMoodView(generics.RetrieveAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
def retrieve(self, request, *args, **kwargs):
mood=self.get_queryset().order_by('-date').first()
return Response(self.get_serializer(instance=mood))
urls
url(r'^mood/max_mood/$', views.MaxMoodView.as_view()),
Alright I got it working with the help of the comments:
view:
class MaxMoodView(generics.ListCreateAPIView):
queryset = Mood.objects.all()
serializer_class = MoodSerializer
def get(self, request, format=None):
mood=self.get_queryset().order_by('-date').first()
serializer = MoodSerializer(mood)
return Response(serializer.data)
urls:
url(r'^mood/max_mood/$', views.MaxMoodView.as_view()),

Optimizing djangos count() method

So I have this queryset:
from django.contrib.gis.db.models.query import GeoQuerySet
from django.db import models as base_models
class RestaurantsQuerySet(GeoQuerySet):
def get_list(self, lng, lat):
reference_point = Point(lng, lat, srid=SRID)
return self.annotate(rating=models.Avg('comments__rating'))\
.annotate(distance=Distance('location', reference_point))
def count(self):
return self.values('id').aggregate(count=base_models.Count('id'))['count']
I thought that the query would look something like:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
GROUP BY "restaurants_restaurant"."id") subquery
And instead django orm creates this little monstrosity:
SELECT COUNT("__col1")
FROM (
SELECT "restaurants_restaurant"."id" AS Col1, "restaurants_restaurant"."id" AS "__col1"
FROM "restaurants_restaurant"
LEFT OUTER JOIN "comments_comment" ON ("restaurants_restaurant"."id" = "comments_comment"."restaurant_id")
GROUP BY "restaurants_restaurant"."id", ST_Distance_Sphere("restaurants_restaurant"."location",
ST_GeomFromEWKB('\x0101000020e61000003eb555a41d2d4b405a338d81d0a73240'::bytea
))) subquery
First method to be called is get_list. It looks as if django would "remember" that call and that the qs was annotated with rating and distance and places it also into the count query. So I guess the question is - how do I "reset" this queryset to the state before annotating it?
EDIT:
Okay, seems my question was not complete. I also have a RestaurantsList view defined as follows:
class RestaurantList(generics.ListAPIView):
def get_queryset(self):
return Restaurant.objects.get_list(self._lng, self._lat)
I took a look into the entrails of django-rest-framework and I can see this:
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
So it looks like it always uses the queryset that was returned from the get_queryset method and since it's annotated with distance and rating it ends up being included in the count query. Still no solution to this...
After browsing through django-rest-framework code I came up with this idea:
Override default Paginator class for a given view:
class RestaurantList(generics.ListAPIView):
pagination_class = custom.LimitOffsetPagination
Create custom pagination class:
class LimitOffsetPagination(pagination.LimitOffsetPagination):
def __init__(self):
self._countable_queryset = None
self._was_counted = False
def was_initialized(self):
return self._countable_queryset is not None
def set_raw_queryset_for_count(self, queryset: QuerySet):
self._countable_queryset = queryset
def paginate_queryset(self, queryset, request, view=None):
self.limit = self.get_limit(request)
if self.limit is None:
return None
self.offset = self.get_offset(request)
self.count = self._countable_queryset.count()
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True
return list(queryset[self.offset:self.offset + self.limit])
Override views paginator property:
#property
def paginator(self):
paginator = super().paginator
if not paginator.was_initialized():
paginator.set_raw_queryset_for_count(Restaurant.objects.all())
return paginator
Now the count query will look a bit more friendly
SELECT COUNT(*) AS "__count" FROM "restaurants_restaurant"

Categories

Resources