How can I prefetch related objects in Django? - django

Assuming I have the following model with related methods:
class Turbine(models.Model):
...
pass
def relContracts(self):
contracts = self.contracted_turbines.all()
return contracts
class Contract(models.Model):
turbines = models.ManyToManyField(Turbine,related_name='contracted_turbines')
def _contracted_windfarm_name(self):
windfarms = self.turbines.order_by().values_list("wind_farm__name", flat=True).distinct().select_related
if len(windfarms) == 1:
return windfarms[0]
else:
return ", ".join([str(x) for x in windfarms])
contracted_windfarm_name = property(_contracted_windfarm_name)
def _turbine_age(self):
first_commisioning = self.turbines.all().aggregate(first=Min('commisioning'))['first']
start = self.start_operation.year
age = start - first_commisioning.year
return age
turbine_age = property(_turbine_age)
Django-debug-toolbar tells me, that the functions "_contracted_windfarm_name" and "_turbine_age" result in database duplicates for each contract.
My contracts queryset is received by the following get_queryset method where I already prefetched 'turbines' for other methods succesfully:
def get_queryset(self, **kwargs):
qs = super(ContractTableView, self).get_queryset().filter(active=True).prefetch_related('turbines', 'turbines__wind_farm')
self.filter = self.filter_class(self.request.GET, queryset=qs)
return self.filter.qs
I tried prefetching 'turbines__contracted_turbines' without being able to reduce the number of duplicates.
The _contracted_windfarm_name method is used to populate a column of a django-tables2 method as follows:
contracted_windfarm = dt2.Column(accessor='contracted_windfarm_name', verbose_name='Wind Farm', orderable=False)
Where am I mistaking? How can I prefetch the associated contracts of a turbine?
SOLUTION: First problem
I added a simple annotation to the queryset within the get_queryset() method:
def get_queryset(self, **kwargs):
qs = super(ContractTableView, self).get_queryset()\
.filter(active=True).prefetch_related('turbines', 'turbines__wind_farm')\
.annotate(first_com_date=Case(When(turbines__commisioning__isnull=False, then=Min('turbines__commisioning'))))
self.filter = self.filter_class(self.request.GET, queryset=qs)
return self.filter.qs
This leads to a slight change in the _turbine_age() method:
def _turbine_age(self):
first_commisioning = self.first_commisioning
start = self.start_operation.year
age = start - first_commisioning.year
return age
turbine_age = property(_turbine_age)
SOLUTION: second problem
With the turbines__wind_farm being prefetched in the get_queryset() method, there is no need to call the distinct() method:
def _contracted_windfarm_name(self):
windfarms = list(set([str(x.wind_farm.name) for x in self.turbines.all()]))
if len(windfarms) == 1:
return windfarms[0]
else:
return ", ".join([str(x) for x in windfarms])
contracted_windfarm_name = property(_contracted_windfarm_name)
All duplicated queries could be removed!
Thanks to #dirkgroten for his valuable contributions!

from django.db.models import Min
class ContractManager(models.Manager):
def with_first_commissioning(self):
return self.annotate(first_commissioning=Min('turbines__commissioning'))
class Contract(models.Model):
objects = ContractManager()
...
then Contract.objects.with_first_commissioning() returns you a Queryset with the additional first_commissioning value for each Contract. So in Contract._turbine_age() you can just remove the first line.
Now the windfarm names case is a bit more complex. If you're using Postgresql (which supports StringAgg) you could similarly add in your ContractManager this queryset:
from django.db.models import Subquery, OuterRef
from django.contrib.postgres.aggregates import StringAgg
def with_windfarms(self):
wind_farms = WindFarm.objects.filter('turbines__contract'=OuterRef('pk')).order_by().distinct().values('turbines__contract')
wind_farm_names = wind_farms.annotate(names=StringAgg('name', delimiter=', ')).values('names')
return self.annotate(wind_farm_names=Subquery(wind_farm_names))
then in your _contracted_windfarm_name() method, you can access self.wind_farm_names assuming you're looping through the results of the queryset (you should probably check with hasattr in case your method gets used in a different way).
If you're not on Postgresql, then just change the queryset to perform a prefetch_related and then make sure you don't add any query-related logic after that:
from django.db.models import Prefetch
def with_windfarms(self):
return self.prefetch_related(Prefetch('turbines', queryset=Turbine.objects.order_by().select_related('wind_farm').distinct('wind_farm__name')))
so that in your _contracted_wind_farms method, you can do [str(x.wind_farm.name) for x in self.turbines]
In both cases, I'm assuming somewhere in your views you loop through the contracts in the queryset:
for contract in Contract.objects.with_first_commissioning():
contract._turbine_age()...
for contract in Contract.objects.with_windfarms():
contract._contracted_windfarm_name()...

