Django model manager get queryset depending on lookup value - python

Assuming I have model and custom manager like this:
class FooManager(models.Manager):
def get_query_set(self):
super(FooManager, self).get_query_set()
class Foo(models.Model):
name = models.CharField(max_length=10)
status = models.BooleanField(default=False)
objects = FooManager()
I feel like this should be pretty simple but looked through documentation and "self" probably I missed something obvious. My question is how I can return object/queryset depending on lookups passed during actual query?
For example:
objs = Foo.objects.all()
print(objs, "Objects queryset only with status set to True")
objs = Foo.objects.filter(name__icontains='a')
print(objs, "Objects queryset with 'a' in name and status set to True")
objs = Foo.objects.filter(name__icontains='a', status=False)
print(objs, "Objects queryset with 'a' in name and status set to False")
I could imagine something looks like this sample:
class FooManager(models.Manager):
def get_query_set(self):
status = self.passed_lookups['status']
queryset = super(FooManager, self).get_query_set()
if status is None:
queryset = queryset.filter(status=True)
else:
queryset = queryset.filter(status=status)
return queryset

queryset of manager class has no access to query passed to it.
you can do this by creating two query manager class for your model. see below
class FooManager(models.Manager):
def get_query_set(self):
super(FooManager, self).get_query_set().filter(status=True)
class Foo(models.Model):
name = models.CharField(max_length=10)
status = models.BooleanField(default=False)
objects = FooManager()
all_objects = models.Manager()
in your code you must use Foo.objects.filter when you want to filter status and use Foo.all_objects when you dont want to filter status

Related

Is it possible to override filter lookup with predefined values with custom Manager/Queryset in Django

