I am working on a project that has two separate but interrelated Django web sites (projects in Django’s parlance). In an earlier blog post, I described setting up the second project (mk_ai) to have read-only access to the first project’s database (mk_web_core) in dev but then getting around those access restrictions for testing. The main thing I need for testing is a big, set of hierarchical data to be loaded into the first project’s test database. I can use the manage commands dumpdata and loaddata to preserve date in my development environment, but when I tried to load that same data into the test database, I ran into problems.
We are using GenericForeignKeys and GenericRelations. Django implements GenericForeignKeys by creating a database foreign key into the django_content_type table. In our mixed database setup, my django_content_type table is in the mk_ai schema. So, even if I set up my database router to allow_relation across databases AND the postgres database adapter would even attempt to make that join, the content types in the references in mk_web_core would not be in mk_ai’s django_content_type table. So we can’t use Django’s GenericForeignKeys. What shall we do instead?
Rails implements a similar type of relationship with a feature it calls Polymorphic Associations. Django stores the object’s id + a FK link to row in the content_type table representing the the object’s model. Rails store’s the object’s id + the object’s class name in a field it calls
class PageBlock(models.Model):
page = models.ForeignKey('Page')
position = models.PositiveSmallIntegerField()
allowed_block_types = models.Q(app_label='materials', model='text') | \
models.Q(app_label='materials', model='video') | \
models.Q(app_label='course_materials', model='image')
block_type = models.ForeignKey(ContentType, limit_choices_to=allowed_block_types)
object_id = models.PositiveSmallIntegerField()
material = GenericForeignKey(block_type', 'object_id')
class PageBlock(models.Model):
"""
This is a mapping table to all us to access collections of
blocks regardless of their actual type.
TODO:
Figure out how to make the object_id options fill a select
list once the user chooses a block_type in the form on the
admin interface.
"""
BLOCK_TYPE_NAMES = [('text', 'TextBlock'),
('video', 'VideoBlock'),
('image', 'ImageBlock'),
]
page = models.ForeignKey('Page')
position = models.PositiveSmallIntegerField()
block_type_name = models.CharField(max_length=100, choices=BLOCK_TYPE_NAMES)
# The block_id would be a ForeignKey field into a Video, Image... if we were mapping to just one model
block_id = models.PositiveSmallIntegerField()
@property
def block(self):
if self.block_type == 'TextBlock':
return TextBlock.objects.get(pk=self.block_id)
if self.block_type == 'VideoBlock':
return VideoBlock.objects.get(pk=self.block_id)
if self.block_type == 'ImageBlock':
return ImageBlock.objects.get(pk=self.block_id)
# ORIGINALLY
class VideoBlock(models.Model):
title = models.CharField(max_length=256)
content = models.FileField(upload_to='videos/')
page_block = GenericRelation(PageBlock,
object_id_field='object_id',
content_type_field='page_block')
@property
def model_name(self):
return "VideoBlock"
# AFTER REMOVING THE GenericForeignKey
class VideoBlock(models.Model):
title = models.CharField(max_length=256)
content = models.FileField(upload_to='videos/')
@property
def model_name(self):
return "VideoBlock"
@property
def page_block(self):
return self.PageBlock.objects.filter(block_type_name='VideoBlock',
object_id=self.id)