Фреймворк для сайтов

Django поставляется с опциональным фреймворком для построения сайтов. Это позволяет держать некоторые объекты и функциональность в одном месте в то же время разделяя по сайтам, используя доменные имена либо “подробные” имена Django-сайтов.

Если в рамках одной установки Django требуется разрабатывать более чем один сайт с отличающейся функциональностью, то как раз для этого случая и был разработан фреймворк для сайтов.

Поддержка сайтов базируется в основном на простой модели:

class models.Site

Модель для хранения атрибутов сайта domain и name. Настройка SITE_ID задаёт ID класса Site (атрибут id добавляется автоматически), который ассоциирован с данным файлом настроек.

domain

Доменное имя, ассоциированное с данным сайтом

name

Человекопонятное “подробное” имя сайта.

Как использовать данную модель решать вам, но Django предоставляет несколько способов взаимодействия через соглашения.

Пример использования

Для наглядности продемонстрируем сей механизм на нескольких примерах.

Связь контента с несколькими сайтами

Сайты газеты Lawrence Journal-World LJWorld.com и Lawrence.com написаны на Django. LJWorld.com ориентирован на освещение глобальных событий, в то время как Lawrence.com фокусируется на местных. Время от времени возникает необходимость публиковать статьи на обе площадки.

Решение в лоб - заставлять контент-менеджеров публиковать статьи дважды: и в LJWorld.com, и в Lawrence.com. Это неудобно не только для людей, но и для железа - придётся хранить в БД 2 одинаковых записи.

Если чуть-чуть подумать то можно реализовать более гибкое и в то же время простое решение: оба сайта используют одну и ту же базу статей, каждая из кторых связана с одним или более сайтом. В Django это реализуется через ManyToManyField в модели Article:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

Это решение достаточно красиво:

  • Позволяет редактировать контент двух сайтов в одном интерфейсе (админке Django).

  • Позволяет избежать избыточности в плане хранения записей в БД.

  • Позволяет разработчикам сайтов использовать один и тот же код для двух сайтов. Код представления лишь проверяет надо ли выводить данную статью на текущем сайте. Например, как-нибудь так:

    from django.contrib.sites.shortcuts import get_current_site
    
    def article_detail(request, article_id):
        try:
            a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id)
        except Article.DoesNotExist:
            raise Http404
        # ...
    

Связь контента с одним сайтом

Кроме того вы можете связать свои модели с Site через отношение один-ко-многим, используя ForeignKey.

Например, если статья должна быть доступна только на одном сайте, то вы можете использовать следующую модель:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site)

Она имеет преимущества, описанные выше.

Получение значения текущего сайта в представлении

Вы можете использовать фреймворк для построения сайтов в представлениях для выполнения конкретных вещей, необходимых лишь для текущего, например:

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
        pass
    else:
        # Do something else.
        pass

Конечно, это ужачный пример завязки на ID сайта который подходит только для мелких исправлений, которые надо выполнить быстро. Гораздо лучше проверять доменное имя:

from django.contrib.sites.shortcuts import get_current_site

def my_view(request):
    current_site = get_current_site(request)
    if current_site.domain == 'foo.com':
        # Do something
        pass
    else:
        # Do something else.
        pass

Преимущество в том, что даже если описываемая функциональность Django и не задействована, всё равно вернётся экземпляр RequestSite.

Если у вас нет доступа к объекту запроса, можно получить текущий сайт через метод get_current() класса Site. В этом случае надо быть уверенным, что задана константа SITE_ID. Этот пример эквивалентен предыдущему:

from django.contrib.sites.models import Site

def my_function_without_request():
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Do something
        pass
    else:
        # Do something else.
        pass

Получение текущего домена для отображения

LJWorld.com и Lawrence.com имеют функциональность по рассылке уведомлений, которая позволяет читателям получать уведомления. Самый простой пример: читатель заполняет форму, и ему тут же приходит письмо “Спасибо, что подписались.”

