If your site publishes a large database, the build-and-publish routine can take a long time to run. Sometimes that’s acceptable, but if you’re periodically making small updates to the site it can be frustrating to wait for the entire database to rebuild every time there’s a minor edit.
We tackle this problem by hooking targeted build routines to our Django models. When an object is edited, the model is able to rebuild only those pages that object is connected to. We accomplish this with a BuildableModel
class you can inherit. It works the same as a standard Django model, except that you are asked define a list of the detail views connected to each object.
An abstract base model that creates an object that can builds out its own detail pages.
detail_views
An iterable containing paths to the views that are built using the object, which should inherit from buildable class-based views </buildableviews>
.
build()
Iterates through the views pointed to by detail_views
, running each view's build_object
method with self
. Then calls _build_extra()
and _build_related()
.
unbuild()
Iterates through the views pointed to by detail_views
, running each view's unbuild_object
method with self
. Then calls _unbuild_extra()
and _build_related()
.
_build_extra()
A place to include code that will build extra content related to the object that is not rendered by the detail_views
, such a related image. Empty by default.
_build_related()
A place to include code that will build related content, such as an RSS feed, that does not require passing in the object to a view. Empty by default.
_unbuild_extra()
A place to include code that will remove extra content related to the object that is not rendered by the detail_views
, like deleting a related image. Empty by default.
from django.db import models
from bakery.models import BuildableModel
class MyModel(BuildableModel)
detail_views = ('myapp.views.ExampleDetailView',)
title = models.CharField(max_length=100)
description = models.TextField()
is_published = models.BooleanField(default=False)
def _build_related(self):
from myapp import views
views.MySitemapView().build_queryset()
views.MyRSSFeed().build_queryset()
With a buildable model in place, a update posted to the database by an entrant using the Django admin can set into motion a small build that is then synced with your live site on Amazon S3. We use that system to host applications with in-house Django administration panels that, for the entrant, walk and talk like a live database, but behind the scenes automatically figure out how to serve themselves on the Web as flat files. That’s how a site like graphics.latimes.com is managed.
This is accomplished by handing off the build from the user’s save request in the admin to a job server that does the work in the background. This prevents a push-button save in the admin from having to wait for the entire build to complete before returning a response. Here is the save override that assesses whether the publication status of an object has changed, and then passes off build instructions to a Celery job server.
The key is figuring out what build or unbuild actions to trigger in an override of the Django model's default save method.
example myapp/models.py
from myapp import tasks
from django.db import models
from django.db import transaction
from bakery.models import BuildableModel
class MyModel(BuildableModel)
detail_views = ('myapp.views.ExampleDetailView',)
title = models.CharField(max_length=100)
description = models.TextField()
is_published = models.BooleanField(default=False)
def _build_related(self):
from myapp import views
views.MySitemapView().build_queryset()
views.MyRSSFeed().build_queryset()
@transaction.atomic
def save(self, *args, **kwargs):
"""
A custom save that builds or unbuilds when necessary.
"""
# if obj.save(build=False) has been passed, we skip everything.
if not kwargs.pop('build', True):
super(MyModel, self).save(*args, **kwargs)
# Otherwise, for the standard obj.save(), here we go...
else:
# First figure out if the record is an addition, or an edit of
# a preexisting record.
try:
preexisting = MyModel.objects.get(id=self.id)
except MyModel.DoesNotExist:
preexisting = None
# If this is an addition...
if not preexisting:
# We will publish if that's the boolean
if self.is_published:
action = 'publish'
# Otherwise we will do nothing do nothing
else:
action = None
# If this is an edit...
else:
# If it's being unpublished...
if not self.is_published and preexisting.is_published:
action = 'unpublish'
# If it's being published...
elif self.is_published:
action = 'publish'
# If it's remaining unpublished...
else:
action = None
# Now, no matter what, save it normally
super(MyModel, self).save(*args, **kwargs)
# Finally, depending on the action, fire off a task
if action == 'publish':
tasks.publish.delay(self)
elif action == 'unpublish':
tasks.unpublish.delay(self)
The tasks don’t have to be complicated. Ours are as simple as this.
example myapp/tasks.py
import sys
import logging
from celery.task import task
from django.conf import settings
from django.core import management
logger = logging.getLogger(__name__)
@task()
def publish(obj):
"""
Build all the pages and then sync with S3.
"""
try:
# Here the object is built
obj.build()
# And if the settings allow publication from this environment...
if settings.PUBLISH:
# ... the publish command is called to sync with S3.
management.call_command("publish")
except Exception, exc:
logger.error(
"Task Error: publish",
exc_info=sys.exc_info(),
extra={
'status_code': 500,
'request': None
}
)
@task()
def unpublish(obj):
"""
Unbuild all the pages and then sync with S3.
"""
try:
obj.unbuild()
if settings.PUBLISH:
management.call_command("publish")
except Exception, exc:
logger.error(
"Task Error: unpublish",
exc_info=sys.exc_info(),
extra={
'status_code': 500,
'request': None
}
)