Tech Blog

Content written on chalkboard

CMS Placeholders and YOU!

Recently updated on

Here at Imaginary, a number of our client sites make use of the excellent Django-CMS which provides CMS pages that can be populated with a variety of content plugins. This allows client faculty to manage content on CMS-specific pages without the continued involvement by our production staff. This is pretty handy. However, a problem with this system arises when a single page needs both CMS editable content and traditional Django view logic. For a long time I wished that CMS supported a way for me to embed arbitrary CMS content on non-CMS pages. This would allow me to control the view logic but retain the ability for end-users to manage the content in a way to which they are accustomed.

I am by no means a Django-CMS expert, but it was hard to accept that there was no way to accomplish this. In fact, I spent a long time in #django-cms on Freenode attempting to explain to a couple CMS devs exactly what I was trying to do. Despite this being a fairly straightforward feature to desire, I had a hard time even communicating exactly how I thought it should work. I was pointed at AppHooks several times but, alas, this is not the same thing.

Eventually, after simply staring at the Django-CMS documentation and source-code for a while, the answer came to me. The core component to the solution is essentially leveraging CMS' PlaceholderField. The PlaceholderField is a bit magical (and a bit finicky), but I was able to come up with a fairly friendly way to associate Placeholders with my own view/pages.  The technique essentially boils down to wrapping up the PlaceholderField into a new Model that I call CMSCopy and automatically associating instances with my views by way of a decorator.

Here is the (very simple) CMSCopy model:

  from django.db import models

  from cms.models.fields import PlaceholderField

  class CMSCopy(models.Model):
    slug = models.SlugField()
    content = PlaceholderField('content')

    def __unicode__(self):
      return "CMSCopy: '%s'" % self.slug

With only two fields, there is not much to explain here. The slug allows the CMSCopy to have a unique, human-readable identifier and the content is provided by a Placeholder. Anytime I need to provide a view with somewhere to stick CMS content, I simply create a CMSCopy instance specifically for that view. The view then simply needs to provide the template context with the CMSCopy. Since this is repetitive to do, the decorator makes it nearly effortless. The end result looks like:

  @include_copy('anon_landing', 'member_landing')
  def landing(request):
    context = dict()
    if request.user.is_authenticated():
      return PartialResponse('members/landing.html', context)
    else:
      return PartialResponse('members/anon_landing.html', context)

The astute will immediately recognize that the views are returning a mysterious PartialResponse object instead of the normal Django HttpResponse objects. The PartialResponse implementation is as follows:

 class PartialResponse(object):
  def __init__(self, template, context):
   self.template = template
   self.context = context

An exceedingly simple class that stores the template and partial context. We return a PartialResponse instead of full HttpResponse because the decorator needs to add the CMSCopy instances to the context before the view truly returns. The full decorator implementation is below. Essentially what you see is that the decorator (at import-time) will create any CMSCopys you have named, if they don't already exist. When the view is actually called, the decorator takes the PartialResponse, adds the relevant CMSCopy objects to the context, and returns the full response. The Placeholders can then be rendered in the template with the CMS templatetag, `render_placeholder`:

  {% extends "core/base.html" %}

  {% load placeholder_tags %}

  {% block content %}
   {% render_placeholder homepage_top %}
   {% render_placeholder homepage_bottom %}
  {% endblock %}

The include_copy decorator:

  class include_copy(object):
    def __init__(self, *slugs):
      self.slugs = slugs

      for slug in slugs:
        (copy_obj, created) = CMSCopy.objects.get_or_create(slug=slug)
        if created: # create missing placeholder
          old_ph = copy_obj.content
          ph = Placeholder.objects.create(slot=slug)
          add_plugin(ph, TextPlugin, 'en')
          copy_obj.content = ph
          copy_obj.save()
          old_ph.delete()

    def __call__(self, view):
      def wrapped_f(request, *args, **kwargs):
        copyctx = {} # query for the view's copy
        for slug in self.slugs:
          copyctx[slug] = CMSCopy.objects.get(slug=slug).content

        # call view as normal
        response = view(request, *args, **kwargs)
        if isinstance(response, PartialResponse):
          template = response.template
          context = response.context
          # update context with copy objects
          context.update(copyctx)
          # return rendered response
          return render_to_response(template, context,
                       context_instance=RequestContext(request))
        # return view's original http response
        else:
          return response
      return wrapped_f

 

Comments

Comments are closed.