Django поставляется с опциональным фреймворком для поддержки нескольких сайтов. Это позволяет держать некоторые объекты и функциональность в одном месте в то же время разделяя по сайтам, используя разные доменные имена и названия Django-сайтов.
Если в рамках одной установки Django требуется разрабатывать более чем один сайт с отличающейся функциональностью, то как раз для этого случая и был разработан фреймворк для сайтов.
Поддержка сайтов базируется в основном на простой модели:
Модель для хранения атрибутов domain и name сайта.
Доменное имя, ассоциированное с данным сайтом. Например www.example.com.
Поле domain теперь содержит unique.
Название сайта.
Настройка SITE_ID указывает ID объекта Site в базе данных, который связан с текущими настройками и установленным проектом. Если эта настройка не указана, функция get_current_site() попытается получить текущий сайт, сравнивая domain с именем хоста, которое возвращает метод request.get_host().
Как использовать данную модель решать вам, но 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("Article does not exist on this site")
# ...
Кроме того вы можете связать свои модели с 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, on_delete=models.CASCADE)
Она имеет преимущества, описанные выше.
Вы можете использовать фреймворк для построения сайтов в представлениях для выполнения конкретных вещей, необходимых лишь для текущего, например:
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 имеют разные пути к шаблонам (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 везде, где только можно, для удаления дублирования и упрощения кода.
Функция 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'
>>> 'https://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'https://example.com/mymodel/objects/3/'
Для того, чтобы воспользоваться описанными выше возможностями, необходимо:
Добавить 'django.contrib.sites' в INSTALLED_APPS.
Задать SITE_ID:
SITE_ID = 1
Запустить migrate.
django.contrib.sites регистрирует обработчик сигнала post_migrate, который создаёт новый сайт с именем example.com и доменом example.com. Эта запись также будет создана после инициализации тестовой БД. Для установки правильного имени и домена для проекта можно воспользоваться data migration.
Чтобы использовать поддержку сайтов на боевом сервере необходимо для каждого SITE_ID создать свой файл настроек (возможно, с импортом общих, чтобы избежать дублирования) и затем указать соответствующий DJANGO_SETTINGS_MODULE.
Так как текущий сайт хранится в базе данных, то каждый вызов 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()
Если вы используете Site в качестве внешнего ключа в какой-либо модели, то вам пригодится класс CurrentSiteManager. Это модель manager, которая автоматически фильтрует запросы на принадлежность к текущему Site.
Обязательная настройка SITE_ID
CurrentSiteManager можно использовать, только если указана настройка SITE_ID.
Используйте 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, on_delete=models.CASCADE)
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, on_delete=models.CASCADE)
objects = models.Manager()
on_site = CurrentSiteManager('publish_on')
Если вы передадите в CurrentSiteManager несуществующее имя, то возникнет исключение ValueError.
Напомним, что модель может содержать обычный (не специфичный для сайта) Manager вместе с CurrentSiteManager. Это описано в manager documentation. Если вы зададите менеджер вручную, то Django не будет создавать автоматически objects = models.Manager(). Помимо всего прочего не забывайте, что некоторые части Django (например, админка и обобщённые представления) используют тот менеджер, который задан первым, так что если вы хотите иметь доступ ко всем объектам (а не только специфичным для сайта), определите в модели objects = models.Manager() перед CurrentSiteManager.
Если вы часто используете подобный шаблон:
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 использует эту информацию в нескольких местах. Даже если вы создаёте единственный сайт, потратьте пару секунд, чтобы задать domain и name в базе данных и константу SITE_ID в настройках.
Где внутри Django используются сайты:
В модуле redirects framework каждый объект перенаправления привязан к конкретному сайту. Django ищет его, учитывая текущий сайт.
В модуле 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 для перехода.
Некоторые приложения из django.contrib могут воспользоваться информацией о сайтах, но спроектированы с учётом того, что её может и не быть. (Некоторые люди не хотят или не могут установить дополнительную таблицу.) В этом случае создаётся заглушка RequestSite.
Класс предоставляет такой же интерфейс как и Site (включая атрибуты domain и name), но берёт их из объекта HttpRequest, а не из базы данных.
Задаёт name и domain для метода get_host().
Объект RequestSite имеет схожий с Site интерфейс за исключением метода __init__(), который принимает HttpRequest. Это позволяет вычислить domain и name на основании домена из запроса. Он имеет также методы save() и delete(), вызов которых приведёт к исключению NotImplementedError.
Для обеспечения обратной совместимости Django предоставляет функцию django.contrib.sites.shortcuts.get_current_site.
Эта функция проверяет, что django.contrib.sites установлен и возвращает текущий объект Site или RequestSite, который основан на запросе. При определении текущего сайта используется request.get_host(), если настройка SITE_ID не определена.
Метод request.get_host() может вернуть домен и порт, если заголовок Host содержит явно указанный порт, например example.com:80. В этих случаях, если не найден ни один сайт в базе данных, порт будет обрезан и выполнится поиск только по домену. Это не относится к RequestSite, который всегда использует неизменное значение хоста.
Была добавлена возможность определить текущий сайт по request.get_host().
Был добавлен функционал поиска сайта после обрезания порта.
Mar 31, 2016