In Django you can use the exclude to create SQL similar to not equal. An example could be.
Model.objects.exclude(status='deleted')
Now this works great and exclude is very flexible. Since I'm a bit lazy, I would like to get that functionality when using get_object_or_404, but I haven't found a way to do this, since you cannot use exclude on get_object_or_404.
What I want is to do something like this:
model = get_object_or_404(pk=id, status__exclude='deleted')
But unfortunately this doesn't work as there isn't an exclude query filter or similar. The best I've come up with so far is doing something like this:
object = get_object_or_404(pk=id)
if object.status == 'deleted':
return HttpResponseNotfound('text')
Doing something like that, really defeats the point of using get_object_or_404, since it no longer is a handy one-liner.
Alternatively I could do:
object = get_object_or_404(pk=id, status__in=['list', 'of', 'items'])
But that wouldn't be very maintainable, as I would need to keep the list up to date.
I'm wondering if I'm missing some trick or feature in django to use get_object_or_404 to get the desired result?
Use django.db.models.Q:
from django.db.models import Q
model = get_object_or_404(MyModel, ~Q(status='deleted'), pk=id)
The Q objects lets you do NOT (with ~ operator) and OR (with | operator) in addition to AND.
Note that the Q object must come before pk=id, because keyword arguments must come last in Python.
The most common use case is to pass a Model. However, you can also pass a QuerySet instance:
queryset = Model.objects.exclude(status='deleted')
get_object_or_404(queryset, pk=1)
Django docs example:
https://docs.djangoproject.com/en/1.10/topics/http/shortcuts/#id2
There's another way instead of using Q objects. Instead of passing the model to get_object_or_404 just pass the QuerySet to the function instead:
model = get_object_or_404(MyModel.objects.filter(pk=id).exclude(status='deleted'))
One side effect of this, however, is that it will raise a MultipleObjectsReturned exception if the QuerySet returns multiple results.
get_object_or_404 utilizes the get_queryset method of the object manager. If you override the get_queryset method to only return items that aren't "deleted" then get_object_or_404 will automatically behave as you want. However, overriding get_queryset like this will likely have issues elsewhere (perhaps in the admin pages), but you could add an alternate manager for when you need to access the soft deleted items.
from django.db import models
class ModelManger(models.Manger):
def get_queryset(self):
return super(ModelManger, self).get_queryset().exclude(status='deleted')
class Model(models.Model):
# ... model properties here ...
objects = ModelManager()
all_objects = models.Manager()
So if you need only non-deleted items you can do get_object_or_404(Models, id=id) but if you need all items you can do get_object_or_404(Models.all_objects, id=id).
Related
Using Django 11 with PostgreSQL db. I have the models as shown below. I'm trying to prefetch a related queryset, using the Prefetch object and prefetch_related without assigning it to an attribute.
class Person(Model):
name = Charfield()
#property
def latest_photo(self):
return self.photos.order_by('created_at')[-1]
class Photo(Model):
person = ForeignKey(Person, related_name='photos')
created_at = models.DateTimeField(auto_now_add=True)
first_person = Person.objects.prefetch_related(Prefetch('photos', queryset=Photo.objects.order_by('created_at'))).first()
first_person.photos.order_by('created_at') # still hits the database
first_person.latest_photo # still hits the database
In the ideal case, calling person.latest_photo will not hit the database again. This will allow me to use that property safely in a list display.
However, as noted in the comments in the code, the prefetched queryset is not being used when I try to get the latest photo. Why is that?
Note: I've tried using the to_attr argument of Prefetch and that seems to work, however, it's not ideal since it means I would have to edit latest_photo to try to use the prefetched attribute.
The problem is with slicing, it creates a different query.
You can work around it like this:
...
#property
def latest_photo(self):
first_use_the_prefetch = list(self.photos.order_by('created_at'))
then_slice = first_use_the_prefetch[-1]
return then_slice
And in case you want to try, it is not possible to use slicing inside the Prefetch(query=...no slicing here...) (there is a wontfix feature request for this somewhere in Django tracker).
A have piece of code, which fetches some QuerySet from DB and then appends new calculated field to every object in the Query Set. It's not an option to add this field via annotation (because it's legacy and because this calculation based on another already pre-fetched data).
Like this:
from django.db import models
class Human(models.Model):
name = models.CharField()
surname = models.CharField()
def calculate_new_field(s):
return len(s.name)*42
people = Human.objects.filter(id__in=[1,2,3,4,5])
for s in people:
s.new_column = calculate_new_field(s)
# people.somehow_reorder(new_order_by=new_column)
So now all people in QuerySet have a new column. And I want order these objects by new_column field. order_by() will not work obviously, since it is a database option. I understand thatI can pass them as a sorted list, but there is a lot of templates and other logic, which expect from this object QuerySet-like inteface with it's methods and so on.
So question is: is there some not very bad and dirty way to reorder existing QuerySet by dinamically added field or create new QuerySet-like object with this data? I believe I'm not the only one who faced this problem and it's already solved with django. But I can't find anything (except for adding third-party libs, and this is not an option too).
Conceptually, the QuerySet is not a list of results, but the "instructions to get those results". It's lazily evaluated and also cached. The internal attribute of the QuerySet that keeps the cached results is qs._result_cache
So, the for s in people sentence is forcing the evaluation of the query and caching the results.
You could, after that, sort the results by doing:
people._result_cache.sort(key=attrgetter('new_column'))
But, after evaluating a QuerySet, it makes little sense (in my opinion) to keep the QuerySet interface, as many of the operations will cause a reevaluation of the query. From this point on you should be dealing with a list of Models
Can you try it functions.Length:
from django.db.models.functions import Length
qs = Human.objects.filter(id__in=[1,2,3,4,5])
qs.annotate(reorder=Length('name') * 42).order_by('reorder')
This is a django-filter app specific guestion.
Has anyone tried to introduce conditions for the filters to query according to the condition?
Let me give an example:
Suppose we have a Product model. It can be filtered according to its name and price.
The default django-filter behaviour is that, as we use more filters and chain them together, they filter data using AND statements (it narrows the search).
I'd like to change this behaviour and add a ChoiceFilter, say with two options: AND as well as OR. From this point, the filter should work according to what a user have selected.
Eg. if a user query for products with name__startswith="Juice" OR price__lte=10.00, it should list all the products with names starting with Juice as well as products with price below 10.00.
Django-filter docs say that the filter can take an argument:
action
An optional callable that tells the filter how to handle the queryset. It recieves a
QuerySet and the value to filter on and should return a Queryset that is filtered
appropriately.
which seems to be what I am looking for, but the docs lacks any further explanation. Suggestions please?
#EDIT:
This is views.py:
def product_list(request):
f = ProductFilter(request.GET, queryset=Product.objects.all())
return render_to_response('my_app/template.html', {'filter': f})
Because of the way the final queryset is constructed, making each filter be ORed together is difficult. Essentially, the code works like this:
FilterSet, filterset.py line 253:
#property
def qs(self):
qs = self.queryset.all()
for filter_ in self.filters():
qs = filter_.filter(qs)
Filters, filters.py line 253:
def filter(self, qs):
return qs.filter(name=self.value)
Each filter can decide how to apply itself to the incoming queryset, and all filters, as currently implemented, filter the incoming queryset using AND. You could make a new set of filters that OR themselves to the incoming queryset, but there is no way of overriding the behaviour from the FilterSet side.
action won't cut it. This callback is used for particular filter field and only has access to that field's value.
The cleanest way would be to create multi-widget filter field, similar to RangeField. Check out the source.
So instead two date fields you use name, price and the logic type [AND|OR] as fields, this way you have access to all these values at once to use in custom queryset.
EDIT 1:
This is a little gist I wrote to show how to query two fields with selected operator.
https://gist.github.com/mariodev/6689472
Usage:
class ProductFilter(django_filters.FilterSet):
nameprice = NamePriceFilter()
class Meta:
model = Product
fields = ['nameprice']
It's actually not very flexible in terms of re-usage, but certainly can be re-factored to make it useful.
In order to make filters work with OR, you should make a subclass of FilterSet and override qs from Tim's answer like this:
#property
def qs(self):
qs = self.queryset.none()
for filter_ in self.filters():
qs |= filter_.filter(self.queryset.all())
I haven't tested this, but I think you got the idea. QuerySets support bitwise operations, so you can easily combine results of two filters with OR.
class FileFilterSet(django_filters.FilterSet):
class Meta:
model = File
fields = ['project']
def __init__(self, *args, **kwargs):
super(FileFilterSet, self).__init__(*args, **kwargs)
for name, field in self.filters.items():
if isinstance(field, ModelChoiceFilter):
field.extra['empty_label'] = None
field.extra['initial'] = Project.objects.get(pk=2)
# field.extra['queryset'] = Project.objects.filter(pk=2)
class FileFilter(FilterView):
model = File
template_name = 'files_list.html'
filterset_class = FileFilterSet
I am trying to test my Django views. This view passes a QuerySet to the template:
def merchant_home(request, slug):
merchant = Merchant.objects.get(slug=slug)
product_list = merchant.products.all()
return render_to_response('merchant_home.html',
{'merchant': merchant,
'product_list': product_list},
context_instance=RequestContext(request))
and test:
def test(self):
"Merchant home view should send merchant and merchant products to the template"
merchant = Merchant.objects.create(name='test merchant')
product = Product.objects.create(name='test product', price=100.00)
merchant.products.add(product)
test_client = Client()
response = test_client.get('/' + merchant.slug)
# self.assertListEqual(response.context['product_list'], merchant.products.all())
self.assertQuerysetEqual(response.context['product_list'], merchant.products.all())
EDIT
I am using self.assertQuerysetEqual instead of self.assertListEqual. Unfortunately this still doesn't work, and the terminal displays this:
['<Product: Product object>'] != [<Product: Product object>]
assertListEqual raises: 'QuerySet' object has no attribute 'difference' and
assertEqual does not work either, although self.assertSetEqual(response.context['product_list'][0], merchant.products.all()[0]) does pass.
I assume this is because the QuerySets are different objects even though they contain the same model instances.
How do I test that two QuerySets contain the same data? I am even testing this correctly? This is my 4th day learning Django so I would like to know best practices, if possible. Thanks.
By default assertQuerysetEqual uses repr() on the first argument. This is why you were having issues with the strings in the queryset comparison.
To work around this you can override the transform argument with a lambda function that doesn't use repr():
self.assertQuerysetEqual(queryset_1, queryset_2, transform=lambda x: x)
Use assertQuerysetEqual, which is built to compare the two querysets for you. You will need to subclass Django's django.test.TestCase for it to be available in your tests.
I just had the same problem. The second argument of assertQuerysetEqual needs to be a list of the expected repr()s as strings. Here is an example from the Django test suite:
self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"], ordered=False)
I ended up solving this issue using map to repr() each entry in the queryset inside the self.assertQuerysetEqual call, e.g.
self.assertQuerysetEqual(queryset_1, map(repr, queryset_2))
An alternative, but not necessarily better, method might look like this (testing context in a view, for example) when using pytest:
all_the_things = Things.objects.all()
assert set(response.context_data['all_the_things']) == set(all_the_things)
This converts it to a set, which is directly comparable with another set. Be careful with the behaviour of set though, it might not be exactly what you want since it will remove duplicates and ignore the order of objects.
I found that using self.assertCountEqual(queryset1, queryset2) also solves the issue.
I have an issue with the django-filter application: how to hide the items that will produce zero results. I think that there is a simple method to do this, but idk how.
I'm using the LinkWidget on a ModelChoiceFilter, like this:
provider = django_filters.ModelChoiceFilter(queryset=Provider.objects.all(),
widget=django_filters.widgets.LinkWidget)
What I need to do is filter the queryset and select only the Provider that will produce at least one result, and exclude the others.
There is a way to do that?
Basically, you need to apply filters, and then apply them again, but on newly-generated queryset. Something like this:
f = SomeFilter(request.GET)
f = SomeFilter(request.GET, queryset=f.qs)
Now when you have correct queryset, you can override providers dynamically in init:
def __init__(self, **kw):
super(SomeFilter, self).__init__(**kw)
self.filters['provider'].extra['queryset'] = Provider.objects.filter(foo__in=self.queryset)
Not pretty but it works. You should probably encapsulate those two calls into more-efficient method on filter.
Maybe the queryset can be a callable instead of a 'real' queryset object. This way, it can be generated dynamically. At least this works in Django Models for references to other models.
The callable can be a class method in you Model.
If I understand your question correctly I believe you want to use the AllValuesFilter.
import django_tables
provider = django_filters.AllValuesFilter(
widget=django_filters.widgets.LinkWidget)
More information is available here: http://github.com/alex/django-filter/blob/master/docs/ref/filters.txt#L77