Было бы избыточным реализовывать этот механизм дважды, так что в реальности выполняется один и тот же код. Однако, сообщение должно быть разным для сайтов. Используя объект Site мы можем подставить соответствующие name и domain.

Покажем это на примере:

from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...

    current_site = get_current_site(request)
    send_mail('Thanks for subscribing to %s alerts' % current_site.name,
        'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
        'editor@%s' % current_site.domain,
        [user.email])

    # ...

Для Lawrence.com письмо будет содержать строку “Thanks for subscribing to lawrence.com alerts.”, для LJWorld.com - “Thanks for subscribing to LJWorld.com alerts.”.

Заметим, что более гибкой (но и более тяжёлой) была бы реализация через шаблонизатор Django. Предполагая, что Lawrence.com и LJWorld.com имеют разные пути к шаблонам (TEMPLATE_DIRS), вышло бы что-то типа:

from django.core.mail import send_mail
from django.template import loader, Context

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...

    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    send_mail(subject, message, 'editor@ljworld.com', [user.email])

    # ...

В этом случае нужно было бы создавать шаблоны subject.txt и message.txt для каждого сайта. Такое решение более гибкое, но и более сложное.

Хорошей идеей будет использовать Site везде, где только можно, для удаления дублирования и упрощения кода.

Получение текущего домена для полного URL

Функция get_absolute_url() полезна для получения URL без протокола и имени домена. Если же нужен полный URL, то его можно сгенерировать следующим образом:

>>> from django.contrib.sites.models import Site
>>> obj = MyModel.objects.get(id=3)
>>> obj.get_absolute_url()
'/mymodel/objects/3/'
>>> Site.objects.get_current().domain
'example.com'
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'

Включение поддержки фреймворка для сайтов

Изменено в Django 1.6:

В предыдущих версиях Django он был включён по умолчанию.

Для того, чтобы воспользоваться описанными выше возможностями, необходимо:

  1. Добавить 'django.contrib.sites' в INSTALLED_APPS.

  2. Задать SITE_ID:

    SITE_ID = 1
    
  3. Запустить migrate.

django.contrib.sites регистрирует обработчик сигнала post_migrate, который создаёт новый сайт с именем example.com и доменом example.com. Эта запись также будет создана после инициализации тестовой БД. Для установки правильного имени и домена для проекта можно воспользоваться data migration.

Чтобы использовать поддержку сайтов на боевом сервере необходимо для каждого SITE_ID создать свой файл настроек (возможно, с импортом общих, чтобы избежать дублирования) и затем указать соответствующий DJANGO_SETTINGS_MODULE.

Кеширование текущего объекта Site

Так как текущий сайт хранится в базе данных, то каждый вызов Site.objects.get_current() приведёт к выполнению SQL запроса. Разработчики Django позаботились об оптимизации: после первого запроса значение кешируется и в дальнейшем возвращается именно оно без обращения к БД.

Если же вам нужно всё-таки выполнять запрос каждый раз, можно очистить кеш путём вызова Site.objects.clear_cache():

# First call; current site fetched from database.
current_site = Site.objects.get_current()
# ...

# Second call; current site fetched from cache.
current_site = Site.objects.get_current()
# ...

# Force a database query for the third call.
Site.objects.clear_cache()
current_site = Site.objects.get_current()

CurrentSiteManager

class managers.CurrentSiteManager

Если вы используете Site в качестве внешнего ключа в какой-либо модели, то вам пригодится класс CurrentSiteManager. Это модель manager, которая автоматически фильтрует запросы на принадлежность к текущему Site.

Используйте CurrentSiteManager для добавления этой функциональности непосредственно в модель:

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

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

Таким образом, Photo.objects.all() вернёт все объекты Photo, а Photo.on_site.all() только те, которые доступны на данном сайте согласно SITE_ID.

Другими словами эти 2 выражения эквивалентны:

Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()

