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):

SUMMERNOTE_CONFIG = {
# 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.

Advertisements

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'

    @method_decorator(login_required)
    @method_decorator(permission_required('zinnia.add_entry'))
    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
            else:
                break

        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.save()
        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']
        self.object.save()

        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'

    @method_decorator(login_required)
    @method_decorator(permission_required('zinnia.change_entry'))
    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.

Easily Generate PDFs in Python

A common task in any web application these days is generating files for user reports, the most common being PDFs.  I’ve been building a simple app to track assets and who’s been assigned what asset within a department, and I needed a PDF report of the main page that shows asset allocation.

Naturally, I went with xhtml2pdf since I wanted to take a HTML file, feed it with some context data, and return the output as the PDF.  This is all fine for basic table PDFs, but the moment you add anything fancy like border lines and background colours….things get a bit messed up layout.

After a few minutes of Google search, I came across weasyprint.  It renders the PDFs much better, exactly what is seen in the browser.  For installing this, the recommended way is to go through the CheeseShop as always:

pip install WeasyPrint

But for my Ubuntu 12.04 dev environment, I needed a few more extra plugins to get things installed:

sudo apt-get install libgdk-pixbuf2.0-0 libffi-dev

If these libraries are missing, you’ll get this error:

weasyprint-install-error

So after running through the setup…should take a couple of seconds to download and compile all the dependencies, you can now generate your PDF. I’m developing the system in Django, so here’s what I did:

from django.http import HttpResponse
from django.template import RequestContext, loader
from weasyprint import HTML

context_dict = {
'assets': Item.objects.all(),
}

template = loader.get_template('asset/pdf.html')
html = template.render(RequestContext(request, context_dict))
response = HttpResponse(mimetype='application/pdf')
HTML(string=html).write_pdf(response)

return response

Incase you have inline images using the static templatetag, you might want to make a few edits make a HTTP request on the app since WeasyPrint will have trouble determining the base URL. Note that it might cause a deadlock on a single-threaded server as pointed out in this StackOverflow post:


HTML(string=html, base_url=request.build_absolute_uri()).write_pdf(response)

After that, the result should be a pleasing to look at PDF supporting all sorts of CSS formatting options. For my deployment, I’m using inline CSS in my base template, but you can also feed in more CSS from the included CSS class. More details on this: Python API and StackOverflow

Of course your can always render the HTML directly without an extra library parsing it then return the plain HTML and have the user print the page as a PDF, but this means the user’s browser determines the output….Chrome and Firefox always do a good job here, but you never know what might happen down the line. But if this is OK for you, here’s what you can do:


context_dict = {
'assets': Item.objects.all(),
}
template_name = "asset/pdf.html"
return render_to_response(template_name, context_dict)