Creating a dynamic multisite using Django

Posted on: August 13, 2011Author: Rob

Sites framework

The sites framework is a great little module which allows you to link content to one or more sites. It is included in django.contrib and enabled by default. Sites are defined in the admin panel and each site receives a unique ID. You can define which site should be referenced for all your content by setting the SITE_ID variable in your projects settings.py. Django sets this to 1 by default. You are free to set it to whatever number you like though, as long as you create an entry for the id in the admin panel.

Models

Let’s update our Polls model to support multiple sites. First of all, we need to make our models aware of the fact we will be running multiple sites. Fortunately, this is fairly easy. Simply adjust your models to include a foreign key reference to the Sites framework. Let’s open up our Poll/models.py file, which should look something like this:

from django.db import models
import datetime

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    def __unicode__(self):
        return self.question
    def was_published_today(self):
        return self.pub_date.date() == datetime.date.today()

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()
    def __unicode__(self):
        return self.choice

In order to make the models aware of the framework, add the following import at the beginning of the file:

from django.contrib.sites.models import Site

This will enable us to reference the Site model, which we will use to distinquish between our various sites. The next step is to actually make the references where we need them. In this case, we want to make site specific polls, so we will simply add a foreign key to the Poll model, referencing the Site model, like this:

class Poll(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
site = models.ForeignKey(Site)
    # ...

Wait a second, only in the Poll model?? Why not in the Choice model as well? Technically you could add a reference in the Choice model as well (feel free to do so). However, since Choice references Poll and Poll references Site, adding yet another reference seems a bit redundant.

Admin

Now, whenever we want to add a Poll for a certain site, we simply select the site we want to add it for in the admin panel and presto, it is done. Easy wasn’t it? Oh but wait… We can’t select our sites yet, why is that? Well, during the Django tutorial, we rearranged our Poll edit form a bit and manually defined our fieldsets. Let’s fix that first, open up polls/admin.py and change the fieldsets in PollAdmin to the following:

    fieldsets = [
        (None, {'fields': ['question', 'site']}),
        ('Date information', {'fields': ['pub_date'], 'classes':['collapse']}),
    ]

Phew, that solved it, we can now safely select the site we want to add the poll to. But we still only see one site… Let’s add some more choices. Go to the sites area (admin->sites) and add a couple of new sites.

For this tutorial I created the following three sites: example.com, example1.com and example2.com. I have also created a poll for each site so we can play around some more.

Managers

Let’s open up our public site at /polls to see what has happened.

All Polls

We still see all the polls we have defined, regardless of the site we defined them for. Our views aren’t aware of our sites yet. They still simply request all the polls we have available like this:

urlpatterns = patterns('',
    (r'^$',
        ListView.as_view(
            queryset=Poll.objects.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='polls/index.html')),
    # ...
)

Do you see how it references Poll.objects? Objects is a so called model manager. A lot can be said about managers, but basically they serve as the bridge to our database and allow us to easily request our objects. By default Django attaches a models.Manager() to every object automagically, which you can access using .objects. The models.Manager doesn’t know anything about sites, though, so it will happily fetch whatever content we request from the database, no questions asked. How do we solve this?

One way to solve this would be to add an extra site argument to all our queries to Poll.objects, but this would lead to a lot of duplicate code, which is of course totally against the DRY programming principle. Fortunately, Django already solved this for us by providing an alternative CurrentSiteManager which does know all about sites and filters all content we request based on the site we are currently at. Sweet huh? All we need to do is go back to our polls/models.py and update our Poll class a bit by adding the manager. First we import the manager:

from django.contrib.sites.managers import CurrentSiteManager

Next we add the manager to the Poll class. Note how we also add model.Manager() to the class. This is, because Django won’t automatically add the model.Manager to a model if we provide our own. Adding objects ourselves allows us to use this model as usual in our admin panel while using the CurrentSiteManager in our public views.

   class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

Now that the CurrentSiteManager has been added, we can use it in our views and get the desired result:

urlpatterns = patterns('',
    (r'^$',
        ListView.as_view(
            queryset=Poll.on_site.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='polls/index.html')),
    # ...
)

Your polls page should look something like this by now:

Filtered Poll

The code

Let’s wrap up the changes we made so far up and see what our files look like:

polls/models.py:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
import datetime

class Poll(models.Model):
    question = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()
    def __unicode__(self):
        return self.question
    def was_published_today(self):
        return self.pub_date.date() == datetime.date.today()

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    choice = models.CharField(max_length=200)
    votes = models.IntegerField()
    def __unicode__(self):
        return self.choice

polls/admin.py

from polls.models import Poll, Choice
from django.contrib import admin

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3

class PollAdmin(admin.ModelAdmin):
    list_display = ('question', 'pub_date', 'was_published_today')
    list_filter = ['pub_date']
    search_fields = ['question']
    date_hierarchy = 'pub_date'
    fieldsets = [
        (None, {'fields': ['question', 'site']}),
        ('Date information', {'fields': ['pub_date'], 'classes':['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Poll, PollAdmin)

polls/views.py:

from django.conf.urls.defaults import *
from django.views.generic import DetailView, ListView
from polls.models import Poll

urlpatterns = patterns('',
    (r'^$',
        ListView.as_view(
            queryset=Poll.on_site.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='polls/index.html')),
    (r'^(?P\d+)/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/detail.html')),
    url(r'^(?P\d+)/results/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/results.html'),
        name='poll_results'),
    (r'^(?P\d+)/vote/$', 'polls.views.vote'),
)

And that’s it! Your Django project has now become multi site aware. But wait a minute, we can still request a single Poll from another site, how do we solve that? I’ll leave that to you. I do recommend looking into the Detailview: Viewing subsets of objects section of the Django manual, though, and I’m sure you’ll figure it out from there. Just make sure to always check if the object you are requesting or editing belongs to the same site you are currently on.

Continue to the middleware.

Pages: 1 2 3

  • http://dyve.net Dylan Verheul

    Very nice tutorial. Do you think it is safe to overwrite settings.SITE_ID? What about multithreading servers?

    • Rob

      I think Django’s thread safety should prevent any serious issues, although I must admit I haven’t tested that yet. It is a valid concern though, I’ll see if I can think of an easy way to test this :).

      • RobM

        Hi Rob,

        Did you ever figure out a way to test if indeed Django prevents serious problems? Seems there hasn’t been a follow up on this for a while.

    • Rob

      It seems like this solution indeed faces some threading issues. However, the general direction is valid, we just need some extra measures. Another example implementation can be found here:

      https://bitbucket.org/wkornewald/djangotoolbox/src/535feb981c50/djangotoolbox/sites/dynamicsite.py

  • PhilipR

    Hi Rob

    This example was exactly what I was looking for, and it’s well written. Did you ever test the thread safety issue?

    • http://www.two-legs.com/ Rob Been

      Hi Philip,

      Because we are directly updating the settings in the middleware this solution unfortunatly does have some threading issues.

      However, I’ve found another example implementation which uses adds another middleware to avoid the threading issues:

      https://github.com/shestera/django-multisite

      Hope this helps :D