Zinnia Customisations Part II

In the first part of this series, we looked at how to create an app that will hold the customisations we want done to zinnia (customised pages for adding and editing blog posts).  Now, it’s time to wrap this app and tie the app into the rest of the site’s structure and enable users to start using the new logic.

To kick things off, you need two template files copied into your root templates folder (For my test project, I’ve also added the zinnia-theme-bootstrap helper theme to bootstrap the template files):

  • base.html : zinnia’s base template
  • entry_detail_base.html : renders blog post info

Note that if you do end up using the extra Bootstrap 3 theme, your extend statements will be a bit different, as well as the overall HTML markup, but the gist of the changes remains the same.  For example the default base.html template is:

{% extends "zinnia/skeleton.html" %}

But after adding the theme, is becomes:

{% extends "zinnia:zinnia/base.html" %}

Note the app namespace template loader syntax used in the extend statement.  This helper app is installed along with zinnia-theme-bootstrap and it enables you to both extend and override a template at the same time.  So essentially what that one line does is extend the default base.html template in zinnia’s template directory, and further override a specific block within that template, in this case the sidebar. Pretty cool stuff.  It’s an improvement over Django’s default template loaders that require you to copy the entire template you want to override, even if you only want to override one small block, and allows you to extend and override a specific block in the extended template.

So you’ll now want to start updating the templates you copied over into your project’s root template folder, starting with base.html.  Look for this line somewhere towards the end of the HTML markup:
<a href="{% url 'admin:zinnia_entry_add' %}" title="{% trans "Post an entry" %}">

Change it to:

<a href="{% url 'custom_add_blog_post' %}" title="{% trans "Post an entry" %}">

Then let’s move on to entry_detail_base.html.  Here, you’ll want to look for this line:

<a href="{% url 'admin:zinnia_entry_change' object.pk %}" title="{% trans "Edit the entry" %}">

Change it to:

<a href="{% url 'custom_edit_blog_post' object.pk %}" title="{% trans "Edit the entry" %}">

And finally, two last steps.  First your URL conf entries by add this in a non-conflicting location:

(r'^summernote/', include('django_summernote.urls')),
url(r'^', include('customisations.urls')),

Also, you’ll need to update settings.py:

First, add your new apps into INSTALLED_APPS:

  1. ‘chosen’,
  2. ‘django_summernote’,
  3. ‘customisations’,

And lastly, some settings needed by django-summernote (full reference here):

# Using SummernoteWidget - iframe mode
# or set False to use SummernoteInplaceWidget - no iframe mode
'iframe': True, 

# Change editor size
'width': '100%',

# Set editor language/locale
'lang': 'en-US',

And that wraps up things.  Hope this helps you.  There are plenty of customisations you can do…this is just one of the many.  The only limitation is your creativity with extending the core code I guess.


Zinnia Customisations Part I

I got the pleasure of working on customising a few things in Zinnia a while back, and been meaning to write up about this for a while now.  The customisation was fairly simple…instead of the user being redirected to the admin to create or edit blog posts, they should remain within the same site layout the rest of the website uses.  So, here’s how I did that. I’ve also tweaked things a bit here, so you’ll need to grab a few items from the Cheeseshop on top of zinnia itself. Note that at the time the site I was working on was built, Zinnia was at v0.14, hence the pip version prefix:

pip install django-blog-zinnia==0.14 django-summernote==0.5.5 django-chosen

Next, depending on your setup, you might have to create an app to hold these customisations you are making.  So run: ./manage.py startapp customisations Then let’s get started. First, a few lines for your views file in the just created app.

from django.template.defaultfilters import slugify
from django.contrib.sites.models import Site
from django.views.generic import CreateView, UpdateView
from django.http import HttpResponseRedirect
from django.utils.encoding import smart_str
from django.utils.html import linebreaks
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import permission_required, login_required

from zinnia.models.entry import Entry
from zinnia.settings import MARKUP_LANGUAGE

