I am developing a Django app called
discuss, which allows users to post comments.
Now my particular application is to allow users to comment on a game, and I would like them to be able to load in their own game boards. However, I want to write the app as reusably as possible, so I do not want the gameboards to be part of the discuss app.
I have two models:
class Discussion(models.Model): name = models.CharField(max_length=60) def __unicode__(self): return self.name class Post(models.Model): discussion = models.ForeignKey(Discussion) forerunner = models.ForeignKey("self", blank=True, null=True) author = models.ForeignKey(User) body = models.TextField() created = models.DateTimeField(auto_now_add=True) def __unicode__(self): return self.title
I have been toying with a few ways of going about this:
- Add the
(content_type, object_id, content_object)trio of fields to my
Postclass. This would allow the user to associate any model with their post, but only one. These are both undesirable features for my case. It is also messy-looking to me.
ManyToManyField(Post)to my game’s
Boardclass, i.e. point back the other way, so that the reusable app’s Post class remains pure. This could work except that it pollutes the Board class instead; not all boards appear in posts.
- Add a new joining model like this:
class PostedBoard(models.Model): board = models.ForeignKey(game.models.Board) posts = models.ManyToManyField(discuss.models.Post)
This would probably work but feels very wrong.
Postfor the game, e.g.:
class GamePost(discuss.models.Post): boards = models.ManyToManyField(game.models.Board)
The last feels like the right object-oriented approach, but I wasn’t sure how well it would actually work in Django. The purpose of this post is simple: subclassing Django models does work, with a caveat: the usual object manager does not know the new post’s subclass. This means if you use
discussion.post_set.objects, you will not know the subclasses of the returned objects.
d = Discussion.objects.get() # <Discussion: MyDiscussion> d.post_set.all() # [<Post: First challenge>, <Post: Second challenge>, # <Post: A comment on challenge 2>] g = GamePost(discussion = d, title = "Test subclassing", ...) g.save() g.pk # Note that the subclass's primary key is 4, not 1 # 4 d.post_set.all() # success! # [<Post: First challenge>, <Post: Second challenge>, # <Post: A comment on challenge 2>, <Post: Test subclassing>] # Ah - but here's the rub: # this command does not know the new post's subclass gg = d.post_set.all() isinstance(gg, GamePost) # False
There are a number of solutions out there to deal with this problem – this one seems well-regarded.
I have decided to go with a simple approach which takes advantage of the fact that the object’s primary key is the same whatever class it shows up as. If I need to use the instance as a member of its subclass, just use:
def as_subclass(instance, subclass): try: return subclass.objects.get(pk=instance.pk) except subclass.DoesNotExist: return instance
or, if you have lots of subclassing going on, here is a more automated method which searches through all the possible subclasses and checks each one in turn (assuming only one level of inheritance):
def as_leaf_class(instance): subclasses = instance.__class__.__subclasses__() for subclass in subclasses: t = subclass.objects.filter(pk=instance.pk) if len(t)>0: return t return instance
I would love to hear if you’ve had a similar problem before and how you solved it!