Django: how can I overload the Q in django.db.models.query QuerySet for use in my special purpose assignment?

First of all, since this is my first question / post, I would like to thank you all for this wonderful community and amazing service, as, like many developers around the world, stackoverflow is my main resource when it comes to code problems.

Note:

This post is a bit long (sorry), covers two different but related aspects of the situation, and is organized as follows:

  • Background / Context
  • A design issue that I would like your advice on.
  • Solution I am trying to implement for solution 2.
  • Actual problem and question related to 3.

1. In some context:

I have two different objects Model

( Report

and Jobs

) from an existing implementation, which is a bad design. The fact is, both objects are quite similar in purpose, but were likely implemented in two different time frames.

There is a lot of processing going on on these objects and as the system needs to evolve I started writing a metaclass / interface from which both would be subclasses. Currently, the two Models

use different names Fields

for the same purpose, for example author

, and juser

to indicate User

(very stupid), etc.

Since I cannot afford to just change the column names in the database and then go through thousands of lines of code to change all references to those fields (although I could, thanks to the "Use Modern IDEs" feature), and also because this object can be used elsewhere, I used db_column=

feat. so that each model can have the same field name and ultimately handle both objects (instead of having thousands of lines of duplicated code to do the same things).

So, I have something like this:

from django.db import models
class Report(Runnable):
    _name = models.CharField(max_length=55, db_column='name')
    _description = models.CharField(max_length=350, blank=True, db_column='description')
    _author = ForeignKey(User, db_column='author_id')
    # and so on

class Jobs(Runnable):
    _name = models.CharField(max_length=55, db_column='jname')
    _description = models.CharField(max_length=4900, blank=True, db_column='jdetails')
    _author = ForeignKey(User, db_column='juser_id')
    # and so on

      

As I said earlier, in order to avoid rewriting the code of the object object, I used properties that shade the fields:

from django.db import models
class Runnable(models.Model):   
    objects = managers.WorkersManager() # The default manager.

    @property # Report
    def type(self):
        return self._type

    @type.setter # Report
    def type(self, value):
        self._type = value

    # for backward compatibility, TODO remove both
    @property # Jobs
    def script(self):
        return self._type

    @script.setter # Jobs
    def script(self, value):
        self._type = value
    # and so on

      


2. Problem with design:

This is fine and is what I wanted, except that now using Report.objects.filter(name='something')

or Jobs.objects.filter(jname='something')

won't work, obviously due to Django's design (and so on with .get()

, .exclude()

etc.)) and the client code unfortunately filled with them.
I am of course planning to replace them with all the methods of my newly createdWorkersManager

Aparté:
What to expect? "Newly created WorkersManager

"
Yes, after two years and thousands of lines of code, it was not here Manager

, crazy right?
But guess what? which is the least of my problems; and cheer up as most of the code is still in view.py and associated files (instead of being properly inside objects that are supposed to be manipulated), basically a few "pure" imperative pythons ...

Great right?


3. My solution:

After reading a lot (here and there) and researching about it, I found out that:

  • Trying to subclass was Field

    not a solution
  • I might overload QuerySet

    .

So I did:

from django.db.models.query_utils import Q as __originalQ
class WorkersManager(models.Manager):   
    def get_queryset(self):
        class QuerySet(__original_QS):
            """
            Overloads original QuerySet class
            """
            __translate = _translate # an external fonction that changes the name of the keys in kwargs

            def filter(self, *args, **kwargs):
                args, kwargs = self.__translate(*args, **kwargs)
                super(QuerySet, self).filter(args, kwargs)

            # and many more others [...]
        return QuerySet(self.model, using=self._db)

      

And that's quite normal.

4. So what's wrong?

The problem is that Django internally uses Q

db.model.query internally using its own imports and is Q

not shown or mentioned anywhere , so it might be overloaded.

