Tech Blog

Django CMS Plugin Authenticated User Variations

Django CMS Plugin Authenticated User Variations

Recently updated on

Note: a companion app for this post can be found here: 
https://github.com/ImaginaryLandscape/cmsplugin-auth-content-example

In a recent project using django CMS, we found ourselves in need of serving alternate plugin content to authenticated users. This is not the first time a request such as this has been made, and on past occasions we went for a simple approach of naming separate placeholders for the alternative content in the template, and conditionally toggling visibility.

In this instance, however, the layout of the page and scope of the requirement called for a less rigid approach. Our custom django CMS plugin had to be placeable anywhere on the page and in each instance had to support an optional authenticated user variation.

As is often the case when coding a custom solution, there’s a balance to be made between the versatility of the tool, and usability for site editors. The inclination may be to write an application so generically that it can fit just about any use case an editor might need. This noble quest for DRY elegance and reusability may however, obscure the fact that the final product is turning out to be something of an albatross that your client may well have to deal with for years.

To address this, we came up with some robust solutions like a generic custom parent plugin that can contain any combination of plugin children, using a many-to-many relation to Django’s auth groups for endless user types and permissions, and a fallback for anonymous users. This solution seemed pretty reusable for just about any scenario, but potentially very complicated for an editor to manage. I do think something like this, done well and released as an open source plugin, could serve a nice purpose but probably not for this project (maybe for a subsequent post).

After all, one of the primary benefits of using an application like django CMS is the ease with which you can write your own plugins that are specifically tailored for the requirements of each project.

When we considered what was actually required, a single type of plugin which contained four fields needing an optional authenticated version, it seemed a more focused approach would be to contain this logic inside that plugin. Even then, there is certainly room to abstract the fields into a separate model with a relation to user group. This would be preferable in many scenarios, particularly when dealing with more fields or more user types.

For the sake of clarity and brevity, this example will forego the abstraction and use the basic models.py seen here:

# coding: utf-8
from django.utils.translation import ugettext_lazy as _
from django.db import models
from cms.models import CMSPlugin

class AuthContent(CMSPlugin):
    heading = models.CharField(_("Heading"), max_length=255)
    content = models.TextField(_("Content"), blank=True)
    link = models.URLField(_("Link"), blank=True)
    link_text = models.CharField(_("Link text"), blank=True, max_length=255)

    auth_heading = models.CharField(_("Auth Heading"), max_length=255, blank=True)
    auth_content = models.TextField(_("Auth Content"), blank=True)
    auth_link = models.URLField(_("Auth Link"), blank=True)
    auth_link_text = models.CharField(_("Auth Link text"), blank=True, max_length=255)

    def __unicode__(self):
        if self.auth_heading:
            msg = "- HAS AUTH VERSION"
        else:
            msg = ""
        return "%s %s" % (self.heading, msg)

You’ll see that we’ve simply created 4 fields for the default content, and 4 fields for the authenticated version of that content. The purpose of modifying the __unicode__ method in a CMSPlugin model is to alter what appears in the django-cms structure view. By default, it will only display the ID of the plugin but for quickly reviewing the contents of the plugin, we’re returning the heading field as well as conditionally displaying whether the plugin contains an auth version.

Below is the cms_plugins.py file in its entirety. Note we’ve created two fieldsets for a clearer delineation of the default and auth content fields. You may optionally want to add 'classes': ('collapse',), to the auth fields.

# coding: utf-8
from django.utils.translation import ugettext_lazy as _
from cms.plugin_pool import plugin_pool
from cms.plugin_base import CMSPluginBase

from .models import AuthContent

class AuthContentPlugin(CMSPluginBase):
    module = 'Custom'
    model = AuthContent
    name = _('Auth Content')
    render_template = 'cmsplugin_auth_content/auth_content.html'
    cache = False
    fieldsets = (
        ("Default Content", {
            'fields': ('heading', 'content', 'link', 'link_text')
        }),
        ('Authenticated Content', {
            'description': "Add an optional authenticated user version of this content.",
            'fields': ('auth_heading', 'auth_content', 'auth_link', 'auth_link_text')
        })
    )

    def render(self, context, instance, placeholder):
        context['instance'] = instance
        return context

plugin_pool.register_plugin(AuthContentPlugin)

There was one other issue that we wanted to address in this plugin. When a plugin contained auth content, It would be impossible for a site editor to actually view the default content without publishing the page and logging out. To address that, we decided to add a simple dropdown to the cms toolbar. It sets a query string in the url that can then be used in a template conditional. Here is the complete cms_toolbars.py file.

from cms.toolbar_pool import toolbar_pool
from cms.toolbar_base import CMSToolbar
from django.utils.translation import ugettext_lazy as _

@toolbar_pool.register
class AuthContentToolbar(CMSToolbar):

    def populate(self):
        menu = self.toolbar.get_or_create_menu('authcontent-app', _('View content as..'))
        menu.add_link_item(_('Unauthenticated'), url='?auth_view=false')
        menu.add_link_item(_('Authenticated'), url='?auth_view=true')

The final piece of our plugin is the auth_content.html template seen below. As you can see, our ‘if’ statement checks these three conditions to determine which fields to render.

1. The user is logged in
2. The auth_heading field is not empty
3. The toolbar dropdown isn't set to view unauthenticated user content

The reason we use auth_heading for the conditional in the case is because heading is our only required field. It would of course be possible, if needed, to add an additional method to our AuthContent model checking all auth fields.

{% if request.user.is_authenticated and instance.auth_heading and request.GET.auth_view != "false" %}
<div class="auth-content">
  <h1>{{ instance.auth_heading }}</h1>
  {{ instance.auth_content }}
  {% if instance.auth_link %}
    <a href="{{ instance.auth_link }}">{{ instance.auth_link_text }}</a>
  {% endif %}
</div>
{% else %}
<div class="content">
  <h1>{{ instance.heading }}</h1>
  {{ instance.content }}
  {% if instance.link %}
    <a href="{{ instance.link }}">{{ instance.link_text }}</a>
  {% endif %}
</div>
{% endif %}

We’d love to hear of any other ways people have approached similar tasks or further discussion of what would be useful in a reusable app in the comments. Feel free to install the companion app to try or modify as you see fit.
https://github.com/ImaginaryLandscape/cmsplugin-auth-content-example

Comments

Comments are closed.