Related

Querying for models based on time range in Django REST Framework

I have a DB with models that each have two timestamps.
class Timespan(models.Model):
name = models.CharField(null=False)
start_time = models.DateTimeField(null=False)
end_time = models.DateTimeField(null=False)
I want to be able to query for these objects based on a timestamp range. A GET request would also have a start and end time, and any Timespans that overlap would be returned.
However I'm not sure how to construct a GET Request to do this, nor the View in Django.
Should the GET request just use url parameters?
GET www.website.com/timespan?start=1000&end=1050
or pass it in the body? (if it even makes a difference)
My view currently looks like this:
class TimespanViewSet(OSKAuthMixin, ModelViewSet):
queryset = Timespan.objects.filter()
serializer_class = TimespanSerializer
Which allows me to return obj by ID GET www.website.com/timestamp/42.
I expect I'll need a new viewset for this query. I know how to add a ViewSet with a nested urlpath, but shouldn't there be a way to send a request to /timespan the inclusion of a "start" and "end" parameter changes what is returned?
you can do the conversion and many ways but I guess the easiest way is like this:
...
from django.utils import timezone
...
class TimespanViewSet(OSKAuthMixin, ModelViewSet):
queryset = Timespan.objects.all()
serializer_class = TimespanSerializer
def get_queryset(self,*args,**kwargs):
start_time = self.request.GET.get("start_time",None)
end_time = self.request.GET.get("end_time",None)
if start_time and end_time :
# convert timestamps to timezone objects
start_time_instance = timezone.datetime.fromtimestamp(start_time)
end_time_instance = timezone.datetime.fromtimestamp(end_time)
return self.queryset.filter(start_time=start_time_instance,end_time_instance=end_time_instance)
else:
# do some handling here
return self.queryset
You can customize get_queryset function in the following way:
from django.db.models import Q
class TimespanViewSet(OSKAuthMixin, ModelViewSet):
queryset = Timespan.objects.filter()
serializer_class = TimespanSerializer
def get_queryset(self):
start_timestamp = int(self.request.GET.get("start", "0"))
end_timestamp = int(self.request.GET.get("end", "0"))
if start_timestamp > 0 and start_timestamp > 0:
return Timespan.objects.filter(
Q(start_time___range = (start_timestamp, end_timestamp)) |
Q(end_time___range = (start_timestamp, end_timestamp))
)
else:
return Timespan.objects.all()

django request.GET in models

Is it possible in Django to have models method with request.GET ?
e.g.
class Car(models.Model):
owner = ForeignKey(Owner)
car_model = ...
def car_filter(self, request):
query = request.GET.get("q")
if query:
Car.objects.filter(owner = self.id.order_by('id')
else:
Car.objects.filter(owner = me).order_by('id'
)
?
Purely technically speaking, sure, you can - as long as you can pass the request object from the view. The example code you've posted is syntactically incorrect, but, something like this is technically possible. You just have to make sure that the method is class-method, not instance-method one (since you don't have any instances in this case):
class Car(models.Model):
...
#classmethod
def get_by_owner(cls, request):
query = request.GET.get("q")
if query:
return cls.objects.filter(owner=query)
elif request.user.is_authenticated():
return cls.objects.all()
def your_view(request):
cars = Car.get_by_owner(request)
...
However, DON'T DO THIS. It's a bad idea because you're moving your request processing logic to a model. Models should only care about the data, and user request handling is view's job.
So, I'd suggest to have all the logic in the views:
def your_view(request):
cars = Car.objects.all().order_by("id")
query = request.GET.get("q")
if query:
cars = cars.filter(owner=query)
...
If you need some complicated logic, that a lot of views would share, you can use model managers:
class CarManager(model.Manager):
def owned(self, username=None):
queryset = super(CarManager, self).get_query_set()
if username:
user = Owner.objects.get(username=username)
queryset = queryset.filter(owner=user)
return queryset
class Car(models.Model):
...
objects = CarManager()
...
def your_view(request):
query = request.GET.get("q")
cars = Car.objects.owned(query)
...
Possible, but you have to pass the request manually:
# inside your views
qs_ = car_object.car_filter(request)
but I dont see any sense in doing so.
Everything that has to do with request should go into views which is the place for request-response flow.
Actually you can handle this stuff in your view only
def yourview(self, request):
query = request.GET.get("q")
if query:
Car.objects.filter(owner = self.id).order_by('id')
else:
Car.objects.filter(owner = me).order_by('id')
else other wise you have to send your request object to the model function from your view.

