Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django models.
- You make your model extend
publish.models.Publishable
- Each model instance then has some extra state information and a reference to it's "public" version, as well as extra methods to control "publishing" drafts
- You register your model with the admin and a
ModelAdmin
class that extendspublish.admin.PublishableAdmin
- The admin then only shows you "draft" (and "deleted") instances
- You work on the draft instances, then when you are happy "publish" the draft values to the public instances
Let's say you have an app with a simple models.py
that looks this this:
from django.db import models class MyModel(models.Model): title = models.CharField(max_length=100) class Meta: ordering = ["title"]
and an admin.py
:
from django.contrib import admin from models import MyModel class MyModelAdmin(admin.ModelAdmin): pass admin.site.register(MyModel, MyModelAdmin)
Then to make this model support publishing you would change the models.py
thus:
from django.db import models from publish.models import Publishable class MyModel(Publishable): # extends from Publishable instead of models.Model title = models.CharField(max_length=100) class Meta(Publish.Meta): # note you should extend from Publish.Meta ordering = ["title"]
That will add some extra fields to your model (so you may need to update your db). At this point though the admin will show both the draft and published objects. We obviously do not want that, as we want the user to edit the draft objects and then "publish" them once they are happy. So we have to alter the admin.py
file too:
from django.contrib import admin from publish.admin import PublishableAdmin from models import MyModel class MyModelAdmin(PublishableAdmin): # just extend from PublishableAdmin instead pass admin.site.register(MyModel, MyModelAdmin)
At this point the admin will start showing an action to "Publish selected MyModels" as well as details of an objects "Publication status". Publishing will show a confirmation page - much like when deleting - confirming what is about to be published (possibly including related objects).
You will then need to modify your views to handle showing only the published or draft objects. You'll probably want some way to view both versions on your site somehow. The Publishable
model has a custom manager with some extra methods for this purpose, but you can also use a Q
object on the Publishable class too:
# these two will return only the "draft" objects MyModel.objects.draft() MyModel.objects.filter(Publishable.Q_DRAFT) # these will give you the "published" objects MyModel.objects.published() MyModel.objects.filter(Publishable.Q_PUBLISHED)
The latter form is handy, as the Q
object can be passed in as a paramter to a view function - allowing for easy re-use of the same view function for both previewing draft objects and viewing live objects.
In addition to modifying your views, you may want to consider changing any get_absolute_url
functions to correctly return the relevant URL for viewing the object - taking into account whether it is a published or draft object (using the is_public
field). The PublishableAdmin
class automatically provides a link to the published (View on site) and draft (Preview on site) versions if a model has implemented get_absolute_url
.
There are two signals that can be listened to during the publish process:
publish.signals.pre_publish
publish.signals.post_publish
The handlers for these signals should have the form
def post_publish_handler(sender, instance, deleted, **kw):
Where instance
will be the object being published - much as with the built-in Django signals pre_save and post_save. Note though that publishing an object may trigger multiple pre and post publish signals, depending on what other objects also need publishing. However that you should not receive the same signal for the same object - only for different objects.
The signals are triggered both for publishing changes and publishing deletions. When a change is published you will receive the draft object as the instance and deleted
will be False
. When a deletion is published you will receive the public instance (as that is what is being deleted) and deleted
will be set to True
.
As with the post_delete signal in Django you will need to take care when using the instance if deleted
is True
, as the object will no longer exist in the database.
You can further control the publication process by providing a PublishMeta
class on your model
from publish.models import Publishable from django.db import models class Page(Publishable): title = models.CharField(max_length=100) slug = models.SlugField(max_length=100) body = models.TextField() notes = models.TextField(blank=True) class PublishMeta(Publishable.PublishMeta): publish_exclude_fields = ['notes']
In the above class the "notes" field will be excluded from publication - it will not be copied to the public copy.
There are two other fields that can be specified:
publish_reverse_fields
- list of reverse/child relationships to publishpublish_functions
- dictionary of 'fieldname' : publish_function (same format as setattr)
Publish functions are useful if you need to run some additional action when publishing an object. For example you may want copy a file to a public location or subtly modify a value as it gets copied. A publish function is expected to work the same as the built-in setattr
, but may (and probably will) have other side-effects.
- A ManyToManyField specified using a "through" model will be treated as a regular reverse relationship, but will automatically be published (no need to specify it via
PublishableMeta.publish_reverse_fields
)
To run the tests for this app use the script:
tests/run_tests.sh