Каким образом CurrentSiteManager узнаёт какое поле относится к Site? По умолчанию, CurrentSiteManager смотрит на наличие ForeignKey с именем site или ManyToManyField с именем sites. Если вы используете другое название поля, то ищется ссылка на Site. В этом случае имя поля необходимо передать в CurrentSiteManager. В нашем случае поле названо publish_on:

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

class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(max_length=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

Если вы передадите в CurrentSiteManager несуществующее имя, то возникнет исключение ValueError.

Напомним, что модель может содержать обычный (не специфичный для сайта) Manager вместе с CurrentSiteManager. Это описано в manager documentation. Если вы зададите менеджер вручную, то Django не будет создавать автоматически objects = models.Manager(). Помимо всего прочего не забывайте, что некоторые части Django (например, админка и обобщённые представления) используют тот менеджер, который задан первым, так что если вы хотите иметь доступ ко всем объектам (а не только специфичным для сайта), определите в модели objects = models.Manager() перед CurrentSiteManager.

Middleware для сайтов

Добавлено в Django 1.7.

Если вы часто используете подобный шаблон:

from django.contrib.sites.models import Site

def my_view(request):
    site = Site.objects.get_current()
    ...

то есть простой способ избежать дублирования кода. Добавьте django.contrib.sites.middleware.CurrentSiteMiddleware в MIDDLEWARE_CLASSES. Таким образом для каждого объекта запроса добавится атрибут site (request.site), который указывает на текущий сайт.

Как Django работает с сайтами

Хотя задавать сайты вовсе не обязательно, в то же время всё-таки рекомендуется, т.к. Django использует эту информацию в нескольких местах. Даже если вы создаёте единственный сайт, потратьте пару секунд, чтобы задать domain и name в базе данных и константу SITE_ID в настройках.

Где внутри Django используются сайты:

  • В модуле redirects framework каждый объект перенаправления привязан к конкретному сайту. Django ищет его, учитывая текущий сайт.

  • В плане комментирования каждый комментарий привязан к сайту. Когда он сохраняется, устанавливается ссылка на текущий Site, а в дальнейшем показываются комментарии только для текущего сайта.

  • В модуле flatpages framework каждая статичная страница привязана к определённому сайту. При обращении к ней создаётся Site, который проверятеся на соответствие запрашиваемому сайту в FlatpageFallbackMiddleware.

  • В модуле syndication framework шаблон для title и description автоматически получает доступ к переменной {{ site }} типа Site. Также поддержка URL использует domain из текущего объекта Site, если не указан полный путь.

  • В модуле authentication framework функция django.contrib.auth.views.login() передаёт имя текущего Site в переменную шаблона {{ site_name }}.

  • Популярные представления (django.contrib.contenttypes.views.shortcut) используют домен текущего объекта Site для создания URL.

  • В админке ссылка “view on site” использует текущий Site для генерации полного URL для перехода.

Объект RequestSite

Некоторые приложения из django.contrib могут воспользоваться информацией о сайтах, но спроектированы с учётом того, что её может и не быть. (Некоторые люди не хотят или не могут установить дополнительную таблицу.) В этом случае создаётся заглушка RequestSite.

class requests.RequestSite

Класс предоставляет такой же интерфейс как и Site (включая атрибуты domain и name), но берёт их из объекта HttpRequest, а не из базы данных.

__init__(request)

Задаёт name и domain для метода get_host().

Изменено в Django 1.7:

Этот класс определён в django.contrib.sites.models.

Объект RequestSite имеет схожий с Site интерфейс за исключением метода __init__(), который принимает HttpRequest. Это позволяет вычислить domain и name на основании домена из запроса. Он имеет также методы save() и delete(), вызов которых приведёт к исключению NotImplementedError.

сокращение get_current_site

Для обеспечения обратной совместимости Django предоставляет функцию django.contrib.sites.shortcuts.get_current_site.

shortcuts.get_current_site(request)

Эта функция проверяет, что django.contrib.sites установлен и возвращает текущий объект Site или RequestSite, который основан на запросе.

Изменено в Django 1.7:

Эта функция определена в django.contrib.sites.models.