Content Architecture

Content in Mezzanine primarily revolves around the models found in two packages, mezzanine.core and mezzanine.pages. Many of these models are abstract, and very small in scope, and are then combined together as the building blocks that form the models you’ll actually be exposed to, such as mezzanine.core.models.Displayable and mezzanine.core.pages.Page, which are the two main models you will inherit from when building your own models for content types.

Before we look at Displayable and Page, here’s a quick list of all the abstract models used to build them:

  • mezzanine.core.models.SiteRelated - Contains a related django.contrib.sites.models.Site field.
  • mezzanine.core.models.Slugged - Implements a title and URL (slug).
  • mezzanine.core.models.MetaData - Provides SEO meta data, such as title, description and keywords.
  • mezzanine.core.models.TimeStamped - Provides created and updated timestamps.
  • mezzanine.core.models.Displayable - Combines all the models above, then implements publishing features, such as status and dates.
  • mezzanine.core.models.Ownable - Contains a related user field, suitable for content owned by specific authors.
  • mezzanine.core.models.RichText - Provides a WYSIWYG editable field.
  • mezzanine.core.models.Orderable - Used to implement drag/drop ordering of content, whether out of the box as Django admin inlines, or custom such as Mezzanine’s page tree.

And for completeness, here are the primary content types provided out of the box to end users, that make use of Displayable and Page:

  • mezzanine.blog.models.BlogPost - Blog posts that subclass Displayable as they’re not part of the site’s navigation.
  • mezzanine.pages.models.RichTextPage - Default Page subclass, providing a WYSIWYG editable field.
  • mezzanine.pages.models.Link - Page subclass for links pointing to other URLs.
  • mezzanine.forms.models.Form - Page subclass for building forms.
  • mezzanine.galleries.models.Gallery - Page subclass for building image gallery pages.

These certainly serve as examples for implementing your own types of content.

Displayable vs Page

Displayable itself is also an abstract model, that at its simplest, is used to represent content that contains a URL (also known as a slug). It also provides the core features of content such as:

  • Meta data such as a title, description and keywords.
  • Auto-generated slug from the title.
  • Draft/published status with the ability to preview drafts.
  • Pre-dated publishing.
  • Searchable by Mezzanine’s Search Engine.

Subclassing Displayable best suits low-level content that doesn’t form part of the site’s navigation - such as blog posts, or events in a calendar. Unlike Page, there’s nothing particularly special about the Displayable model - it simply provides a common set of features useful to content.

In contrast, the concrete Page model forms the primary API for building a Mezzanine site. It extends Displayable, and implements a hierarchical navigation tree. The rest of this section of the documentation will focus on the Page model, and the way it is used to build all the types of content a site will have available.

The Page Model

The foundation of a Mezzanine site is the model mezzanine.pages.models.Page. Each Page instance is stored in a hierarchical tree to form the site’s navigation, and an interface for managing the structure of the navigation tree is provided in the admin via mezzanine.pages.admin.PageAdmin. All types of content inherit from the Page model and Mezzanine provides a default content type via the mezzanine.pages.models.RichTextPage model which simply contains a WYSIWYG editable field for managing HTML content.

Creating Custom Content Types

In order to handle different types of pages that require more structured content than provided by the RichTextPage model, you can simply create your own models that inherit from Page. For example if we wanted to have pages that were authors with books:

from django.db import models
from mezzanine.pages.models import Page

# The members of Page will be inherited by the Author model, such
# as title, slug, etc. For authors we can use the title field to
# store the author's name. For our model definition, we just add
# any extra fields that aren't part of the Page model, in this
# case, date of birth.

class Author(Page):
    dob = models.DateField("Date of birth")

class Book(models.Model):
    author = models.ForeignKey("Author")
    cover = models.ImageField(upload_to="authors")

Next you’ll need to register your model with Django’s admin to make it available as a content type. If your content type only exposes some new fields that you’d like to make editable in the admin, you can simply register your model using the mezzanine.pages.admin.PageAdmin class:

from django.contrib import admin
from mezzanine.pages.admin import PageAdmin
from .models import Author

admin.site.register(Author, PageAdmin)

Any regular model fields on your content type will be available when adding or changing an instance of it in the admin. This is similar to Django’s behaviour when registering models in the admin without using an admin class, or when using an admin class without fieldsets defined. In these cases all the fields on the model are available in the admin.

If however you need to customize your admin class, you can inherit from PageAdmin and implement your own admin class. The only difference is that you’ll need to take a copy of PageAdmin.fieldsets and modify it if you want to implement your own fieldsets, otherwise you’ll lose the fields that the Page model implements:

from copy import deepcopy
from django.contrib import admin
from mezzanine.pages.admin import PageAdmin
from .models import Author, Book

author_extra_fieldsets = ((None, {"fields": ("dob",)}),)

class BookInline(admin.TabularInline):
    model = Book

class AuthorAdmin(PageAdmin):
    inlines = (BookInline,)
    fieldsets = deepcopy(PageAdmin.fieldsets) + author_extra_fieldsets

admin.site.register(Author, AuthorAdmin)

When registering content type models with PageAdmin or subclasses of it, the admin class won’t be listed in the admin index page, instead being made available as a type of Page when creating new pages from the navigation tree.

Note

When creating custom content types, you must inherit directly from the Page model. Further levels of subclassing are currently not supported. Therefore you cannot subclass the RichTextPage or any other custom content types you create yourself. Should you need to implement a WYSIWYG editable field in the way the RichTextPage model does, you can simply subclass both Page and RichText, the latter being imported from mezzanine.core.models.

