Reputation: 32979
In my django site I have two apps, blog and links. blog has a model blogpost, and links has a model link. There should be a one to many relationship between these two things. There are many links per blogpost, but each link has one and only one blog post. The simple answer is to put a ForeignKey to blogpost in the link model.
That's all well and good, however there is a problem. I want to make the links app reusable. I don't want it to depend upon the blog app. I want to be able to use it again in other sites and perhaps associate links with other non-blogpost apps and models.
A generic foreign key seems like it might be the answer, but not really. I don't want links to be able to associate with any model in my site. Just the one that I explicitly specify. And I know from prior experience that there can be issues using generic foreign keys in terms of database usage because you can't do a select_related over a generic foreign key the way you can with a regular foreign key.
What is the "correct" way to model this relationship?
Upvotes: 17
Views: 3641
Reputation: 634
Anoher way to solve this is how django-mptt does this: define only an abstract model in a reusable app(MPTTModel), and require to inherit it with defining some fields (parent=ForeignKey to self, or whatever your app usecase will require)
Upvotes: 1
Reputation: 52237
This question and Van Gale's answer lead me to the question, how it could be possible, to limit contenttypes for GFK without the need of defining it via Q objects in the model, so it could be completly reuseable
the solution is based on
settings.TAGGING_ALLOWED
. This is necessary for usage in the admin-interfaceMy code is quite rough and not fully tested
settings.py
TAGGING_ALLOWED=('myapp.modela', 'myapp.modelb')
models.py:
from django.db import models
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db.models import get_model
from django.conf import settings as s
from django.db import IntegrityError
TAGABLE = [get_model(i.split('.')[0],i.split('.')[1])
for i in s.TAGGING_ALLOWED if type(i) is type('')]
print TAGABLE
TAGABLE_Q = eval( '|'.join(
["Q(name='%s', app_label='%s')"%(
i.split('.')[1],i.split('.')[0]) for i in s.TAGGING_ALLOWED
]
))
class TaggedItem(models.Model):
content_type = models.ForeignKey(ContentType,
limit_choices_to = TAGABLE_Q)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
def save(self, force_insert=False, force_update=False):
if self.content_object and not type(
self.content_object) in TAGABLE:
raise IntegrityError(
'ContentType %s not allowed'%(
type(kwargs['instance'].content_object)))
super(TaggedItem,self).save(force_insert, force_update)
from django.db.models.signals import post_init
def post_init_action(sender, **kwargs):
if kwargs['instance'].content_object and not type(
kwargs['instance'].content_object) in TAGABLE:
raise IntegrityError(
'ContentType %s not allowed'%(
type(kwargs['instance'].content_object)))
post_init.connect(post_init_action, sender= TaggedItem)
Of course the limitations of the contenttype-framework affect this solution
# This will fail
>>> TaggedItem.objects.filter(content_object=a)
# This will also fail
>>> TaggedItem.objects.get(content_object=a)
Upvotes: 0
Reputation: 22248
I'd go with generic relations. You can do something like select_related, it just require some extra work. But I think it's worth it.
One possible solution for generic select_related-like functionality:
http://bitbucket.org/kmike/django-generic-images/src/tip/generic_utils/managers.py
(look at GenericInjector manager and it's inject_to method)
Upvotes: 0
Reputation: 43932
If you think the link app will always point to a single app then one approach would be to pass the name of the foreign model as a string containing the application label instead of a class reference (Django docs explanation).
In other words, instead of:
class Link(models.Model):
blog_post = models.ForeignKey(BlogPost)
do:
from django.conf import setings
class Link(models.Model):
link_model = models.ForeignKey(settings.LINK_MODEL)
and in your settings.py:
LINK_MODEL = 'someproject.somemodel'
Upvotes: 24
Reputation: 15824
I think TokenMacGuy is on the right track. I would look at how django-tagging handles a similar generic relationship using the content type, generic object_id, and generic.py. From models.py
class TaggedItem(models.Model):
"""
Holds the relationship between a tag and the item being tagged.
"""
tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
object_id = models.PositiveIntegerField(_('object id'), db_index=True)
object = generic.GenericForeignKey('content_type', 'object_id')
objects = TaggedItemManager()
class Meta:
# Enforce unique tag association per object
unique_together = (('tag', 'content_type', 'object_id'),)
verbose_name = _('tagged item')
verbose_name_plural = _('tagged items')
Upvotes: 2
Reputation: 156258
Probably you need to use the content types app to link to a model. You might then arrange for your app to check the settings to do some additional checking to limit which content types it will accept or suggest.
Upvotes: 0