I would like to perform a logical exclusive OR (XOR) on django.db.models.Q
objects, using operator module to limit the choices of a model field to a subset of foreignkey. I am doing this in Django 1.4.3 along with Python 2.7.2. I had something like this:
import operator
from django.conf import settings
from django.db import models
from django.db.models import Q
from django.contrib.auth.models import User, Group
def query_group_lkup(group_name):
return Q(user__user__groups__name__exact=group_name)
class Book(models.Model):
author = models.ForeignKey(
User,
verbose_name=_("Author"),
null=False,
default='',
related_name="%(app_label)s_%(class)s_author",
# This would have provide an exclusive OR on the selected group name for User
limit_choices_to=reduce(
operator.xor,
map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
)
AUTHORIZED_AUTHORS
is a list of existing group names.
But this did not work, because Q
objects do not support ^
operator (only |
and &
operators from the docs). The message from the stacktrace was (partly) the following:
File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/db/models/loading.py", line 64, in _populate
self.load_app(app_name, True)
File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/db/models/loading.py", line 88, in load_app
models = import_module('.models', app_name)
File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/utils/importlib.py", line 35, in import_module
__import__(name)
File "/opt/dvpt/toto/apps/book/models.py", line 42, in <module>
class Book(models.Model):
File "/opt/dvpt/toto/apps/book/models.py", line 100, in Book
map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
TypeError: unsupported operand type(s) for ^: 'Q' and 'Q'
Therefore, inspired by this answer I attempted to implement an XOR for my specific lookup. It is not really flexible as the lookup is hardcoded (I would need to use kwargs
in the arguments of query_xor
for example...). I ended up doing something like this:
from django.conf import settings
from django.db import models
from django.db.models import Q
from django.db.models.query import EmptyQuerySet
from django.contrib.auth.models import User, Group
def query_xor_group(names_group):
"""Get a XOR of the queries that match the group names in names_group."""
if not len(names_group):
return EmptyQuerySet()
elif len(names_group) == 1:
return Q(user__user__groups__name__exact=names_group[0])
q_chain_or = Q(user__user__groups__name__exact=names_group[0])
q_chain_and = Q(user__user__groups__name__exact=names_group[0])
for name in names_group[1:]:
query = Q(user__user__groups__name__exact=name)
q_chain_or |= query
q_chain_and &= query
return q_chain_or & ~q_chain_and
class Book(models.Model):
author = models.ForeignKey(
User,
verbose_name=_("author"),
null=False,
default='',
related_name="%(app_label)s_%(class)s_author",
# This provides an exclusive OR on the SELECT group name for User
limit_choices_to=query_xor_group(getattr(settings, 'AUTHORIZED_AUTHORS', ''))
)
It works as I want but I seems to me rather not pythonic (especially the query_xor_group
method).
Would there be a better (more direct way) of doing this?
Basically, my question can be stripped of the limit_choices_to
part and be summarized as:
How can I make a bitwise exclusive OR on a set of django.db.models.Q
objects in a Djangonic way?
Q object encapsulates a SQL expression in a Python object that can be used in database-related operations. Using Q objects we can make complex queries with less and simple code. For example, this Q object filters whether the question starts wiht 'what': from django. db.
values() Returns a QuerySet that returns dictionaries, rather than model instances, when used as an iterable. Each of those dictionaries represents an object, with the keys corresponding to the attribute names of model objects.
A QuerySet is a collection of data from a database. A QuerySet is built up as a list of objects. QuerySets makes it easier to get the data you actually need, by allowing you to filter and order the data.
You could add an __xor__()
method to Q that uses and/or/not to do the XOR logic.
from django.db.models import Q
class QQ:
def __xor__(self, other):
not_self = self.clone()
not_other = other.clone()
not_self.negate()
not_other.negate()
x = self & not_other
y = not_self & other
return x | y
Q.__bases__ += (QQ, )
After doing this I was able to Q(...) ^ Q(...)
in a filter()
call.
Foobar.objects.filter(Q(blah=1) ^ Q(bar=2))
Which means the original attempt no longer throws an unsupported operand exception.
limit_choices_to=reduce(
operator.xor,
map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
)
Tested in Django 1.6.1
on Python 2.7.5
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With