Displaying Custom Content Types

When creating models that inherit from the Page model, multi-table inheritance is used under the hood. This means that when dealing with the page object, an attribute is created from the subclass model’s name. So given a Page instance using the previous example, accessing the Author instance would be as follows:

>>> Author.objects.create(title="Dr Seuss")
<Author: Dr Seuss>
>>> page = Page.objects.get(title="Dr Seuss")
>>> page.author
<Author: Dr Seuss>

And in a template:

<h1>{{ page.author.title }}</h1>
<p>{{ page.author.dob }}</p>
{% for book in page.author.book_set.all %}
<img src="{{ MEDIA_URL }}{{ book.cover }}">
{% endfor %}

The Page model also contains the method Page.get_content_model for retrieving the custom instance without knowing its type:

>>> page.get_content_model()
<Author: Dr Seuss>

Page Templates

The view function mezzanine.pages.views.page handles returning a Page instance to a template. By default the template pages/page.html is used, but if a custom template exists it will be used instead. The check for a custom template will first check for a template with the same name as the Page instance’s slug, and if not then a template with a name derived from the subclass model’s name is checked for. So given the above example the templates pages/dr-seuss.html and pages/author.html would be checked for respectively.

The view function further looks through the parent hierarchy of the Page. If a Page instance with slug authors/dr-seuss is a child of the Page with slug authors, the templates pages/authors/dr-seuss.html, pages/authors/dr-seuss/author.html, pages/authors/author.html, pages/author.html, and pages/page.html would be checked for respectively. This lets you specify a template for all children of a Page and a different template for the Page itself. For example, if an additional author were added as a child page of authors/dr-seuss with the slug authors/dr-seuss/theo-lesieg, the template pages/authors/dr-seuss/author.html would be among those checked.

Overriding vs Extending Templates

A typical problem that reusable Django apps face, is being able to extend the app’s templates rather than overriding them. The app will usually provide templates that the app will look for by name, which allows the developer to create their own versions of the templates in their project’s templates directory. However if the template is sufficiently complex, with a good range of extendable template blocks, they need to duplicate all of the features of the template within their own version. This may cause the project’s version of the templates to become incompatible as new versions of the upstream app become available.

Ideally we would be able to use Django’s extends tag to extend the app’s template instead, and only override the template blocks we’re interested in. The problem with this however, is that the app will attempt to load the template with a specific name, so we can’t override and extend a template at the same time, as circular inheritance will occur, e.g. Django thinks the template is trying to extend itself, which is impossible.

To solve this problem, Mezzanine provides the overextends template tag, which allows you to extend a template with the same name. The overextends tag works the same way as Django’s extends tag, (in fact it subclasses it), so it must be the first tag in the template. What it does differently is that the template using it will be excluded from loading when Django searches for the template to extend from.

Page Processors

So far we’ve covered how to create and display custom types of pages, but what if we want to extend them further with more advanced features? For example adding a form to the page and handling when a user submits the form. This type of logic would typically go into a view function, but since every Page instance is handled via the view function mezzanine.pages.views.page we can’t create our own views for pages. Mezzanine solves this problem using Page Processors.

Page Processors are simply functions that can be associated to any custom Page models and are then called inside the mezzanine.pages.views.page view when viewing the associated Page instance. A Page Processor will always be passed two arguments - the request and the Page instance, and can either return a dictionary that will be added to the template context, or it can return any of Django’s HttpResponse classes which will override the mezzanine.pages.views.page view entirely.

To associate a Page Processor to a custom Page model you must create the function for it in a module called page_processors.py inside one of your INSTALLED_APPS and decorate it using the decorator mezzanine.pages.page_processors.processor_for.

Continuing on from our author example, suppose we want to add an enquiry form to each author page. Our page_processors.py module in the author app would be as follows:

from django import forms
from django.http import HttpResponseRedirect
from mezzanine.pages.page_processors import processor_for
from .models import Author

class AuthorForm(forms.Form):
    name = forms.CharField()
    email = forms.EmailField()

@processor_for(Author)
def author_form(request, page):
    form = AuthorForm()
    if request.method == "POST":
        form = AuthorForm(request.POST)
        if form.is_valid():
            # Form processing goes here.
            redirect = request.path + "?submitted=true"
            return HttpResponseRedirect(redirect)
    return {"form": form}

The processor_for decorator can also be given a slug argument rather than a Page subclass. In this case the Page Processor will be run when the exact slug matches the page being viewed.

Page Permissions

The navigation tree in the admin where pages are managed will take into account any permissions defined using Django’s permission system. For example if a logged in user doesn’t have permission to add new instances of the Author model from our previous example, it won’t be listed in the types of pages that user can add when viewing the navigation tree in the admin.

In conjunction with Django’s permission system, the Page model also implements the methods can_add, can_change and can_delete. These methods provide a way for custom page types to implement their own permissions by being overridden on subclasses of the Page model.

Each of these methods takes a single argument which is the current request object. This provides the ability to define custom permission methods with access to the current user as well.

Note

The can_add permission in the context of an existing page has a different meaning than in the context of an overall model as is the case with Django’s permission system. In the case of a page instance, can_add refers to the ability to add child pages.

For example, if our Author content type should only contain one child page at most, and only be deletable when added as a child page (unless you’re a superuser), the following permission methods could be implemented:

class Author(Page):
    dob = models.DateField("Date of birth")

    def can_add(self, request):
        return self.children.count() == 0

    def can_delete(self, request):
        return request.user.is_superuser or self.parent is not None