I was wondering if there was a way to use Django's filter() on query sets using a dynamically generated python property using property(). I have first_name and last_name of every user, and I want to filter based on their concatenated name first_name last_name. (The reason behind this is that when I do autocomplete I search to see if the query matches first name, last name, or part of the concatenation. I want John S to match John Smith, for example.
I created a property of name:
def _get_name(self):
return self.first_name + " " + self.last_name
name = property(_get_name)
This way I can call user.name to get the concatenated name.
However, if I try to do User.objects.filter(name__istartswith=query) I get the error Cannot resolve keyword 'name' into field.
Any ideas on how to do this? Do I have to create another field in the database to store the full name?
The accepted answer is not entirely true.
For many cases, you can override get() in the model manager to pop dynamic properties from the keyword arguments, then add the actual attributes you want to query against into the kwargs keyword arguments dictionary. Be sure to return a super so any regular get() calls return the expected result.
I'm only pasting my own solution, but for the __startswith and other conditional queries you could add some logic to split the double-underscore and handle appropriately.
Here was my work-around to allow querying by a dynamic property:
class BorrowerManager(models.Manager):
def get(self, *args, **kwargs):
full_name = kwargs.pop('full_name', None)
# Override #1) Query by dynamic property 'full_name'
if full_name:
names = full_name_to_dict(full_name)
kwargs = dict(kwargs.items() + names.items())
return super(BorrowerManager, self).get(*args, **kwargs)
In models.py:
class Borrower(models.Model):
objects = BorrowerManager()
first_name = models.CharField(null=False, max_length=30)
middle_name = models.CharField(null=True, max_length=30)
last_name = models.CharField(null=False, max_length=30)
created = models.DateField(auto_now_add=True)
In utils.py (for the sake of context):
def full_name_to_dict(full_name):
ret = dict()
values = full_name.split(' ')
if len(values) == 1:
raise NotImplementedError("Not enough names to unpack from full_name")
elif len(values) == 2:
ret['first_name'] = values[0]
ret['middle_name'] = None
ret['last_name'] = values[1]
return ret
elif len(values) >= 3:
ret['first_name'] = values[0]
ret['middle_name'] = values[1:len(values)-1]
ret['last_name'] = values[len(values)-1]
return ret
raise NotImplementedError("Error unpacking full_name to first, middle, last names")
filter() operates on the database level (it actually writes SQL), so it won't be possible to use it for any queries based on your python code (dynamic property in your question).
This is an answer put together from many other answers in this department : )
I had a similar problem and was looking for solution. Taking for granted that a search engine would be the best option (e.g. django-haystack with Elasticsearch), that's how I would implement some code for your needs using only the Django ORM (you can replace icontains with istartswith):
from django.db.models import Value
from django.db.models.functions import Concat
queryset = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name')
return queryset.filter(full_name__icontains=value)
In my case I didn't know whether the user would insert 'first_name last_name' or viceversa, so I used the following code.
from django.db.models import Q, Value
from django.db.models.functions import Concat
queryset = User.objects.annotate(first_last=Concat('first_name', Value(' '), 'last_name'), last_first=Concat('last_name', Value(' '), 'first_name'))
return queryset.filter(Q(first_last__icontains=value) | Q(last_first__icontains=value))
With Django <1.8, you would probably need to resort to extra with the SQL CONCAT function, something like the following:
queryset.extra(where=['UPPER(CONCAT("auth_user"."last_name", \' \', "auth_user"."first_name")) LIKE UPPER(%s) OR UPPER(CONCAT("auth_user"."first_name", \' \', "auth_user"."last_name")) LIKE UPPER(%s)'], params=['%'+value+'%', '%'+value+'%'])
Think it's not possible in django to filter on properties that does not present as a database filed, but what you can do to make cool autocomplete search is something like this:
if ' ' in query:
query = query.split()
search_results = list(chain(User.objects.filter(first_name__icontains=query[0],last_name__icontains=query[1]),
User.objects.filter(first_name__icontains=query[1],last_name__icontains=query[0])))
else:
search_results = User.objects.filter(Q(first_name__icontains=query)| Q(last_name__icontains=query))
This code gives the user of your system a flexibility to start typing either first name or last name and the user will be thankful to you for allowing this.
Related
I have two models: City, and its alias CityAlias. The CityAlias model contains all the names in the City, plus the aliases. What I want is that whenever City is searched by name, the CityAlias model should be queried. This is what I've come up with:
class CityQuerySet(models.QuerySet):
""" If City is searched by name, search it in CityAlias """
def _search_name_in_alias(self, args, kwargs):
for q in args:
if not isinstance(q, models.Q): continue
for i, child in enumerate(q.children):
# q.children is a list of tuples of queries:
# [('name__iexact', 'calcutta'), ('state__icontains', 'bengal')]
if child[0].startswith('name'):
q.children[i] = ('aliases__%s' % child[0], child[1])
for filter_name in kwargs:
if filter_name.startswith('name'):
kwargs['aliases__%s' % filter_name] = kwargs.pop(filter_name)
def _filter_or_exclude(self, negate, *args, **kwargs):
# handles 'get', 'filter' and 'exclude' methods
self._search_name_in_alias(args=args, kwargs=kwargs)
return super(CityQuerySet, self)._filter_or_exclude(negate, *args, **kwargs)
class City(models.Model):
name = models.CharField(max_length=255, db_index=True)
state = models.ForeignKey(State, related_name='cities')
objects = CityQuerySet.as_manager()
class CityAlias(models.Model):
name = models.CharField(max_length=255, db_index=True)
city = models.ForeignKey(City, related_name='aliases')
Example: Kolkata will have an entry in City model, and it will have two entries in the CityAlias model: Kolkata and Calcutta. The above QuerySet allows to use lookups on the name field.
So the following two queries will return the same entry:
City.objects.get(name='Kolkata') # <City: Kolkata>
City.objects.get(name__iexact='calcutta') # <City: Kolkata>
So far so good. But the problem arises when City is a ForeignKey in some other model:
class Trip(models.Model):
destination = models.ForeignKey(City)
# some other fields....
Trip.objects.filter(destination__name='Kolkata').count() # some non-zero number
Trip.objects.filter(destination__name='Calcutta').count() # always returns zero
Django internally handles these joins differently, and doesn't call the get_queryset method of City's manager. The alternative is to call the above query as following:
Trip.objects.filter(destination=City.objects.get(name='Calcutta'))
My question is that can I do something, so that however the City model is searched by name, it always searches in the CityAlias table instead?
Or is there another better way to implement the functionality I require?
I think it is better (and more pythonic) to be explicit in what you ask for throughout instead of trying to do magic in the Manager and thus:
City.objects.get(aliases__name__iexact='calcutta') # side note: this can return many (same in original) so you need to catch that
And:
Trip.objects.filter(destination__aliases__name='Calcutta').count()
I was trying to use Custom Lookups but apparently you cannot add a table to the join list. (Well, you could add an extra({"table": ...}) in the model's manager but it's not an elegant solution).
So I'd propose you:
1) Keep always your 'main/preferred' name city also as a CityAlias. So the metadata of the city will be in City... but all the naming information will be in CityAlias. (and maybe change the names)
In this way all look-ups will happen in that table. You could have a boolean to mark which instance is the original/preferred.
class City(models.Model):
state = models.ForeignKey(State, related_name='cities')
[...]
class CityAlias(models.Model):
city = models.ForeignKey(City, related_name='aliases')
name = models.CharField(max_length=255, db_index=True)
2) If you are thinking about translations... Have you thought about django-modeltranslation app?
In this case, it would create a field for each language and it would be always better than having a join.
3) Or, if you are using PostgreSQL, and you are thinking about "different translations for the same city-name" (and I'm thinking with transliterations from Greek or Russian language), maybe you could use PostgreSQL dictionaries, trigrams with similarities, etc. Or even in this case, the 1st approach.
Speaking of keeping it simple. Why not just give the City model a char field 'CityAlias' that contains the string? If I understand your question correctly, this is the most simple solution if you only need one alias per city. It just looks to me as though you are complicating a simple problem.
class City(models.Model):
name = models.CharField(max_length=255, db_index=True)
state = models.ForeignKey(State, related_name='cities')
alias = models.CharField(max_length=255)
c = City.objects.get(alias='Kolkata')
>>>c.name
Calcutta
>>>c.alias
Kolkata
I'm trying to make this table with a clickable field which changes the boolean for the entry to its opposite value. It works, but I want an alternative text as "False" or "True" does not look nice, and the users are mainly Norwegian.
def bool_to_norwegian(boolean):
if boolean:
return "Ja"
else:
return "Nei"
class OrderTable(tables.Table):
id = tables.LinkColumn('admin_detail', args=[A('id')])
name = tables.Column()
address = tables.Column()
order = tables.Column()
order_placed_at = tables.DateTimeColumn()
order_delivery_at = tables.DateColumn()
price = tables.Column()
comment = tables.Column()
sent = tables.LinkColumn('status_sent', args=[A('id')])
paid = tables.LinkColumn('status_paid', args=[A('id')], text=[A('paid')])
class Meta:
attrs = {'class': 'order-table'}
If you look under the "paid" entry I am testing this right now, why can't I access the data with the same accessor as I do in the args? If I change the args to args=[A('paid')] and look at the link, it does indeed have the correct data on it. The model names are the same as the ones in this table, and "paid" and "sent" are BooleanFields.
This is kind of what I ultimately want:
text=bool_to_norwegian([A('paid')])
Here is what I send to the table:
orders = Order.objects.order_by("-order_delivery_at")
orders = orders.values()
table = OrderTable(orders)
RequestConfig(request).configure(table)
The text argument expects a callable that accepts a record, and returns a text value. You are passing it a list (which it will just ignore), and your function is expecting a boolean instead of a record. There is also no need for using accessors here.
Something like this should work:
def bool_to_norwegian(record):
if record.paid:
return "Ja"
else:
return "Nei"
Then in your column:
paid = tables.LinkColumn('status_paid', text=bool_to_norwegian)
(Note, it is not clear from your question where the data is coming from - is paid a boolean? You may need to adjust this to fit).
As an aside, the way you are passing args to your columns is weird (it seems the documentation also recommends this, but I don't understand why - it's very confusing). A more standard approach would be:
id = tables.LinkColumn('admin_detail', A('id'))
or using named arguments:
id = tables.LinkColumn('admin_detail', accessor=A('id'))
I'm doing a simple filter -
filters.py
class TblserversFilter(django_filters.FilterSet):
name = django_filters.CharFilter(name="servername", lookup_type="exact")
class Meta:
model = Tblservers
fields = ['servername']
What I would like to do, if possible, is to have two lookup_types associated with the field. Specifically I want exact AND contains and then somehow replace the operator depending on the filter.
name=serverabc would be an exact search and name~abc will be a fuzzy search.
You could do a method_filter and then prefix your filter queries with different symbols for exact and icontains and other filters that you want at the client side.
Since code is better than a thousand words:
exact_prefix = '#'
icontains_prefix = '~'
class TblserversFilter(django_filters.FilterSet):
name = django_filters.MethodFilter(
action=name_filter)
def name_filter(self, value):
if value:
value_prefix = value[0]
if value_prefix == exact_prefix:
return self.filter(name=value)
elif value_prefix == icontains_prefix:
return self.filter(name__icontains=value)
# this can continue for all the types of filters you want
else:
return self.filter(name=value)
else:
return self.filter(name=value)
class Meta:
model = Tblservers
fields = ['servername']
EDIT:
In django-filter 1.0 MethodFilter was replaced with Filter's method argument. So solution rewritten for v1.0 would be following (not tested):
exact_prefix = '#'
icontains_prefix = '~'
class TblserversFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
method='name_filter')
def name_filter(self, qs, name, value):
if value:
value_prefix = value[0]
if value_prefix == exact_prefix:
return qs.filter(name=value)
elif value_prefix == icontains_prefix:
return qs.filter(name__icontains=value)
# this can continue for all the types of filters you want
else:
return qs.filter(name=value)
else:
return qs.filter(name=value)
class Meta:
model = Tblservers
fields = ['servername']
First my apologies for the shameless library self-plug.
At some point I was trying to do something similar in django-filters however the solution was much more complex then anticipated. I ended up creating my own library for doing filtering in Django which natively supports the exact functionality you are looking for - django-url-filter. Its API is very similar to django-filters:
from django import forms
from url_filter.filter import Filter
from url_filter.filtersets import ModelFilterSet
class TblserversFilter(FilterSet):
name = Filter(form_field=forms.CharField(max_length=15), lookups=['exact', 'contains'])
class Meta(object):
model = Tblservers
fields = ['name', 'servername']
Note that the URL will look a bit different though:
?name=foo # exact
?name__exact=foo
?name__contains=foo
Also you will need to manually call the filter set in order to filter the queryset:
fs = TblserversFilter(data=query, queryset=...)
filtered_qs = fs.filter()
Syntax of the URL parameters is very similar to Django ORM.
You can look at the docs for more examples. Hopefully it might be of use.
I want to find a user based on the fields username, first_name and last_name in the DB.
What I do have so far is:
def _build_q_for_field(fieldname, wordlist):
def _param(x):
return {fieldname + '__icontains': x}
return reduce(operator.or_, (Q(**_param(w)) for w in wordlist))
query_string = "Fabian M"
query_words = query_string.strip().split(' ') # ['Fabian', 'M']
qs = User.objects.all()
qs = qs.filter(
_build_q_for_field('username', query_words) |
_build_q_for_field('first_name', query_words) |
_build_q_for_field('last_name', query_words)
)
Assume that we have the following very simple userset:
User 1: first_name = 'Fabian', last_name = 'Mueller', username = 'fmueller'
User 2: first_name = 'Fabian', last_name = 'Schulze', username = 'fschulze'
The code above would return both objects, because all conditions are connected via OR and both users do have first_name = 'Fabian'. But I want to have only the user Fabian Mueller in the result set, because the second word in the word list (M) is not contained in the second result.
Do you have a clever idea how to archive this? Combining the Q's in the filter method using AND instead of OR does work in the example above, but has serious problems if the word list contains only a single word and would immediately exclude results where the username does not match in such a nice fashion as in the example above.
Do you have a clever solution how to deal with this problem? Thanks in advance!
The problem can be solved if an additional field full_name is introduced in the query. It seems necessary to do so, because otherwise all combinations of fields and words in the query have to be considered. The possible combinations quickly explode.
Unfortunately Django < 1.8 needs an "raw" SQL statement (and thus this depends on the database in the backend), whereas Django 1.8 allows to abstract the problem by providing the django.db.models.functions.Concat and queryset.annotate().
query_db_fields = ['first_name', 'last_name', 'username']
if DJANGO_VERSION >= '1.8.0':
from django.db.models.functions import Concat
qs = qs\
.annotate(full_name=Concat(*query_db_fields))\
.filter(reduce(operator.and_, [Q(full_name__icontains=w) for w in query_words]))
else:
# (DEPRECATED) Remove this part when support for Django < 1.8 is being dropped
for w in query_words:
qs = qs.extra(
where=['lower(concat({fields:})) LIKE lower(%s)'.format(fields=','.join(query_db_fields))],
params=['%{}%'.format(w)]
)
I want to create a new type of field for django models that is basically a ListOfStrings. So in your model code you would have the following:
models.py:
from django.db import models
class ListOfStringsField(???):
???
class myDjangoModelClass():
myName = models.CharField(max_length=64)
myFriends = ListOfStringsField() #
other.py:
myclass = myDjangoModelClass()
myclass.myName = "bob"
myclass.myFriends = ["me", "myself", "and I"]
myclass.save()
id = myclass.id
loadedmyclass = myDjangoModelClass.objects.filter(id__exact=id)
myFriendsList = loadedclass.myFriends
# myFriendsList is a list and should equal ["me", "myself", "and I"]
How would you go about writing this field type, with the following stipulations?
We don't want to do create a field which just crams all the strings together and separates them with a token in one field like this. It is a good solution in some cases, but we want to keep the string data normalized so tools other than django can query the data.
The field should automatically create any secondary tables needed to store the string data.
The secondary table should ideally have only one copy of each unique string. This is optional, but would be nice to have.
Looking in the Django code it looks like I would want to do something similar to what ForeignKey is doing, but the documentation is sparse.
This leads to the following questions:
Can this be done?
Has it been done (and if so where)?
Is there any documentation on Django about how to extend and override their model classes, specifically their relationship classes? I have not seen a lot of documentation on that aspect of their code, but there is this.
This is comes from this question.
There's some very good documentation on creating custom fields here.
However, I think you're overthinking this. It sounds like you actually just want a standard foreign key, but with the additional ability to retrieve all the elements as a single list. So the easiest thing would be to just use a ForeignKey, and define a get_myfield_as_list method on the model:
class Friends(model.Model):
name = models.CharField(max_length=100)
my_items = models.ForeignKey(MyModel)
class MyModel(models.Model):
...
def get_my_friends_as_list(self):
return ', '.join(self.friends_set.values_list('name', flat=True))
Now calling get_my_friends_as_list() on an instance of MyModel will return you a list of strings, as required.
What you have described sounds to me really similar to the tags.
So, why not using django tagging?
It works like a charm, you can install it independently from your application and its API is quite easy to use.
I also think you're going about this the wrong way. Trying to make a Django field create an ancillary database table is almost certainly the wrong approach. It would be very difficult to do, and would likely confuse third party developers if you are trying to make your solution generally useful.
If you're trying to store a denormalized blob of data in a single column, I'd take an approach similar to the one you linked to, serializing the Python data structure and storing it in a TextField. If you want tools other than Django to be able to operate on the data then you can serialize to JSON (or some other format that has wide language support):
from django.db import models
from django.utils import simplejson
class JSONDataField(models.TextField):
__metaclass__ = models.SubfieldBase
def to_python(self, value):
if value is None:
return None
if not isinstance(value, basestring):
return value
return simplejson.loads(value)
def get_db_prep_save(self, value):
if value is None:
return None
return simplejson.dumps(value)
If you just want a django Manager-like descriptor that lets you operate on a list of strings associated with a model then you can manually create a join table and use a descriptor to manage the relationship. It's not exactly what you need, but this code should get you started.
Thanks for all those that answered. Even if I didn't use your answer directly the examples and links got me going in the right direction.
I am not sure if this is production ready, but it appears to be working in all my tests so far.
class ListValueDescriptor(object):
def __init__(self, lvd_parent, lvd_model_name, lvd_value_type, lvd_unique, **kwargs):
"""
This descriptor object acts like a django field, but it will accept
a list of values, instead a single value.
For example:
# define our model
class Person(models.Model):
name = models.CharField(max_length=120)
friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)
# Later in the code we can do this
p = Person("John")
p.save() # we have to have an id
p.friends = ["Jerry", "Jimmy", "Jamail"]
...
p = Person.objects.get(name="John")
friends = p.friends
# and now friends is a list.
lvd_parent - The name of our parent class
lvd_model_name - The name of our new model
lvd_value_type - The value type of the value in our new model
This has to be the name of one of the valid django
model field types such as 'CharField', 'FloatField',
or a valid custom field name.
lvd_unique - Set this to true if you want the values in the list to
be unique in the table they are stored in. For
example if you are storing a list of strings and
the strings are always "foo", "bar", and "baz", your
data table would only have those three strings listed in
it in the database.
kwargs - These are passed to the value field.
"""
self.related_set_name = lvd_model_name.lower() + "_set"
self.model_name = lvd_model_name
self.parent = lvd_parent
self.unique = lvd_unique
# only set this to true if they have not already set it.
# this helps speed up the searchs when unique is true.
kwargs['db_index'] = kwargs.get('db_index', True)
filter = ["lvd_parent", "lvd_model_name", "lvd_value_type", "lvd_unique"]
evalStr = """class %s (models.Model):\n""" % (self.model_name)
evalStr += """ value = models.%s(""" % (lvd_value_type)
evalStr += self._params_from_kwargs(filter, **kwargs)
evalStr += ")\n"
if self.unique:
evalStr += """ parent = models.ManyToManyField('%s')\n""" % (self.parent)
else:
evalStr += """ parent = models.ForeignKey('%s')\n""" % (self.parent)
evalStr += "\n"
evalStr += """self.innerClass = %s\n""" % (self.model_name)
print evalStr
exec (evalStr) # build the inner class
def __get__(self, instance, owner):
value_set = instance.__getattribute__(self.related_set_name)
l = []
for x in value_set.all():
l.append(x.value)
return l
def __set__(self, instance, values):
value_set = instance.__getattribute__(self.related_set_name)
for x in values:
value_set.add(self._get_or_create_value(x))
def __delete__(self, instance):
pass # I should probably try and do something here.
def _get_or_create_value(self, x):
if self.unique:
# Try and find an existing value
try:
return self.innerClass.objects.get(value=x)
except django.core.exceptions.ObjectDoesNotExist:
pass
v = self.innerClass(value=x)
v.save() # we have to save to create the id.
return v
def _params_from_kwargs(self, filter, **kwargs):
"""Given a dictionary of arguments, build a string which
represents it as a parameter list, and filter out any
keywords in filter."""
params = ""
for key in kwargs:
if key not in filter:
value = kwargs[key]
params += "%s=%s, " % (key, value.__repr__())
return params[:-2] # chop off the last ', '
class Person(models.Model):
name = models.CharField(max_length=120)
friends = ListValueDescriptor("Person", "Friend", "CharField", True, max_length=120)
Ultimately I think this would still be better if it were pushed deeper into the django code and worked more like the ManyToManyField or the ForeignKey.
I think what you want is a custom model field.