Publishing in GLAMkit¶
The publishing system used in GLAMkit is a re-implementation of concepts
and code from
`django-model-publisher
<https://github.com/jp74/django-model-publisher>`__,
though heavily customised for our purposes.
It has been customised to work with the fluent style of projects with polymorphic, translatable models and other such fun.
There are many considerations when using the publishing system with much history which cannot be covered completely here. This is an attempt to document the major pieces to allow people to understand how to use the publishing system and major ideas that need to be known.
Use Publishing in your Project¶
To use GLAMkit Publishing in your project:
- make sure you install GLAMkit with the optional ‘publishing’ extra to get any required libraries, e.g. in your setup.py include something like: ‘django-icekit[forms,search,publishing]’
- add
'icekit.publishing'
toINSTALLED_APPS
- add
'icekit.publishing.middleware.PublishingMiddleware'
toMIDDLEWARE_CLASSES
Once set up in this way, the page and plugin models defined in GLAMkit
such as ArticlePage
and SlideShow
will gain publishing features
in your project.
Make Custom Publishable Models¶
While GLAMkit’s models will general have publishing features built-in, you can add publishing to your own models by subclassing both the model and admin base classes provide in GLAMkit.
To make a standard Django model publishable:
- subclass your model from
icekit.publishing.models.PublishingModel
- subclass your model’s admin from
icekit.publishing.admin.PublishingAdmin
To make a FluentContentsPage
model publishable:
- subclass your model from
icekit.publishing.models.PublishableFluentContentsPage
- subclass your model’s admin from
FluentContentsPageAdmin
andicekit.publishing.admin.PublishingAdmin
To make a fluent contents model (see ContentsPlugins) publishable:
- subclass your model from
icekit.publishing.models.PublishableFluentContents
- subclass your model’s admin from
icekit.publishing.admin.PublishableFluentContentsAdmin
Note: Validating slug uniqueness¶
In publishable models, both the draft and published slugs will be
identical, which means declaring your SlugField
with unique=True
will cause Integrity Errors when you try to publish a model instance.
To address this, add
unique_together = (("slug", "publishing_is_draft"),)
to your model’s
Meta
class.
Once you have modified your project’s models, make new DB migrations to apply the additional database fields required for publishing.
Setting up Admin if you are using Fluent Pages¶
To set up admin for your Fluent Pages:
- add the setting
FLUENT_PAGES_PARENT_ADMIN_MIXIN
with the value'icekit.publishing.admin.ICEKitFluentPagesParentAdminMixin'
- this is used for the listing page admin, which, in Polymorphic models, is separate to the admins for each Page type. - ensure your Admin for each page subclasses
FluentContentsPageAdmin
andicekit.publishing.admin.PublishingAdmin
as above.
If your admin needs to render a custom change_form_template
, this
template should extend admin/fluentpage/change_form.html
, not
admin/publishing/publishing_change_form.html
, which is injected, and
inherits from your template using
{% extends non_publishing_change_form_template %}
.
Filters¶
Consider providing the publishing-related admin filters provided in
icekit.publishing.admin
such as PublishingStatusFilter
, and the
publishing status column for listing pages by adding
‘publishing_column’ to your admin’s list_display
attribute.
Draft Request Context¶
The icekit.publishing.middleware.PublishingMiddleware
middleware
class allows privileged users to view draft pages and page content
before it has been published by adding the ‘preview’ GET parameter to page
URLs, for example: http://site.com/welcome-page/?preview
For the draft request context mechanism to work you must define a text
model setting DRAFT_SECRET_KEY
in the CMS admin at
/admin/model_settings/setting/ and provide secret value of some kind –
any long password-like text is fine.
If you need to perform custom logic to show content to privileged users
you can use the is_draft_request_context()
global function defined
with the middleware that will return true if, and only if, a privileged
user has explicitly requested to view the draft version of a page by
providing the ‘preview’ GET parameter.
Implementation details¶
Usage¶
As complicated as the publishing implementation is behind the scenes – and it is unfortunately very complicated – the following usage guidelines should be enough to use it properly in most situations:
When rendering publishable items, be sure to retrieve only the items that should be visible to the current user – draft items for privileged users, published items for everyone else:
- use the
visible()
queryset method on a QS to publishable items - use the
get_visible()
object method on an object’s FK relationship to a publishable item, which may returnNone
if the target item is only a draft - use the
is_visible
object status flag on a target publishable object if you need to process a set of draft and published items in code and cannot easily use one of the mechanisms above, or - use the
has_been_published
object status flag on publishable objects when you are processing a set of draft and published copies and need to find out whether an object has been published regardless of whether the current object happens to be a draft or published copy. This is basically equivalent toget_visible() is not None
.
Draft Content Protection¶
If you forget to explicitly look up the visible version of publishable
items, you will get the draft version instead and could risk displaying
draft content to the public. To avoid this, the publishing
implementation includes a booby trap that should raise a
PublishingException
in this situation with a message like “Illegal
attempt to access ‘title’ on a DRAFT publishable item…”. If you see
that, check that you are obtaining the correct visible or published
version of items.
If you are sure you want to access draft attributes within a published
context, you can use get_draft_payload()
on the draft item, or add
the attribute to PUBLISHING_PERMITTED_ATTRS
on the model. pk
is
accessible by default, but most other attributes (particularly reverse
relations) will need to be added to PUBLISHING_PERMITTED_ATTRS
individually.
For some situations you might need to get just the published or draft copies of items, such as for the search indexes we only ever want published copies to be indexed regardless of the privileges of the user/process that triggers the indexing. In these situations, you can use the corresponding queryset methods and model methods/fields:
- the
published()
queryset method andget_published()
model method return the published copy of an item in all cases, regardless of the privileges of the current user. This is useful for rendering content that should always and only be safe for public consumption. - the
draft()
queryset method andget_draft()
model method return the draft copy of an item in all cases, regardless of the privileges of the current user. This is useful for filtering items within the Django admin, where only draft items should be accessible.
There are many different states an object can be in. This attempts to cover at least some of them.
Check if an object is the draft object¶
To check if an object is the draft object use the is_draft
property
which will return True
if the specific publishable item is a draft
copy, False
otherwise. This will always return the opposite of
is_published
.
Check if an object is the published object¶
To check if an object is the published object use the is_published
property which returns True
if the specific publishable item is a
published copy, False
otherwise. This will always return the
opposite of is_draft
.
Check if an object has been published¶
To check if a publishable item has been published, regardless of whether
the item you are working with happens to be a draft or published copy,
use the has_been_published
property. This returns True
if the
item is itself published, or is a draft that has a published copy.
Data model¶
The general gist is that every item in Django’s CMS admin is created a draft copy, which may or may not have an associated published copy. When a draft copy is published it is duplicated, along with some processing of related content, such that the DB will contain two copies of the same item: one draft, one published. The Django admin remains largely oblivious to the existence of published copies. When displaying content to users, the draft or published version of publishable items is rendered depending on the privileges of the user: admins might see draft content rendered, whereas the public must only ever see rendered versions of the corresponding published copy (if there is one).
NOTE: The data model for ICEKit’s current publishing approach is a
tweaked version of the one from django-model-publisher
and SFMOMA.
Each publishable model is assigned four main extra columns:
publishing_linked
: a 1-to-1 relationship to self, or as near as possible to self, that on the draft copy of a publishable item will point to its published copy, if any.publishing_is_draft
: boolean field,True
if the current item is a draft copy (the default) orFalse
if it is the published copy.publishing_modified_at
: timestamp used mainly to track when publishable items are updated so that you can work out whether the published copy is up-to-date compared to the draft copy version. That is, any up-to-date published copy should have apublishing_modified_at
: timestamp value equal to or later than the corresponding draft item.publishing_published_at
: used to set a future time when the item is to be considered published, for scheduling publication. I don’t think we use or implement this at all…
Handling unique fields¶
Because the publishing approach creates draft and published copies of
models, any fields marked as unique=True
will raise IntegrityErrors
unless the field is made non-unique.