Dec

11

Private Posts and Custom Managers

I have all these cool ideas for how this blog is going to knock the socks off of sliced bread, or at least, all things since sliced bread. But to get there, I keep stumbling over all this pesky functionality I don't have.

In a way it's one big lesson in project management. All the little, round-down-to-zero-time tasks add up to an alarmingly non-zero number.

Today's addition is a public checkbox on the BlogPost object. That, plus the custom manager that goes with it. Plus the updates to the views. Plus the test to make sure it works. Plus the Django Evolution code.

Here's my BlogPost model with its new public field:

class BlogPost(models.Model):
"""A simple blog post"""
title = models.CharField(('title'), max_length=100)
slug = models.SlugField(
('slug'), max_length=100, unique_for_date='pub_date')
pub_date = models.DateTimeField(('pub_date'), default=datetime.datetime.now)
public = models.BooleanField(
('public'), default=False)
category = models.ForeignKey(Category, null=True, blank=True, verbose_name=('category'))
body = models.TextField(
('body'), blank=True)
preview = models.TextField(_('preview'), blank=True)

The Django Evolution "mutation" code for this looks a lot like the other ones I've posted. Let me know if there's anyone out there who actually wants to keep seeing these.

After I added the field, I wrote a custom manager.

class BlogPostManager(models.Manager):
def public_posts(self):
return self.get_query_set().filter(public=True)

Now this is interesting. There are two schools of thought on how to provide a filtered version of a model's query set. The first technique I learned was to write a manager that overrode the get_query_set method and applied the filtering there. This manager would be set up on the model as a secondary manager, with the default manager explicitly declared above it. Like so:

class BlogPost(models.Model):
"""A simple blog post"""
...
 
admin_objects = models.Manager()
objects = BlogPostManager()

The other option is to provide the filtered query set via a new method in the manager, as I did. Then you can use your new manager as the model's default manager, like so:

class BlogPost(models.Model):
"""A simple blog post"""
...
 
objects = BlogPostManager()

I can do this because my manager isn't limiting the results that come through the normal get_query_set route (limiting results in get_query_set in the default manager can prevent objects from turning up in the admin, and other unhappy surprises.)

Remember friends, Python says, "There's only one way to do it." In this case, I can confidently say (because I'm overconfident, probably) that my method is the one way to do it. And here's why:

def category_detail(request, category_slug):
category = get_object_or_404(Category, slug=category_slug)
queryset = category.blogpost_set.public_posts()
return date_based.archive_index(
request,
queryset,
date_field='pub_date',
template_name='blog/category_detail.html',
template_object_name='blogpost_list',
extra_context={'category': category, 'preview': True},
)

This is the view for displaying the category landing page. Check out the queryset:

     queryset = category.blogpost_set.public_posts()

I wish I could remember where I first saw this manger-customization technique demonstrated, because seeing that custom method call on a relation manager was a revelation! The relation manager system is powered by necromancy, witchcraft and other dark arts and I will not speak of it here. Because I don't understand it. But I do get that inside all the voodoo is whatever manager I provided as my default manager, so voila, there's my custom method. If I had implemented this solution as a separate manager that filtered on get_query_set, this trick wouldn't work. Yes, I know I could query it the other way (BlogPost.objects.filter(category=category)), but that's not always possible. And making the relation manager do what I want is so satisfying!

I also think there's a nice semantic sense to the code: BlogPost.objects.all() vs. BlogPost.objects.public_posts()

Nice!

That the default manager is used in a reverse relation helps me with a problem, I had forever (overriding the delete of a reverse related set).

If I may suggest one tiny improvement, turn the new accessor into a property:

class BlogPostManager(models.Manager): @property def public_posts(self): ...

(just imagine everything hat correct indentation, and see that as a feature request for your blog ;-))

That way, it looks better, when you apply further filter calls:

category.blogpost_set.public_posts.filter(...)

instead of

category.blogpost_set.public_posts().filter(...)

As you don't accept any arguments to public_posts(), there is no need for the brackets.

Just a thought.

Marc

— Marc Remolt (December 11, 2008 at 7:30 a.m.)

Yeah, I think that makes sense.

And I like the feature request! I'll see about adding my enhanced markdown filtering to comments.

— Sam (December 11, 2008 at 10:37 a.m.)

That's the approach I take too: something like Post.objects.published()

I much prefer that method over the technique I've seen most often in other people's code, overriding get_query_set()

Kyle (December 11, 2008 at 11:38 a.m.)