Créer un Manager pour modèle abstrait

Dans ce nouveau article sur Django, nous allons définir un ModelManager pour des modèles abstraits, qui comme vous le savez, ne peuvent pas être requêtées en l'état.

L'héritage entre deux modèles en Django peux se faire de 2 façon dans le framework : de manière concrète ou abstraite.

Un héritage concret est très pratique, dans le cas de clé étrangère par exemple. En effet, il est impossible de créer un lien vers un modèle abstrait, puisque justement il n'a pas d'existence en DB. L'inconvénient est qu'il est nécessaire de maintenir une table pour le modèle papa.

L'héritage abstrait, quant à lui, est utile lorsque des modèles distincts partagent des attributs ou des méthodes, mais qu'ils n'ont pas nécessairement de lien.

En résumé, les modèles concrets sont liés entre eux par un lien 1-1, qui fait office de clé primaire. Lorsqu'un modèle hérite d'un modèle abstrait, ce lien n'existe pas.

Il devient donc pénible de faire d'obtenir la liste des instances d'un modèle qui hérite d'un modèle abstrait.

Voici un petit bout de code qui permet, de manière détournée, d'obtenir toutes les instances d'un enfant d'un modèle abstrait :

from django.core.exceptions import FieldError, ObjectDoesNotExist
from django.db.models.query import QuerySet
import functools


class AbstractQuerySetSequence(list):
    def query(self, _name, *args, **kwargs):
        objects = AbstractQuerySetSequence()
        for k, qs in self:
            try:
                objects.append((k, getattr(qs, _name)(*args, **kwargs)))
            except FieldError as e:
                objects.append((k, qs))
            except Exception as e:
                objects.append((k, e))
        return objects

    def __getattr__(self, _name):
        try:
            return getattr(super(AbstractQuerySetSequence, self), _name)
        except AttributeError:
            return functools.partial(self.query, _name)

    def to_json(self):
        return {k.___name__: list(v) for k, v in self}


class AbstractModelChildrenRegister(object):
    """ Class to register abstract model children and
    query throught them.
    """
    def __init__(self, *args, **kwargs):
        self.models = []

    def register(self, model):
        self.models.append(model)

    def managers(self):
        return [m.objects for m in self.models]

    def query(self, _name, *args, **kwargs):
        objects = AbstractQuerySetSequence()
        for manager in self.managers():
            try:
                attr = getattr(manager, _name)
                obj = attr(*args, **kwargs)
            except FieldError:
                obj = attr()
            except Exception as err:
                obj = err
            objects.append((manager.model, obj))
        return objects

    def get_one(self, *args, **kwargs):
        for manager in self.managers():
            try:
                obj = manager.get(*args, **kwargs)
            except:
                pass
            else:
                return obj
        raise ObjectDoesNotExist

    def __getattr__(self, _name):
        if not hasattr(QuerySet(), _name):
            return getattr(super(AbstractModelChildrenRegister, self), _name)
        return functools.partial(self.query, _name)

Ce 'Manager', placé sur un modèle abstrait, maintient un registre des classes enfantes de celui-ci. On expose ensuite l'api d'un ModelManager et on répercute les appelles à cette api sur les modèles du registre. Le résultat est une liste de tuples de ce type : (ModelEnfant, QuerySet). En cas de FieldError, la queryset précédente (ou .all()) est renvoyée. Si c'est un autre type d'erreur qui est levé, la queryset est remplacée par celle-ci.

Et voilà !

Bon ça à l'air vraiment super sur le papier, mais est-ce que ça fonctionne ? Voyons un exemple. Je passe volontairement les détails de l'initialisation du projet Django.

from django.db import models
from demo.utils.abstract import AbstractModelChildrenRegister


class TitledModel(models.Model):
    title = models.CharField(max_length=32)

    register = AbstractModelChildrenRegister()

    class Meta:
        abstract = True


class Serie(TitledModel):
    pass
TitledModel.register.register(Serie)


class Article(TitledModel):
    draft = models.BooleanField(default=False)
    content = models.TextField()
    serie = models.ForeignKey(Serie, related_name="articles", null=True, blank=True)
TitledModel.register.register(Article)

Voilà on a créé deux modèles qui implémentent TitledModel. On va créer quelques objets et voir comment notre bidouille s'en sort:

>>> from demo.models import TitledModel, Article
>>> Article.objects.create(title='Mon super article 1',
                           content='Super article de la mort')
<Article: Article object>
>>> Article.objects.create(title='Mon super article 2',
                           content='Autre super article de la mort')
<Article: Article object>
>>> TitledModel.register.all()
[(demo.models.Serie, []), (demo.models.Article, [<Article: Article object>])]
>>> TitledModel.register.filter(title='Mon super article 1')\
...                     .values_list('title', flat=True)
[(demo.models.Serie, []), (demo.models.Article, [u'Mon super article 1'])]

Ça marche !