>>> a =Report.objects.filter(name='something')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/venv/local/lib/python2.7/site-packages/django/db/models/manager.py", line 143, in filter
    return self.get_query_set().filter(*args, **kwargs)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/query.py", line 624, in filter
    return self._filter_or_exclude(False, *args, **kwargs)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/query.py", line 642, in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1250, in add_q
    can_reuse=used_aliases, force_having=force_having)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1122, in add_filter
    process_extras=process_extras)
  File "/venv/local/lib/python2.7/site-packages/django/db/models/sql/query.py", line 1316, in setup_joins
    "Choices are: %s" % (name, ", ".join(names)))
FieldError: Cannot resolve keyword 'name' into field. Choices are: _author, _description, _name, # and many more

      

But I remember reading something about how Django loads the first occurrence Model

, and how you could trick it by overriding such Model

before using imports (this is clearly not python specific)
So I ended up trying to overload Q

by overriding it before importing the appropriate class or after, but I can't figure it out.

Here's what I've tried:

from django.db.models.query_utils import Q as __originalQ

__translation = {'name': '_name',} # has much more, just for exemple

def _translate(*args, **kwargs):
    for key in kwargs:
        if key in __translation.keys():
            kwargs[__translation[key]] = kwargs[key]
            del kwargs[key]
    return args, kwargs

class Q(__originalQ):
    """
    Overloads original Q class
    """
    def __init__(self, *args, **kwargs):
        super(Q, self).__init__(_translate(*args, **kwargs))

# now import QuerySet which should use the new Q class
from django.db.models.query import QuerySet as __original_QS

class QuerySet(__original_QS):
    """
    Overloads original QuerySet class
    """
    __translate = _translate # writing shortcut

    def filter(self, *args, **kwargs):
        args, kwargs = self.__translate(*args, **kwargs)
        super(QuerySet, self).filter(args, kwargs)
    # and some others

# now import QuerySet which should use the new QuerySet class
from django.db import models

class WorkersManager(models.Manager):
    def get_queryset(self):
        # might not even be required if above code was successful
        return QuerySet(self.model, using=self._db)

      

This, of course, has no effect, since Q is re-imported from django.db.model.query

into the definition _filter_or_exclude

.
So, of course, an intuitive solution would be to overload _filter_or_exclude

and copy the source code without calling.But super


here's the catch: I'm using an older version of Django that might be updated someday, and I don't want to get involved in the specifics of the Django implementation like I did with get_queryset

. but i think this is kind of ok as it is (as far as i understand) a placeholder for overloading and it is also the only way.

So here I am, and my question is:
Is there no other way to do this? is there no way to overload Q

inside a Django module?

Thanks so much for reading in full :)

here is the potato (Oups, wrong site, sorry :))

EDIT:

So, after trying to overload _filter_or_exclude

, it seems like it has no effect.
I probably missed something about call stack order or something ... I'll continue tomorrow and let you know.

+3


source to share


1 answer


Yes! I found a solution.

It turns out, firstly, I forgot to have return

in my functions, for example:

def filter(self, *args, **kwargs):
    args, kwargs = self.__translate(*args, **kwargs)
    super(QuerySet, self).filter(args, kwargs)

      

Instead:

def filter(self, *args, **kwargs):
    args, kwargs = self.__translate(*args, **kwargs)
    return super(QuerySet, self).filter(args, kwargs)

      

and I also had:

args, kwargs = self.__translate(*args, **kwargs)

      



Instead:

args, kwargs = self.__translate(args, kwargs)

      

which cause the unboxing of the fucntion call, and thus eveything from the original kwargs

ends in args

, thus preventing the effect translate

.

Even worse, I didn't realize that I could overload directly filter

, get

etc. directly from my custom manager ...
Which saves me from working with QuerySet

and Q

.

As a result, the following code works as expected:

def _translate(args, kwargs):
    for key in kwargs.keys():
        if key in __translation.keys():
            kwargs[__translation[key]] = kwargs[key]
            del kwargs[key]
    return args, kwargs

class WorkersManager(models.Manager):
    def filter(self, *args, **kwargs):
        args, kwargs = _translate(args, kwargs)
        return super(WorkersManager, self).filter(*args, **kwargs)

    # etc...

      

0


source







All Articles