base.py
with abstract models and models.py
with models extending the abstract ones and inheriting all the features. In the specific project you can either use the core app directly, or create a specific app which models extend from the base abstract models of the core app and additionally introduce new features.This is a quick example skipping all the unrelated parts like settings, urls, and templates:
- core_project
- apps
- player
- base.py
from django.db impport models
class PlayerBase(models.Model):
name = models.CharField(max_length=100)
class Meta:
abstract = True - models.py
from core_project.apps.player.base import PlayerBase
class Player(PlayerBase):
pass
- base.py
- player
- apps
- specific_project
- apps
- player
- models.py
from core_project.apps.player.base import PlayerBase
class Player(PlayerBase):
points = models.IntegerField()
- models.py
- player
- apps
The concept works fine until you need to use foreign keys or many-to-many relations in the abstract models. As Josh Smeaton has already noticed, you can't set foreign keys to abstract models as they have no own database tables and they know nothing about the models which will extend them.
Let's say, we have the following situation:
GameBase
and MissionBase
are abstract models and the model extending MissionBase
should receive a foreign key to the model extending GameBase
.Thanks to Pro Django book by Marty Alchin, I understood how the models get created in the background. By default, all python classes are constructed by the
type
class. But whenever you use __metaclass__
property for your classes, you can define a different constructor. Django models are classes constructed by ModelBase
class which extends the type
class.In order to solve the problem of foreign keys to the models extending the abstract classes, we can have a custom constructor extending the
ModelBase
class.base.py
# -*- coding: utf-8 -*-
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.fields import FieldDoesNotExist
class GameMissionCreator(ModelBase):
"""
The model extending MissionBase should get a foreign key to the model extending GameBase
"""
GameModel = None
MissionModel = None
def __new__(cls, name, bases, attrs):
model = super(GameMissionCreator, cls).__new__(cls, name, bases, attrs)
for b in bases:
if b.__name__=="GameBase":
cls.GameModel = model
elif b.__name__=="MissionBase":
cls.MissionModel = model
if cls.GameModel and cls.MissionModel:
try:
cls.MissionModel._meta.get_field("game")
except FieldDoesNotExist:
cls.MissionModel.add_to_class(
"game",
models.ForeignKey(cls.GameModel),
)
return model
class GameBase(models.Model):
__metaclass__ = GameMissionCreator
title = models.CharField(max_length=100)
class Meta:
abstract = True
class MissionBase(models.Model):
__metaclass__ = GameMissionCreator
title = models.CharField(max_length=100)
class Meta:
abstract = True
models.py
# -*- coding: utf-8 -*-
from base import *
class Game(GameBase):
pass
class Mission(MissionBase):
pass
GameMissionCreator
is a constructor of GameBase
, MissionBase
, Game
, and Mission
classes. When it creates a class extending GameBase
, the game model is registered as a property. When it creates a class extending MissionBase
, the mission model is registered as a property. When both models are registered, a foreign key is added dynamically from one model to the other.One drawback of this constructor-class example is that if there are more than one classes extending
GameBase
or MissionBase
, then the code won't function correctly.Anyway, the example shown illustrates the possible solution and gives a direction for further development of the idea.
Is there a workaround for the drawback you mention? I can't seem to find a way to do it...
ReplyDeleteI have the same problem but with only one parent ABC.
ReplyDeleteAnother class would contain a ForeignKey to the abstract class.
I tried using your example to create a constructor to do so, but I still get "cannot dfine relation with absract class" error. Any idea why ?
The idea of the trick was that it didn't create a ForeignKey to an abstract class, but rather a ForeignKey to a leaf class which is created from an abstract class. The constructor checks if the class created is created from an abstract class or not. Probably you could even check the abstractness explicitly by model._meta.abstract. If you have multiple models created from the abstract class which will get ForeignKeys from other models, you need a way to define to which non-abstract class the ForeignKey is assigned. That might be done saving final non-abstract classes in a dictionary and referencing to them by names (let's say, defined in the settings).
ReplyDeleteIf this didn't help you, maybe you can send a link to pasted code snippet so that I could check it and maybe give some advices.
Recently I found out that all this can be achieved much simpler just telling the app name and model name in the foreign key definition, like this:
ReplyDeleteclass GameBase(models.Model):
title = models.CharField(max_length=100)
class Meta:
abstract = True
class MissionBase(models.Model):
game = models.ForeignKey("game_app.Game")
title = models.CharField(max_length=100)
class Meta:
abstract = True
I like the "Wish this site were powered by Django button". What I wish is that all blogspot sites had a "Click here for printable view" button on them. :(
ReplyDelete