Update Mezzanine blog posts to allow being marked login required

I was working on developing a Mezzanine site where a desired feature was to allow marking BlogPosts as login required, the same way that Pages can be marked as login required. After implenting a solution I decided it would be nice to document it for myself, and anyone else who is interested.

The code

A prerequisite to the following code is creating a django app. Throughout the post the name blog_login_required is used, but any Django app will do.

Let's get started:

python manage.py startapp blog_login_required

After creating the app add it to your INSTALLED_APPS setting.

1. settings.py

First let's add a login_required field to BlogPosts using the EXTRA_MODEL_FIELDS Mezzanine setting.

EXTRA_MODEL_FIELDS = (
    (
        "mezzanine.blog.models.BlogPost.login_required",
        "BooleanField",
        ("Login required",),
        {"default": False},
    ),
)

If you are using South migrations, which you should be, we need to create the migrations for this. We will put the migrations in an external app to avoid modifying Mezzanine directly. To do this run the following two commands:

$ python manage.py schemamigration blog —auto  —stdout » blog_login_required/migrations/0001_add_login_required_to_blogpost.py
$ python manage.py migrate blog_login_required

2. views.py

The built in Mezzanine view for BlogPosts doesn't handle login required so I created my own. In this case I opted to recreate the view because the original is very small (4 or 5 lines) and this was more efficient. If it was more complicated I may have opted to use a middleware, the disadvantage of this would be that the middleware would have to look up the blog post just to check login_required and the view would later lookup the same blog post.

from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import get_object_or_404, redirect
from django.utils.http import urlquote

from mezzanine.blog.models import BlogPost
from mezzanine.conf import settings
from mezzanine.utils.views import render


def blog_post_detail(request, slug, year=None, month=None, day=None,
                     template="blog/blog_post_detail.html"):
    """. Custom templates are checked for using the name
    ``blog/blog_post_detail_XXX.html`` where ``XXX`` is the blog
    posts's slug.
    """
    blog_posts = BlogPost.objects.published(
        for_user=request.user).select_related()
    blog_post = get_object_or_404(blog_posts, slug=slug)

    if blog_post.login_required and not request.user.is_authenticated():
        path = urlquote(request.get_full_path())
        bits = (settings.LOGIN_URL, REDIRECT_FIELD_NAME, path)
        return redirect("%s?%s=%s" % bits)

    context = {"blog_post": blog_post, "editable_obj": blog_post}
    templates = [u"blog/blog_post_detail_%s.html" % unicode(slug), template]
    return render(request, templates, context)

3. urls.py

Next we need to create url patterns to instruct the site to use the new blog post detail rather than Mezzanine's. Because of the required order of the blog url patterns I recreated all of them, there may be a better solution, but this is the best I could think of. These patterns can be placed directly in your project's urls, they just need to show up before Mezzanine urls are included.

url("^%s%sfeeds/(?P<format>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_feed", name="blog_post_feed"),
url("^%s%stag/(?P<tag>.*)/feeds/(?P<format>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_feed", name="blog_post_feed_tag"),
url("^%s%stag/(?P<tag>.*)%s$" % _blog_format_string, "mezzanine.blog.views.blog_post_list",
    name="blog_post_list_tag"),
url("^%s%scategory/(?P<category>.*)/feeds/(?P<format>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_feed", name="blog_post_feed_category"),
url("^%s%scategory/(?P<category>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_list", name="blog_post_list_category"),
url("^%s%sauthor/(?P<username>.*)/feeds/(?P<format>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_feed", name="blog_post_feed_author"),
url("^%s%sauthor/(?P<username>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_list", name="blog_post_list_author"),
url("^%s%sarchive/(?P<year>\d{4})/(?P<month>\d{1,2})%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_list", name="blog_post_list_month"),
url("^%s%sarchive/(?P<year>.*)%s$" % _blog_format_string,
    "mezzanine.blog.views.blog_post_list", name="blog_post_list_year"),
url("^%s%s(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/"
    "(?P<slug>.*)%s$" % _blog_format_string,
    "blog_login_required.views.blog_post_detail", name="blog_post_detail_date"),
url("^%s%s(?P<slug>.*)%s$" % _blog_format_string,
    "blog_login_required.views.blog_post_detail", name="blog_post_detail"),

You also need to define the _blog_format_string somewhere above your url patterns, it looks like this:

from mezzanine.conf import settings

BLOG_SLUG = settings.BLOG_SLUG.rstrip("/")
_blog_format_string = (
    BLOG_SLUG,
    "/" if settings.BLOG_SLUG else "",
    "/" if settings.APPEND_SLASH else "",
)

4. admin.py

Finally, I monkey patched the Mezzanine admin to include login required. To do this I added the following line to admin.py in blog_login_required. For this to work blog_login_required needs to appear before mezzanine.blog in INSTALLED_APPS.

BlogPostAdmin.fieldsets[0][1]["fields"].extend(["login_required"])

Credits

Help with putting migrations in an external app

Comments