QuerySet for non-empty TextField - django

For a model like:
class Item(models.Model):
notes = models.TextField(blank=True)
....
I'm attempting to do a simple queryset for all Items where the "notes" field is non-empty. Not finding mention of this capability in the docs, but via a comment on a bug report, discovered that you can actually compare with greater than:
items_with_notes = Item.objects.filter(notes__gt='')
This works, but feels like a hack. "Greater than" seems like it should be used for numeric comparisons, not for checking whether a text field is blank. Surprised not to find something like:
Item.objects.exclude(notes=blank)
Am I overlooking something, or is .filter(notes__gt='') the right way to do it?

.exclude(notes=u'')
Read more here: django.db.models.query.QuerySet.exclude

you can also use Q object:
from django.db.models import Q
Item.objects.filter(~Q(notes=''))

Related

Django filter a ForeignKey field when it is null

Let's say I have two tables in Django, TableA and TableB. Table A contains some boolean field, bool, and TableB contains a foreign key field, for_field to TableA, which can be Null.
class TableA(models.Model):
bool = models.BooleanField()
class TableB(models.Model):
for_field = models.ForeignKey('TableA', null=True)
If I want to filter TableB so as to get all the entries where for_field.bool is True or for_field is Null, what is the shortest way to achieve this?
I'm using .filter((Q(for_field__is_null=True) | Q(for_field__bool=True)), but I wonder if there's shorter code for this.
After some experiments it seems that .exclude(for_field__bool=False) will contain also for_field__isnull=True entries and will not raise any exceptions. You can be sure by executing .exclude(for_field__bool=False).filter(for_field__isnull=True) and see some results also.
And honestly I don't know which option is faster, but IMO your variant with two Q objects much more readable because it shows logic you're really want. So I actually suggest you to stick with it.
I'm pretty sure, that your option is the shortest possible (correct me if I'm wrong). That is because you can't do OR queries without Q objects.

Django conditional expressions in queries

