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.pages.models.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 relateddjango.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 subclassDisplayable
as they’re not part of the site’s navigation.mezzanine.pages.models.RichTextPage
- DefaultPage
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.
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()
,
can_delete()
, and can_move()
. These methods provide a way for
custom page types to implement their own permissions by being
overridden on subclasses of the Page
model.
With the exception of can_move()
, each of these methods takes a
single argument which is the current request object, and return a
Boolean. 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.
The can_move()
method has a slightly different interface, as it
needs an additional argument, which is the new parent should the move
be completed, and an additional output, which is a message to be
displayed when the move is denied. The message helps justify reverting
the page to its position prior to the move, and is displayed using
Django messages framework. Instead of a Boolean return value,
can_move()
raises a PageMoveException
when the move is denied,
with an optional argument representing the message to be displayed.
In any case, can_move()
does not return any values.
Note
The can_move()
permission can only constrain moving existing
pages, and is not observed when creating a new page. If you want
to enforce the same rules when creating pages, you need to
implement them explicitly through other means, such as the
save
method of the model or the save_model
method of the
model’s admin.
For example, if our Author
content type should only contain one
child page at most, can only be deleted when added as a child page
(unless you’re a superuser), and cannot be moved to a top-level
position, the following permission methods could be implemented:
from mezzanine.pages.models import Page, PageMoveException
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
def can_move(self, request, new_parent):
if new_parent is None:
msg = 'An author page cannot be a top-level page'
raise PageMoveException(msg)
Integrating Third-party Apps with Pages¶
Sometimes you might need to use regular Django applications within your
site, that fall outside of Mezzanine’s page structure. Of course this is
fine since Mezzanine is just Django - you can simply add the app’s
urlpatterns to your project’s urls.py
module like a regular Django
project.
A common requirement however is for pages in Mezzanine’s navigation to
point to the urlpatterns for these regular Django apps. Implementing
this simply requires creating a page in the admin, with a URL matching
a pattern used by the application. With that in place, the template
rendered by the application’s view will have a page
variable in
its context, that contains the current page object that was created
with the same URL. This allows Mezzanine to mark the page
instance
as active in the navigation, and to generate breadcrumbs for the
page
instance as well.
An example of this setup is Mezzanine’s blog application, which does not
use Page
content types, and is just a regular Django app.