I am trying to implement the behaviour for my models so that when you are deleting the object(s) it is not deleting physically, but just add some attribute to state that it was deleted.
So I created custom queryset, manager and mixin to apply for each created model:
class StateQuerySet(models.query.QuerySet):
def delete(self):
self.update(active=False)
class StateManager(models.Manager):
def get_queryset(self):
return StateQuerySet(self.model, using=self._db).filter(active=True)
class ModelMixin(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
active = models.BooleanField(default=True)
objects = StateManager()
def delete(self, *args, **kwargs):
self.active = False
self.save()
class Meta:
abstract = True
And the models:
class Organizer(ModelMixin, models.Model):
name = models.CharField(max_length=256)
class EventData(ModelMixin, models.Model):
caption = models.CharField(max_length=512)
description = models.TextField(blank=True, default='')
organizer = models.ForeignKey(Organizer, on_delete=models.CASCADE)
So, the idea, is when I do any:
Organizer.objects.all() / Organizer.objects.filter(name__startswith='<some_start_prefix>')
I will recieve Organizer objects only which are active=True (i.e. "not deleted").
All looks good, but when I have related objects, there is an issue.
If, for example, I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>')
It will return all EventData objects even the 'deleted'.
But if I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>', organizer__active=True)
All works as expected and only 'active' records returned. So I don't want to use:
organizer__active=True
for each query in the views.
I've read the docs but still don't understand how to create this with custom Manager and QuerySet. Could you please help, or guide how to do this? What I am missing?
So to summarize: when I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>')
I want to recieve all EventData objects where Organizer.active set to True, but without writing additional organizer__active=True each time in the client code. Is it possible to do it in the Manager or QuerySet?
Ok, so you can indeed add some custom queries to your model's queryset.
You might do something like;
def filter_active_organizer(self, prefix):
return self.filter(organizer__name__startswith=prefix, organizer__active=True)
Let me illustrate with something from a project I've got;
from django.db.models import Q, QuerySet
from django.utils import timezone
class EventQuerySet(QuerySet):
"""
Custom queryset/manager for the Event model...adds common filtering
operations
"""
def filter_open(self):
"""
Filter events in this queryset to only include those whose entry
process is currently open.
"""
now = timezone.now()
return self.filter(
Q(entry_open__isnull=True) | Q(entry_open__lte=now),
Q(entry_close__isnull=True) | Q(entry_close__gt=now)
)
def filter_closed(self):
"""
Filter events in this queryset to only include those whose entry
process is currently closed.
"""
now = timezone.now()
return self.filter(
(Q(entry_open__isnull=False) & Q(entry_open__gt=now)) |
(Q(entry_close__isnull=False) & Q(entry_close__lte=now))
)
EventManager = EventQuerySet.as_manager()
class Event(models.Model):
class Meta:
app_label = 'entry'
verbose_name = _('Event')
verbose_name_plural = _('Events')
objects = EventManager
These queries can then be accessed using Event.objects.filter_open() or added to admin with a filter class;
class FilterStatus(admin.SimpleListFilter):
"""
List filter to filter events by whether they are open or closed
(this is a computed value)
"""
title = 'entry status'
parameter_name = 'entry_status'
def lookups(self, request, model_admin):
"""
Return the lookup choices for this filter
"""
return [
('open', _('Open')),
('closed', _('Closed')),
]
def queryset(self, request, queryset):
"""
Perform the filtering (if required)
"""
if self.value() == 'open':
return queryset.filter_open()
if self.value() == 'closed':
return queryset.filter_closed()
return queryset

Django REST Framework customize update method

My application has the following structure:
models.py
class EventHost(models.Model):
hostid = models.ForeignKey(Host, on_delete=models.PROTECT)
eventid = models.ForeignKey(Event, on_delete=models.CASCADE)
someparam = models.CharField(max_length=100, blank=True, null=True)
...
class Meta:
unique_together = ("hostid", "event")
class Host(models.Model):
hostid = models.IntegerField(primary_key=True)
hostname = models.CharField(max_length=100)
...
class Event(models.Model):
eventid = models.AutoField(primary_key=True)
eventname = models.CharField(max_length=100)
...
hosts = models.ManyToManyField(Host, through='EventHost')
views.py
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.order_by('-date')
serializer_class = EventSerializer
class HostViewSet(viewsets.ModelViewSet):
queryset = Host.objects.order_by('-hostid')
serializer_class = HostSerializer
class EventHostViewSet(viewsets.ModelViewSet):
queryset = EventHost.objects.all()
serializer_class = EventHostSerializer
Currently to update EventHost table I'm doing http put providing id (which is primary key) in my url .
I'd like to be able to update EventHost providing hostname and eventname (which will be passed in url) instead of id.
Using SQL it would look like this:
update eventhost set someparam='somevalue' from eventhost as a, event as b, host as c where b.id = a.eventid and c.hostid = a.hostid and b.eventname='myevent' and c.hostname='myhost';
From documentation I understood that I would need to modify the default update method for the viewset or/and modify queryset. Any ideas how should it be achieved?
You can override get_object method and use your parameters provided from url like that:
class EventHostViewSet(viewsets.ModelViewSet):
queryset = EventHost.objects.all()
serializer_class = EventHostSerializer
def get_object(self):
return EventHost.objects.get(hostid=self.kwargs['hostid'],eventid=self.kwargs['eventid'])
In this way, you must manage if there is no record with this query scenario, as custom
Assuming that you have properly constructed URL.
Edited EventHostViewSet.get_object method:
class EventHostViewSet(viewsets.ModelViewSet):
...
lookup_field = 'eventname'
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
filter_kwargs = {'hostid__hostname': self.kwargs.get('hostname'),
'eventid__eventname': self.kwargs.get('eventname')}
obj = get_object_or_404(queryset, **filter_kwargs)
self.check_object_permissions(self.request, obj)
return obj
EventHostViewSet registration:
router.register(rf'event-hosts/(?P<hostname>[.]+)', EventViewSet)
Some comments about your problem:
You will have problem when in your system will exist two EventHost with same hostid__hostname and eventid__event because queryset get method should only return ONE record
in DRF PUT method needs all fields to be provided in request data, if you want to update selected fields you should use PATCH method or override update viewset method (set partial to True)
EDITED AGAIN:
This is really bad design, you should not do this like that (somehow you should use id / maybe #action decorator to construct specific url for updating like that).

Django: Can I use objects.filter() for generic foreignkey?

symbol.py
class Symbol(BaseModel):
name = models.CharField(max_length=30,)
class Meta:
abstract = True
class StockSymbol(Symbol):
market = models.CharField(max_length=10,)
my_daily_price = GenericRelation(MyDailyPrice)
daily_price.py
class DailyPrice(BaseModel):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
abstract = True
class MyDailyPrice(DailyPrice):
open = models.DecimalField(
max_digits=15,
decimal_places=2,
)
What I want to do is,
symbol = StockSymbol.objects.first()
MyDailyPrice.objects.filter(content_object=symbol)
But it occured errors:
FieldError: Field 'content_object' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.
StockSymbol already has GenericRelation. What's wrong with it?
Or do I have to override ojbect manager?
You can filter with content_type and object_id, instead of content_object.
from django.contrib.admin.options import get_content_type_for_model
symbol = StockSymbol.objects.first()
MyDailyPrice.objects.filter(content_type=get_content_type_for_model(symbol), object_id=symbol.pk)
I wrapped up #Akash's answer in a method to be added to a custom Manager or QuerySet:
def gfks(self, **kwargs):
filters = {}
for field, obj in kwargs.items():
gfk = self.model._meta.get_field(field)
filters[gfk.ct_field] = ContentType.objects.get_for_model( obj )
filters[gfk.fk_field] = obj.pk
return self.filter(**filters)
For example, if you had a model called Comparison with two GFKs called product1 and product2, and added this method, usage would look like:
comp = Comparison.objects.gfks(product1=foo, product2=bar)
Would be nice if Django's contenttypes app provided some similar sugar automatically, but I'll settle for adding this to my BaseQuerySet class in the meantime.

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')
...

Default filter in Django model

Is is possible to set a default filter in Django models?
Something like:
class MyModel(models.Model):
timestamp = models.DateTimeField(default=datetime.utcnow)
active = models.BooleanField(default=True)
class Meta:
ordering = ['-timestamp']
filtering = [active=True]
You'll have to override the manager:
class MyModelManager(models.Manager):
def get_queryset(self):
return super(MyModelManager, self).get_queryset().filter(active=True)
class MyModel(models.Model):
timestamp = models.DateTimeField(default=datetime.utcnow)
active = models.BooleanField(default=True)
objects = MyModelManager()
class Meta:
ordering = ['-timestamp']
get_queryset was get_query_set before Django 1.6
I was able to accomplish what I wanted by adding managers to the model. I also created an abstract base class to make it easier to add this to other models without having to replicate the same code - here is the modified example:
class MyActiveManager(models.Manager):
def get_queryset(self):
return super(MyModelManager, self).get_queryset().filter(active=True)
class MyInactiveManager(models.Manager):
def get_queryset(self):
return super(MyModelManager, self).get_queryset().filter(active=False)
class AbstractModel(models.Model):
timestamp = models.DateTimeField(default=datetime.utcnow)
active = models.BooleanField(default=True)
objects = MyActiveManager()
objects_inactive = MyInactiveManager()
objects_all = models.Manager()
class Meta:
abstract = True
ordering = ['-timestamp']
class MyModel(AbstractModel):
# Define active-enabled model here
Now, any model I want to have an "active" (and "timestamp" in this example) field can just inherit from the base model. When I use MyModel.objects.all() - I get all objects with active=True - this is especially useful if I already have alot of code using the objects manager. If I want only inactive results, I use MyModel.objects_inactive.all(), and if I want all records regardless of the value of active, I use MyModel.objects_all.all()

Categories