Filtering Django REST framework using IN operator

I basically need something like /?status=[active,processed] or /?status=active,processed
My current setting is: 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) and it's only filtering one value correctly (/?status=active)
I think there is no inbuilt functionality for that. But you can implement a custom filter to do that. This custom filter you can use in your filterset.
import django_filters as df
class InListFilter(df.Filter):
"""
Expects a comma separated list
filters values in list
"""
def filter(self, qs, value):
if value:
return qs.filter(**{self.name+'__in': value.split(',')})
return qs
class MyFilterSet(df.FilterSet):
status = InListFilter(name='status')
You can use 'field_in' when using Class.object.filter method.
class FileterdListAPIView(generics.ListAPIView):
serializer_class = FooSerializer
def get_queryset(self):
user_profile = self.kwargs['pk']
if user_profile is not None:
workers = Worker.objects.filter(user_profile = user_profile)
queryset = MyModel.objects.filter(worker_in=(workers))
else:
return ''
return queryset

Convert a list or a raw set to a queryset

In my django views, I have the following code in my CBV:
def get_filtered_queryset(self, queryset):
filtered_queryset = # some code here
document_queryset = # some code here
return set(list(filtered_queryset) + list(document_queryset))
How can I change the above function to return a Queryset instead?
You can't cast list or set objects into a QuerySet.
Depending on the query you are looking at using, you can construct some pretty complex queries using Q Objects.
For example:
from django.db.models import Q
def get_filtered_queryset(self, queryset):
filtered_queryset = Q(question__startswith='What')
document_queryset = Q(pub_date=date(2005, 5, 2)
return Document.objects.get(filtered_queryset | document_queryset )

Django: how to use custom manager in get_previous_by_FOO()?

I have a simple model MyModel with a date field named publication_date. I also have a custom manager that filters my model based on this date field.
This custom manager is accessible by .published and the default one by .objects.
from datetime import date, datetime
from django.db import models
class MyModelManager(models.Manager):
def get_query_set(self):
q = super(MyModelManager, self).get_query_set()
return q.filter(publication_date__lte=datetime.now())
class MyModel(models.Model):
...
publication_date = models.DateField(default=date.today())
objects = models.Manager()
published = MyModelManager()
This way, I got access to all objects in the admin but only to published ones in my views (using MyModel.published.all() queryset).
I also have
def get_previous(self):
return self.get_previous_by_publication_date()
def get_next(self):
return self.get_next_by_publication_date()
which I use in my templates: when viewing an object I can link to the previous and next object using
{{ object.get_previous }}
The problem is: this returns the previous object in the default queryset (objects) and not in my custom one (published).
I wonder how I can do to tell to this basic model functions (get_previous_by_FOO) to use my custom manager.
Or, if it's not possible, how to do the same thing with another solution.
Thanks in advance for any advice.
Edit
The view is called this way in my urlconf, using object_detail from the generic views.
(r'^(?P<slug>[\w-]+)$', object_detail,
{
'queryset': MyModel.published.all(),
'slug_field': 'slug',
},
'mymodel-detail'
),
I'm using Django 1.2.
In fact, get_next_or_previous_by_FIELD() Django function (which is used by get_previous_by_publication_date...) uses the default_manager.
So I have adapted it to reimplement my own utility function
def _own_get_next_or_previous_by_FIELD(self, field, is_next):
if not self.pk:
raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
op = is_next and 'gt' or 'lt'
order = not is_next and '-' or ''
param = smart_str(getattr(self, field.attname))
q = Q(**{'%s__%s' % (field.name, op): param})
q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk})
qs = MyModel.published.filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
try:
return qs[0]
except IndexError:
def get_previous(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], False)
def get_next(self):
return self._own_get_next_or_previous_by_FIELD(MyModel._meta.fields[4], True)
This is not a very clean solution, as I need to hardcode the queryset and the field used, but at least it works.