I have two models, one with ForeignKey I'm trying to match. In order to do so, I'm looking up second model by a specific number and a date. The problem is it has two dates and I have to make decision on which date to choose. Under some circumstances it is set to NULL, in some it is not. If it is I have to get the second date field. I have something like this:
class MyModel1(models.Model):
model2_key = models.ForeignKey(MyModel2)
model1_date=...
model1_number=...
second model:
class MyModel2(models.Model):
model2_date1=...
model2_date2=...
model2_number=...
Now, how to make the choice? I have looked up documentation regarding F expressions, Q expressions, When expressions, Select and I'm a little bit confused. How can I wrtie a function that returns searched MyModel2 object? I have something like this, but it won't work.
def _find_model2(searched_date, searched_number):
searched_model2=MyModel2.objects.get(Q(model2_number=searched_number),
Q(When(model2_date1__e='NULL', then=model2_date2) |
Q(When(model2_date1__ne='NULL', then=model2_date1))=searched_date))
I am quite new to django, so any help will be appreciated.
I have made a workaround this issue, I don't think it's mostefficient and elegant one, but it works. Should anyone have a better solution please post it.
First, all objects whose corresponding dates match are called:
from_query = list(MyModel2.objects.filter(Q(model2_date1__range=
(datetime_min, datetime_max)) | Q(model2_date2__range=(datetime_min, datetime_max)),
model2_number=searched_number))
Then I iterate over found objects:
to_return = []
for item in from_query:
if item.model2_date1:
to_return.append(item)
elif datetime_min <= item.model2_date2 <= datetime_max:
to_return.append(item)
EDIT: I've come up with a solution. Assuring that model2_date1__isnull=True is enough. The solution now looks like this:
from_query = list(MyModel2.objects.get((Q(model2_date1__range=(datetime_min, datetime_max)) |
Q(Q(model2_date2__range=(datetime_min, datetime_max)),
Q(model2_date1__isnull=True)),
model2_number=searched_number))

Django startswith on fields

let's say that I have an Address model with a postcode field. I can lookup addresses with postcode starting with "123" with this line:
Address.objects.filter(postcode__startswith="123")
Now, I need to do this search the "other way around". I have an Address model with a postcode_prefix field, and I need to retrieve all the addresses for which postcode_prefix is a prefix of a given code, like "12345". So if in my db I had 2 addresses with postcode_prefix = "123" and "234", only the first one would be returned.
Something like:
Address.objects.filter("12345".startswith(postcode_prefix))
The problem is that this doesn't work.
The only solution I can come up with is to perform a filter on the first char, like:
Address.objects.filter(postcode_prefix__startswith="12345"[0])
and then, when I get the results, make a list comprehension that filters them properly, like this:
results = [r for r in results if "12345".startswith(r.postcode_prefix)]
Is there a better way to do it in django?
Edit: This does not answer the original question but how to word a query the other way around.
I think what you are trying to do with your "something like" line is properly written as this:
Address.objects.filter(postcode__startswith=postcode_prefix)
In SQL terms, what you want to achieve reads like ('12345' is the postcode you are searching for):
SELECT *
FROM address
WHERE '12345' LIKE postcode_prefix||'%'
This is not really a standard query and I do not see any possibility to achieve this in Django using only get()/filter().
However, Django offers a way to provide additional SQL clauses with extra():
postcode = '12345'
Address.objects.extra(where=["%s LIKE postcode_prefix||'%%'"], params=[postcode])
Please see the Django documentation on extra() for further reference. Also note that the extra contains pure SQL, so you need to make sure that the clause is valid for your database.
Hope this works for you.
Bit of a mouthful but you can do this by annotating your search value and then filtering against it. All happens pretty quickly in-database.
from django.db.models import Value as V, F, CharField
Address.objects.exclude(
postcode_prefix=''
).annotate(
postcode=Value('12345', output_field=CharField())
).filter(
postcode__startswith=F('postcode_prefix')
)
The exclude is only necessary if postcode_prefix can be empty. This would result in an SQL like '%', which would match every postcode.
I'm sure you could do this via a nice templated function these days too... But this is clean enough for me.
A possible alternative. (Have no idea how it compares to the accepted solution with a column as the second param to like, in execution time)
q=reduce(lambda a,b:a|b, [Q(postcode__startswith=postcode[:i+1]) for i in range(len(postcode))])
Thus, you generate all prefixes, and or them together...
The raw SQL query that would do that you need looks something like this:
select * from postal_code_table where '1234567' like postal_code||'%'
This query will select any postal_code from your table that is a substring of '1234567' and also must start from begining, ie: '123', '1234', etc.
Now to implement this in Django, the preferred method is using a custom look up:
from django.db.models.fields import Field
from django.db.models import Lookup
#Field.register_lookup
class LowerStartswithContainedBy(Lookup):
'''Postgres LIKE query statement'''
lookup_name = 'istartswithcontainedby'
def as_sql(self, compiler, connection):
lhs, lhs_params = self.process_lhs(compiler, connection)
rhs, rhs_params = self.process_rhs(compiler, connection)
params = lhs_params + rhs_params
return f"LOWER({rhs}) LIKE LOWER({lhs}) || '%%'", params
Now you can write a django query such as the following:
PostCode.objects.filter(code__istartswithcontainedby='1234567')
Similarly, if you are just looking for substring and do not require the startswith condition, simply modify the return line of as_sql method to the following:
return f"LOWER({rhs}) LIKE '%%' || LOWER({lhs}) || '%%'", params
For more detailed explanation, see my git gist Django custom lookup
A. If not the issue https://code.djangoproject.com/ticket/13363,
you could do this:
queryset.extra(select={'myconst': "'this superstring is myconst value'"}).filter(myconst__contains=F('myfield'))
Maybe, they will fix an issue and it can work.
B. If not the issue 16731 (sorry not providing full url, not enough rep, see another ticket above) you could filter by fields that added with '.annotate', with creation of custom aggreation function, like here:
http://coder.cl/2011/09/custom-aggregates-on-django/
C. Last and successful. I have managed to do this using monkeypatching of the following:
django.db.models.sql.Query.query_terms
django.db.models.fields.Field.get_prep_lookup
django.db.models.fields.Field.get_db_prep_lookup
django.db.models.sql.where.WhereNode.make_atom
Just defined custom lookup '_starts', which has reverse logic of '_startswith'

Django Haystack: Range search on MultiValueField

I was wondering if it is possible to do a range search on a MultiValueField. I have a model that looks like the following:
Book
Title = 'Awesome Book'
Prices = [ Decimal('10.00'), Decimal('15.00'), Decimal('20.00') ]
I am indexing the prices field with a MultiValueField and I would like to be able to do the follow:
sqs = SearchQueryResult()
sqs.filter(prices__gt=Decimal('10.00'), prices__lt=Decimal('20.00'))
Is this possible or do I have to use something else to do a range search on multiple values?
Update:
I forgot to mention that the __gt doesn't work and I think it's because it's indexing it as a list of strings. I found the following link where they talk about subclassing MultiValueField. I tried this but I can't get it to give me a list of decimals. The subclassed MultiValueFiled looks like the following:
class MultiValueDecimalField(fields.MultiValueField):
field_type = 'decimal'
One way to solve this problem is doing the following:
sqs.filter(prices__in=['%.2f' % (x/100.00) for x in range(1000, 2000)])
It's very ugly but it works. Still open to other answer though.
Same thing: when I applied filters __gte, __gt etc. I noticed that SearchQuerySet returns incorrect data. When I changed field type to FloatField everything started working right. looks like bug, or smth
Have you tried the range field lookup?
SearchQuerySet().filter(view_count__range=[3, 5])
http://django-haystack.readthedocs.org/en/latest/searchqueryset_api.html#field-lookups

Get last record in a queryset

How can I retrieve the last record in a certain queryset?
Django Doc:
latest(field_name=None) returns the latest object in the table, by date, using the field_name provided as the date field.
This example returns the latest Entry in the table, according to the
pub_date field:
Entry.objects.latest('pub_date')
EDIT : You now have to use Entry.objects.latest('pub_date')
You could simply do something like this, using reverse():
queryset.reverse()[0]
Also, beware this warning from the Django documentation:
... note that reverse() should
generally only be called on a QuerySet
which has a defined ordering (e.g.,
when querying against a model which
defines a default ordering, or when
using order_by()). If no such ordering
is defined for a given QuerySet,
calling reverse() on it has no real
effect (the ordering was undefined
prior to calling reverse(), and will
remain undefined afterward).
The simplest way to do it is:
books.objects.all().last()
You also use this to get the first entry like so:
books.objects.all().first()
To get First object:
ModelName.objects.first()
To get last objects:
ModelName.objects.last()
You can use filter
ModelName.objects.filter(name='simple').first()
This works for me.
Django >= 1.6
Added QuerySet methods first() and last() which are convenience methods returning the first or last object matching the filters. Returns None if there are no objects matching.
When the queryset is already exhausted, you may do this to avoid another db hint -
last = queryset[len(queryset) - 1] if queryset else None
Don't use try...except....
Django doesn't throw IndexError in this case.
It throws AssertionError or ProgrammingError(when you run python with -O option)
You can use Model.objects.last() or Model.objects.first().
If no ordering is defined then the queryset is ordered based on the primary key. If you want ordering behaviour queryset then you can refer to the last two points.
If you are thinking to do this, Model.objects.all().last() to retrieve last and Model.objects.all().first() to retrieve first element in a queryset or using filters without a second thought. Then see some caveats below.
The important part to note here is that if you haven't included any ordering in your model the data can be in any order and you will have a random last or first element which was not expected.
Eg. Let's say you have a model named Model1 which has 2 columns id and item_count with 10 rows having id 1 to 10.[There's no ordering defined]
If you fetch Model.objects.all().last() like this, You can get any element from the list of 10 elements. Yes, It is random as there is no default ordering.
So what can be done?
You can define ordering based on any field or fields on your model. It has performance issues as well, Please check that also. Ref: Here
OR you can use order_by while fetching.
Like this: Model.objects.order_by('item_count').last()
If using django 1.6 and up, its much easier now as the new api been introduced -
Model.object.earliest()
It will give latest() with reverse direction.
p.s. - I know its old question, I posting as if going forward someone land on this question, they get to know this new feature and not end up using old method.
In a Django template I had to do something like this to get it to work with a reverse queryset:
thread.forumpost_set.all.last
Hope this helps someone looking around on this topic.
MyModel.objects.order_by('-id')[:1]
If you use ids with your models, this is the way to go to get the latest one from a qs.
obj = Foo.objects.latest('id')
You can try this:
MyModel.objects.order_by('-id')[:1]
The simplest way, without having to worry about the current ordering, is to convert the QuerySet to a list so that you can use Python's normal negative indexing. Like so:
list(User.objects.all())[-1]