Customize Zinnia’s look and feel
The templates provided for Zinnia are simple but complete and as generic as
possible. You can easily change them by
specifying a template directory. If you are not familiar with Django,
part two of the excellent Django tutorial explains in details how to
customize the look and feel of the admin
app:
it’s actually the same thing in Zinnia.
A good starting point is to copy-paste the zinnia/base.html
template, and edit the extends
instruction in order to fit into
your skin.
Note
- The main content is displayed in a block named
content
.
- Additional data is displayed in a block named
sidebar
.
You can also create your own app containing some Zinnia’s templates based
on inheritance. For example you can find these applications which aim
to turn Zinnia’s templates HTML5 ready, which can be a good starting point
to make your own at:
Now that we have seen the basic mechanisms to add and customize Zinnia’s
templates we will see in details the different possibilities in the
customization process.
CSS customizations
Most of the time the customization process of Zinnia is about editing the
cascading style sheet of the differents pages delivered by the Weblog.
First of all you have to note that each page of the Weblog has several
classes applied on the <body>
markup. For examples if the document has
paginated entries, the paginated
and page-{id}
classes will be
added. Many classes are used within the default templates so should take a
look on it, maybe it will be useful for you.
Secondly all the documents served by Zinnia have the zinnia
class name
on the <body>
. If you remove this class, all the default CSS provided
by Zinnia will not be applied. And if you add it on templates provided by
third-party applications, the Zinnia’s style will be applied. Pretty
useful, for enabling or disabling Zinnia’s default style.
Of course adding or removing classes can easily be done in your own
templates by overriding the block named body-class
.
You also have to note that a real effort has be done for providing clean
and valid HTML documents, without redundant and useless classes or IDs
overweighting the document respecting the presentation-free markup
rule.
Now that you have all of these information in mind, you can add new
cascading style sheets into your templates, containing your customization
rules and of course remove the default CSS files provided by Zinnia if
needed.
Variations on the default theme
Beside the zinnia
class name in the <body>
tag of the
zinnia/skeleton.html
template, three other class names are available:
<body class="zinnia default blue right-sidebar {% block body-class %}{% endblock %}">
The default
class name represents the original default theme of
Zinnia. You can remove this class, or replace with the classes light
or
dark
to activate the variations with high readability and contrast,
thanks to the Solarized project.
The blue
class represents the main color used within the
theme. Available color are: yellow
, orange
, red
, magenta
,
violet
, blue
, cyan
, green
.
The right-sidebar
class sets the sidebar at right and left-sidebar
at left, by default if none of these classes are present, the sidebar is
set at right. You can hide the sidebar by using the no-sidebar
class.
With these 3 sets of classes available in the CSS, you now have 4*9*3=108
variations of the default theme available. Try them and choose your
favorite!
Compass and Sass
If you take a look at zinnia/static/zinnia/css/screen.css
you will
probably notice that the CSS is compressed. It has been generated by
Compass and it is good pratice not to edit this file directly.
All the CSS documents are actually generated by the awesome Compass tool
and you must take a look on this video tutorial if your are not familiar
with it.
Compass is an open-source CSS authoring framework which uses the
Sass stylesheet language to make writing stylesheets powerful and
easy.
Aside of zinnia/static/zinnia/css
directory, you can see another
directory named sass
which is organized like this:
sass/
|-- config/
|-- mixins/
|-- partials/
`-- screen.scss
The partials
folder contains all the partials used to build the
CSS, the mixins
folder contains reusable mixins like the tag-cloud
and finally the config
folder contains all the configurable
variables. For example the screen.scss
file will include at the
end all the files who belong in these directories into a single compiled
CSS document, named screen.css
.
This organization allow you to easily customize the default Zinnia’s CSS by
doing a copy of these files or reuse some parts of the Zinnia’s CSS into
third-party templates.
Because Compass and his librairies evolve, here the actual versions of
the gems (Ruby powered !) used to build the CSS documents:
$ gem list
*** LOCAL GEMS ***
chunky_png (1.2.6)
compass (0.12.2)
fssm (0.2.9)
sass (3.2.1)
susy (1.0.1)
Special templates
Since the beginning of Zinnia, the development has been influenced by the
idea of Power templates for easy rendering. Customizing all the
templates of the Weblog must be possible, easy and fast. So Zinnia has a
unique feature for returning custom templates depending on the view’s
context.
Templates for filters
Zinnia as a complete Weblog application provides views for filtering the
last entries by authors, categories and tags. In these views you have the
possibility to use a dedicated template related to the filtering
model. This feature is useful for highlighting a special category or for
providing a template per author.
Each of these views will return a list of templates name to render the page
but only the first template name matching to an existing template will be
used to render.
Examples:
For the URL /blog/categories/events/
the
CategoryDetail
view will be called and
return this list of template names:
['zinnia/category/event/entry_list.html',
'zinnia/category/event_entry_list.html',
'zinnia/category/entry_list.html',
'zinnia/entry_list.html']
For the URL /blog/tags/featured/
the
TagDetail
view will be called and
return this list of template names:
['zinnia/tag/featured/entry_list.html',
'zinnia/tag/featured_entry_list.html',
'zinnia/tag/entry_list.html',
'zinnia/entry_list.html']
For the URL /blog/authors/keneda/
the
AuthorDetail
view will be called and
return this list of template names:
['zinnia/author/keneda/entry_list.html',
'zinnia/author/keneda_entry_list.html',
'zinnia/author/entry_list.html',
'zinnia/entry_list.html']
Templates for archives
Concerning the archive views the same feature is implemented, a list of
template names will be returned depending of the date and the archive
period. This feature take all his sense if want to use Halloween or
Christmas templates for your Weblog. With this feature you can also
program and re-use your themes on several periods.
Another side effect is if you write an Entry during the Halloween
period with dedicated templates, even after the Halloween period the
templates will still be used.
Examples:
For the URL /blog/2012/
the
EntryYear
view will be called and
return this list of template names:
['zinnia/archives/2012/entry_archive_year.html',
'zinnia/archives/entry_archive_year.html',
'zinnia/entry_archive_year.html',
'entry_archive_year.html']
For the URL /blog/2012/week/16/
the
EntryWeek
view will be called and
return this list of template names:
['zinnia/archives/2012/week/16/entry_archive_week.html',
'zinnia/archives/week/16/entry_archive_week.html',
'zinnia/archives/2012/entry_archive_week.html',
'zinnia/archives/entry_archive_week.html',
'zinnia/entry_archive_week.html',
'entry_archive_week.html']
For the URL /blog/2012/04/21/
the
EntryDay
view will be called and
return this list of template names:
['zinnia/archives/2012/04/21/entry_archive_day.html',
'zinnia/archives/month/04/day/21/entry_archive_day.html',
'zinnia/archives/2012/day/21/entry_archive_day.html',
'zinnia/archives/day/21/entry_archive_day.html',
'zinnia/archives/2012/month/04/entry_archive_day.html',
'zinnia/archives/month/04/entry_archive_day.html',
'zinnia/archives/2012/entry_archive_day.html',
'zinnia/archives/entry_archive_day.html',
'zinnia/entry_archive_day.html',
'entry_archive_day.html']
Templates for entry detail
Each entries of the Weblog has the possibility to have his own template to
be rendered by using the ZINNIA_ENTRY_TEMPLATES
settings, so
with this option you can handle multiple presentation for your entries. And
because EntryDetail
is based on an archive
view a custom list of templates is built uppon the publication date.
The entry’s slug is also used to build the template list for having
maximal customization capabilities with ease.
For example if I use the custom.html
template to render the entry
located at the URL /blog/2012/04/21/my-entry/
the list of template
names will be:
['zinnia/archives/2012/04/21/my-entry_custom.html',
'zinnia/archives/month/04/day/21/my-entry_custom.html',
'zinnia/archives/2012/day/21/my-entry_custom.html',
'zinnia/archives/day/21/my-entry_custom.html',
'zinnia/archives/2012/04/21/my-entry.html',
'zinnia/archives/month/04/day/21/my-entry.html',
'zinnia/archives/2012/day/21/my-entry.html',
'zinnia/archives/day/21/my-entry.html',
'zinnia/archives/2012/04/21/custom.html',
'zinnia/archives/month/04/day/21/custom.html',
'zinnia/archives/2012/day/21/custom.html',
'zinnia/archives/day/21/custom.html',
'zinnia/archives/2012/month/04/my-entry_custom.html',
'zinnia/archives/month/04/my-entry_custom.html',
'zinnia/archives/2012/month/04/my-entry.html',
'zinnia/archives/month/04/my-entry.html',
'zinnia/archives/2012/month/04/custom.html',
'zinnia/archives/month/04/custom.html',
'zinnia/archives/2012/my-entry_custom.html',
'zinnia/archives/2012/my-entry.html',
'zinnia/archives/2012/custom.html',
'zinnia/archives/my-entry_custom.html',
'zinnia/my-entry_custom.html',
'my-entry_custom.html',
'zinnia/archives/my-entry.html',
'zinnia/my-entry.html',
'my-entry.html',
'zinnia/archives/custom.html',
'zinnia/custom.html',
'custom.html']
Now you have the choice !
Changing templates
Maybe CSS customizations and adding markup to the templates is not enough
because you need to change a more important part of the templates or you
simply don’t want to use it.
Because all the front views bundled in Zinnia are customizable, changing
the template used to render the view is pretty easy and can be a good
solution for you if you are confortable with Django.
Example of changing the default template for the search view by another
view:
from zinnia.views.search import EntrySearch
class CustomTemplateEntrySearch(EntrySearch):
template_name = 'custom/template.html'
or directly in the urls:
from django.conf.urls import url
from django.conf.urls import patterns
from zinnia.views.search import EntrySearch
urlpatterns = patterns(
'',
url(r'^$', EntrySearch.as_view(
template_name='custom/template.html'),
name='zinnia_entry_search'),
)
Going further
As you can see that you can customize the look and feel of Zinnia by CSS,
SASS, HTML and Python and even by adding custom views. So why don’t you
make a Python package containing a Django application of your complete
theme ? The theme of your weblog will be sharable and easily
installable. Remember to take a look at Zinnia-theme-html5 for having a
good starting point of a packaged theme.
Extending Entry model
The Entry
model bundled in Zinnia can now be extended and customized.
This feature is useful for who wants to add some fields in the model,
or change its behavior. It also allows Zinnia to be a really generic
and reusable application.
Why extending ?
Imagine that I find Zinnia really great for my project but some fields
or features are missing to be the Weblog app that suits to my project.
For example I need to add a custom field linking to an image gallery,
two solutions:
- I search for another Django blogging app fitting my needs.
- I do a monkey patch, into the Zinnia code base.
These two solutions are really bad.
For the first solution maybe you will not find the desired application and
also mean that Zinnia is not a reusable application following the Django’s
convention. For the second solution, I don’t think that I need to provide
more explanations about the evil side of monkey patching (evolution,
reproduction...). That’s why Zinnia provides a third generic solution.
- Customizing the
Entry
model noninvasively with the power of
class inheritance !
The extension process is done in three main steps:
- Write a class containing your customizations.
- Register your class into Zinnia to be used.
- Update the
EntryAdmin
class accordingly.
Writing model extension
In the suite of this document we will show how to add an image gallery into
the Entry
model to illustrate the concepts involved when
extending. We assume that the pieces of codes written for this document
belong in the zinnia_gallery
module/application.
The zinnia.models.entry.EntryAbstractClass
has been moved and
renamed to zinnia.models_bases.entry.AbstractEntry
.
The first step to extend the Entry
model is to define a new class
inherited from the AbstractEntry
and
add your fields or/and override the inherited methods if needed. So in
zinnia_gallery
let’s write our gallery models and the extension in
the Entry
model in models.py
.
from django.db import models
from zinnia.models_bases.entry import AbstractEntry
class Picture(models.Model):
title = models.CharField(max_length=50)
image = models.ImageField(upload_to='gallery')
class Gallery(models.Model):
title = models.CharField(max_length=50)
pictures = models.ManyToManyField(Picture)
class EntryGallery(AbstractEntry):
gallery = models.ForeignKey(Gallery)
def __unicode__(self):
return u'EntryGallery %s' % self.title
class Meta(AbstractEntry.Meta):
abstract = True
In this code sample, we simply add in our Entry
model a new
ForeignKey
field named gallery
pointing to a
Gallery
model and we override the Entry.__unicode__()
method.
Note
You have to respect 2 important rules to make extending working :
- Do not import the
Entry
model in your file defining
the extended model because it will cause a circular
importation.
- Don’t forget to tell that your model is
abstract
. Otherwise a
table will be created and the extending process will not work
as expected.
See also
Model inheritance for more information about the concepts
behind the model inheritence in Django and the limitations.
Writing model customisation
Adding fields is pretty easy, but now that the Entry
model has
been extended, we want to change the image
field wich is an
ImageField
by default to use our new
Picture
instead.
To customise this field, the same process as extending apply, but we can
take advantage of all the abstracts classes provided to build the
AbstractEntry
to rebuild our own custom
Entry
model like this:
from django.db import models
from zinnia.models_bases import entry
class Picture(models.Model):
title = models.CharField(max_length=50)
image = models.ImageField(upload_to='gallery')
class Gallery(models.Model):
title = models.CharField(max_length=50)
pictures = models.ManyToManyField(Picture)
class EntryGallery(
entry.CoreEntry,
entry.ContentEntry,
entry.DiscussionsEntry,
entry.RelatedEntry,
entry.ExcerptEntry,
entry.FeaturedEntry,
entry.AuthorsEntry,
entry.CategoriesEntry,
entry.TagsEntry,
entry.LoginRequiredEntry,
entry.PasswordRequiredEntry,
entry.ContentTemplateEntry,
entry.DetailTemplateEntry):
image = models.ForeignKey(Picture)
gallery = models.ForeignKey(Gallery)
def __unicode__(self):
return u'EntryGallery %s' % self.title
class Meta(entry.CoreEntry.Meta):
abstract = True
Now we have an Entry
model extended with a gallery of pictures and
customised with a Picture
model relation as the image
field.
Note that the same process apply if you want to delete some built-in fields.
Considerations about the database
If you do the extension of the Entry
model after the syncdb
command, you have to manually alter the Zinnia’s tables for reflecting your
changes made on the model class. In the case where your database is empty,
you can simply execute the reset
command on the Zinnia application for
destroying the old database schema and installing the new one.
Now if you are using South and try to write a new migration for
reflecting your changes, the migration script will be written in the
zinnia.migrations
module, which is not recommended because the
result is not replicable for multiple installations and breaks the
migration system with future releases of Zinnia.
Fortunatly South provides an elegant solution with the
SOUTH_MIGRATION_MODULES setting. Once this setting done for the
'zinnia'
key, because you are now out the Zinnia’s default migrations
flow, you have to delete the ghost migrations for Zinnia. At this step you
can now start to write new migrations.
It’s recommended that the new initial migration represents the default
Entry
schema provided by Zinnia, because after that, you just have
to write a new migration for reflecting your changes, and you can alter
your database schema with the migrate
command.
Registering the extension
Once your extension class is defined you simply have to register it,
with the ZINNIA_ENTRY_BASE_MODEL
setting in your Django
settings. The expected value is a string representing the full Python path
to the extented model’s class name. This is the easiest part of the
process.
Following our example we must add this line in the project’s settings.
ZINNIA_ENTRY_BASE_MODEL = 'zinnia_gallery.models.EntryGallery'
If an error occurs when your new class is imported a warning will be raised
and the EntryAbstractClass
will be used.
Updating the admin interface
Now we should update the Entry
‘s admin class to reflect our
changes and use the new fields.
To do that we will write a new admin class inherited from
EntryAdmin
and use the admin site
register/unregister mechanism for using our new class.
In the file zinnia_gallery/admin.py
we can write these code lines
for adding the gallery field:
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from zinnia.models.entry import Entry
from zinnia.admin.entry import EntryAdmin
class EntryGalleryAdmin(EntryAdmin):
# In our case we put the gallery field
# into the 'Content' fieldset
fieldsets = ((_('Content'), {'fields': (
'title', 'content', 'image', 'status', 'gallery')}),) + \
EntryAdmin.fieldsets[1:]
# Unregister the default EntryAdmin
# then register the EntryGalleryAdmin class
admin.site.unregister(Entry)
admin.site.register(Entry, EntryGalleryAdmin)
Note that the zinnia_gallery
application must be registered in the
INSTALLED_APPS
setting after the zinnia
application for
applying the register/unregister mechanism in the admin site.
Now we can easily
customize the templates
provided by Zinnia to display the gallery field into the Weblog’s pages.
For more information you can see another implementation example in the
cmsplugin-zinnia package.
Rewriting Entry’s URL
By default the Entry
model implements a default
get_absolute_url()
method to retrieve the canonical URL for an
instance into the Weblog.
See also
get_absolute_url()
for more information about
the usage of this method if your are not familiar with this concept.
The result of this method is a string composed of the entry’s
creation date
and the slug
. For example this URL:
/blog/2011/07/17/how-to-change-url/
refers to an entry created on the
17th July 2011 under the slug how-to-change-url
.
This URL pattern is common for most of the Weblog engines and have these
following advantages.
- SEO Friendly.
- Human readable.
- You can remove parts of the URL and find archives.
- The slug is unique with the creation date, so you can reuse it.
But if you want to change it into a different form, you have to know that
it’s possible, but not easy.
You have to note that the changes required on the Zinnia’s code base to
simplify this customization step in a generic way, are evil, dirty and
unsecured. You will see throughout this document why this customization is
not directly implemented, why it cannot be handled genericaly and which are
the pitfalls to avoid.
Warning
Before further reading, you have to note that the methods explained
below are reserved for confirmed Django developers, knowing what they
are doing. No warranties and no support will be provided for the
problems encountered if you customize this part of Zinnia.
Choosing your new URL pattern
We can imagine many different forms of new URL for your entries:
/blog/<id>/
/blog/<slug>/
/blog/<year>/<slug>/
/blog/<creation-date>-<slug>/
/blog/<slug>/<tag-1>/<tag-n>/
/blog/<category-1>/<category-n>/<slug>/
As you can see we can imagine a lot of new patterns to handle the canonical
URL of an entry. But you must keep in mind that you must have a unique URL
per entry.
Like we said above, the slug is unique with the creation date, so only
using the entry’ slug to retrieve the matching Entry
instance
is not safe, because the view will fail if you have 2 entries with the
same slug.
If you want to decorate the entry’s slug with the categories’ slugs of the
entry, or with some additionnal datas (like in the latest examples), make
sure that you can write an efficient regular expression for capturing text
in the URL. The complexity of the URL’s regexp will depend on the pattern
choosen for the new URL.
For the rest of this document we will show how to change the entry’s URL
with the /blog/<id>/
pattern. This is just to illustrate the facts
presented in this document, because this pattern is already handled by the
default URL Shortener backend, but have the advantage to be
perfect for this tutorial.
We assume that the code involved in this document belong in the
zinnia_customized
package/application. This package will contain all
the pieces of code to customize the default behaviour of Zinnia.
Accordingly to your new URL pattern you have to override the
Entry.get_absolute_url()
method to pass the desired parameters to
build the canonical URL of an entry.
To do this override, simply use the method explained in the
Extending Entry model document to create a new class based on
AbstractEntry
with the new
get_absolute_url
method.
class EntryWithNewUrl(AbstractEntry):
"""Entry with '/blog/<id>/' URL"""
@models.permalink
def get_absolute_url(self):
return ('zinnia_entry_detail', (),
{'pk': self.id})
class Meta(AbstractEntry.Meta):
abstract = True
Due to the intensive use of this method into the templates, make sure that
your re-implemention is not too slow. For example hitting the database to
recontruct this URL is not a really good idea. That’s why an URL pattern
based on the categories like /blog/<category-1>/<category-n>/<slug>/
is
really bad.
Adding your view
Now we must write a custom view to handle the detailed view of an
Entry
instance from the text parameters passed in the URL.
So in a module called zinnia_customized.views
we can write this view
for handling our new URL.
from django.views.generic.detail import DetailView
from zinnia.models.entry import Entry
from zinnia.views.mixins.entry_protection import EntryProtectionMixin
class EntryDetail(EntryProtectionMixin, DetailView):
queryset = Entry.published.on_site()
template_name_field = 'template'
Pretty easy isn’t it ? For more information, check the documentation about
the DetailView
view. Note that the
EntryProtectionMixin
is used for enabling
password and login protections if needed on the entry.
Configuring URLs
The final step to rewrite the entry’s URL, is to change the URLconf for
the Weblog application. Instead of using the default implementation
provided by zinnia.urls
in your project’s URLconf, you have to
re-implement all the URLsets provided by Zinnia as described in the
URLs section of the installation process.
But instead of including zinnia.urls.entries
you will include your own
URLconf containing the new URL code for the canonical URL of your
entries. Doing a copy of the original module in your own project can save
you a lot time.
...
url(r'^weblog/', include('zinnia_customized.urls')),
...
Now in zinnia_customized.urls
rewrite the url()
named 'zinnia_entry_detail'
with your new regular expression handling the
canonical URL of your entries and the text parameters. Don’t forget to also
change the path to your view retrieving the Entry
instance from
the text parameters.
from zinnia_customized.views import EntryDetail
url(r'^(?P<pk>\d+)/$',
EntryDetail.as_view(),
name='zinnia_entry_detail')
Warning
If you use the pingback XML-RPC service, you will also need change
to pingback_ping()
function for retrieving
the Entry
instance, accordingly to the new text parameters
captured in the URL.
Actually you should consider Zinnia like a ready to use Weblog application
and also like a framework to make customized Weblog engines.
Import / Export
If you already have a blog, Zinnia has the ability to import your posts
from other blogging platforms. Useful for rapid migration.
From WordPress to Zinnia
Zinnia provides a command for importing export files from WordPress.
http://codex.wordpress.org/Tools_Export_SubPanel
Once you have the XML file, you simply have to do this.
$ python manage.py wp2zinnia path/to/your/wordpress.xml
This command will associate the post’s authors to User and
import the tags, categories, post and comments.
For the options execute this.
$ python manage.py help wp2zinnia
From Zinnia to WordPress
Zinnia also provides a command for exporting your blog to WordPress in the
case you want to migrate on it.
Simply execute this command:
$ python manage.py zinnia2wp > export.xml
Once you have the XML export, you can import it into your WordPress site.
http://codex.wordpress.org/Importing_Content
From Blogger to Zinnia
If you are comming from Blogger, you can import your posts and comments
with this simple command:
$ python manage.py blogger2zinnia
For the options execute this.
$ python manage.py help blogger2zinnia
Note that you need to install the gdata package to run the importation.
From Feed to Zinnia
If you don’t have the possibility to export your posts but have a RSS or Atom
feed on your Weblog, Zinnia can import it. This command is the most generic
way to import content into Zinnia. Simply execute this command:
$ python manage.py feed2zinnia http://url.of/the/feed
For the options execute this.
$ python manage.py help feed2zinnia
Note that you need to install the feedparser package to run the
importation.