from .forms import CustomEntryForm

class CreateBlogEntryView(CreateView):
    form_class = CustomEntryForm
    template_name = 'blog/new_blog_entry.html'

    def dispatch(self, *args, **kwargs):
        return super(CreateBlogEntryView, self).dispatch(*args, **kwargs)

    def form_valid(self, form):
        potential_slug = slug = slugify(form.cleaned_data['title'])

        # ensure unique slug
        i = 2
        while True:
            if Entry.objects.filter(slug=potential_slug).count() > 0:
                potential_slug = u'%s-%s' % (slug, i)
                i += 1

        self.object = form.save(commit=False)
        self.object.content = smart_str(self.htmlize(form.cleaned_data['content']))
        self.object.slug = potential_slug
        self.object.content_template = 'zinnia/_entry_detail.html'
        self.object.detail_template = 'entry_detail.html'
        self.object.tags = form.cleaned_data['tags']
        self.object.sites = [Site.objects.get_current().pk]
        self.object.authors = form.cleaned_data['authors']
        self.object.related = form.cleaned_data['related']
        self.object.categories = form.cleaned_data['categories']

        return HttpResponseRedirect(self.object.get_absolute_url())

    def htmlize(self, content):
        Convert to HTML the content if the MARKUP_LANGUAGE
        is set to HTML to optimize the rendering and avoid
        ugly effect in WYMEditor.
        if MARKUP_LANGUAGE == 'html':
            return linebreaks(content)
        return content

class UpdateBlogEntry(UpdateView):
    model = Entry
    form_class = CustomEntryForm
    template_name = 'blog/update_blog_entry.html'

    def dispatch(self, *args, **kwargs):
        return super(UpdateBlogEntry, self).dispatch(*args, **kwargs)

Then update urls.py to point to these views. Choose appropriate names for your confs

from django.conf.urls import url
from django.conf.urls import patterns

import views

urlpatterns = patterns('',
    url(r'^new-blog-entry/$',views.CreateBlogEntryView.as_view(), name='custom_add_blog_post'),
    url(r'^update-blog-entry/(?P[\w-]+)$', views.UpdateBlogEntry.as_view(), name='custom_edit_blog_post'),

You’ll also notice the forms import in views.py, so create that in your app’s forms.py file.

from django import forms

from zinnia.models.entry import Entry
from zinnia.models.category import Category
from zinnia.models.author import Author
from django_summernote.widgets import SummernoteWidget
from chosen import forms as chosenforms

class CustomEntryForm(forms.ModelForm):
    Form for posting a blog entry
    Also reused by the edit view
    authors = chosenforms.ChosenModelMultipleChoiceField(
        queryset=Author.objects.all(), required=False, overlay='Select One or More Authors'
    related = chosenforms.ChosenModelMultipleChoiceField(
        queryset=Entry.objects.all(), required=False, overlay='Select Related Blog Entries'
    categories = chosenforms.ChosenModelMultipleChoiceField(
        queryset=Category.objects.all(), required=False, overlay='Select Blog Categories'

    class Meta:
        model = Entry
        exclude = (
            'slug', 'sites', 'creation_date', 'last_update', 'comment_count', 'pingback_count',
            'trackback_count', 'content_template', 'detail_template'
        widgets = {
            'content': SummernoteWidget(),

Now we have pretty much everything except the templates. I have these in a folder named ‘blog’ within the app’s template folder. Depending on how much of a DRY purist you are, you might split the add/edit template into three, or even more files. For me, three was enough:

  • _form_fields.html: holds the actual form fields used in the form
  • new_blog_entry.html: for when you are creating a new blog post
  • update_blog_entry.html: for when you are editing an existing blog post

The code for these is a bit too long, so to save time, you can get the app as a zip folder from Dropbox.  That’s it for the first part of this post.  In the second and last post, we’ll go through making these changes available for users.  Currently, all this awesomeness is invisible to the user.