Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perform a logical exclusive OR on a Django Q object

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?

like image 896
Marc-Olivier Titeux Avatar asked Feb 05 '13 15:02

Marc-Olivier Titeux


People also ask

What is Q object in Django?

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.

What does .values do in Django?

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.

What is QuerySet?

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.


1 Answers

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

like image 147
Daniel Rucci Avatar answered Sep 20 '22 13:09

Daniel Rucci