Zastanawialiśmy się ostatnimi czasy, w jaki sposób zbudować aplikację tak, by z jednej strony była uniwersalna (abstrakcyjne modele bazowe), a z drugiej zawierała całą niezbędną logikę. Problem polega na tym, że — z oczywistych względów — klucza obcego do modelu abstrakcyjnego stworzyć się nie da. Pozostaje więc zwalić pracę na programistę, który konkretyzuje abstrakcyjne prototypy, prawda?

Dzisiaj do głowy przyszło mi rozwiązanie może nie do końca ładne, ale na pewno działające. Fabryka abstrakcyjnych modeli:

from django.db import models

class AbstractModelMetaclass(type):
    def __new__(cls, name, bases, attrs):
        super_new = super(AbstractModelMetaclass, cls).__new__
        if bases == (object, ):
            return super_new(cls, name, bases, attrs)
        new_attrs = attrs.copy()
        module = attrs['__module__']
        new_cls = super_new(cls, name, bases, {'__module__': module})
        fields = getattr(new_cls, '_fields', {}).copy()
        for key, val in attrs.items():
            if key not in ['construct', '__new__']:
                fields[key] = val
        new_attrs['_fields'] = fields
        return super_new(cls, name, bases, new_attrs)

class AbstractModelFactory(object):
    __metaclass__ = AbstractModelMetaclass

    @staticmethod
    def construct():
        return {}

    def __new__(cls, *args, **kwargs):
        attrs = cls._fields.copy()
        attrs.update(cls.construct(*args, **kwargs))
        attrs['__module__'] = cls.__module__
        attrs['Meta'] = type('Meta', (), {'abstract': True})
        return type(cls.__name__, (models.Model, ), attrs)

Idea jest taka, że implementując swoją podklasę fabryki modeli mamy możliwość zadecydować o polach, jakie trafią do ostatecznego modelu. Dla lepszego zrozumienia, rozważmy abstrakcyjne klasy produktu i kategorii:

class CategoryFactory(AbstractModelFactory):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name

class ProductFactory(AbstractModelFactory):
    name = models.CharField(max_length=100)

    @staticmethod
    def construct(category):
        return {'category': models.ForeignKey(category)}

    def __unicode__(self):
        return u'%s / %s' % (self.category, self.name)

Skonkretyzujmy klasy własną implementacją:

class MyCategory(CategoryFactory()):
    pass

class MyProduct(ProductFactory(category=MyCategory)):
    pass

Upewnijmy się, że zależności działają prawidłowo:

from . import models

c = models.MyCategory(name='cat')
p = models.MyProduct(name='prod', category=c)
print p # 'cat / prod'

Oczywiście — ktoś zaraz powie — to samo można uzyskać tworząc funkcję zwracającą klasę zdefiniowaną w jej ciele. Można, ale nie da się po niej wygodnie dziedziczyć (rozszerzenie istniejącej fabryki).