Фреймворк для сайтов¶
Django поставляется с опциональным фреймворком для поддержки нескольких сайтов. Это позволяет держать некоторые объекты и функциональность в одном месте в то же время разделяя по сайтам, используя разные доменные имена и названия Django-сайтов.
Если в рамках одной установки Django требуется разрабатывать более чем один сайт с отличающейся функциональностью, то как раз для этого случая и был разработан фреймворк для сайтов.
Поддержка сайтов базируется в основном на простой модели:
- class models.Site¶
Модель для хранения атрибутов
domainиnameсайта.- domain¶
Доменное имя, ассоциированное с данным сайтом. Например
www.example.com.
- name¶
Название сайта.
Настройка SITE_ID указывает ID объекта Site в базе данных, который связан с текущими настройками и установленным проектом. Если эта настройка не указана, функция get_current_site() попытается получить текущий сайт, сравнивая domain с именем хоста, которое возвращает метод request.get_host().
Как использовать данную модель решать вам, но Django предоставляет несколько способов взаимодействия через соглашения.
Пример использования¶
Для наглядности продемонстрируем сей механизм на нескольких примерах.
Связь контента с несколькими сайтами¶
Сайты LJWorld.com и Lawrence.com управлялись одной и той же новостной организацией — газетой Lawrence Journal-World в Лоуренсе, штат Канзас. LJWorld.com сосредоточился на новостях, а Lawrence.com – на местных развлечениях. Но иногда редакторы хотели опубликовать статью на обоих сайтах.
Решение в лоб - заставлять контент-менеджеров публиковать статьи дважды: и в LJWorld.com, и в Lawrence.com. Это неудобно не только для людей, но и для железа - придётся хранить в БД 2 одинаковых записи.
Если чуть-чуть подумать то можно реализовать более гибкое и в то же время простое решение: оба сайта используют одну и ту же базу статей, каждая из кторых связана с одним или более сайтом. В Django это реализуется через ManyToManyField в модели Article:
from django.contrib.sites.models import Site
from django.db import models
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.contrib.sites.models import Site
from django.db import models
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
Жестко запрограммировать такие идентификаторы сайтов на случай, если они изменятся, ненадежно. Более чистый способ добиться того же — проверить домен текущего сайта:
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
def register_for_newsletter(request):
# Check form values, etc., and subscribe the user.
# ...
subject = loader.get_template("alerts/subject.txt").render({})
message = loader.get_template("alerts/message.txt").render({})
send_mail(subject, message, "editor@ljworld.com", [user.email])
# ...
В этом случае нужно было бы создавать шаблоны subject.txt и message.txt для каждого сайта. Такое решение более гибкое, но и более сложное.
Хорошей идеей будет использовать Site везде, где только можно, для удаления дублирования и упрощения кода.
Получение текущего домена для полного URL¶
Соглашение Django get_absolute_url() удобно для получения URL-адреса вашего объекта без имени домена, но в некоторых случаях вам может потребоваться отобразить полный URL-адрес - с https://, доменом и всем остальным - для объекта. Для этого вы можете использовать фреймворк сайтов. Пример:
>>> 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¶
Так как текущий сайт хранится в базе данных, то каждый вызов 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.
Обязательная настройка SITE_ID
CurrentSiteManager можно использовать, только если указана настройка SITE_ID.
Используйте CurrentSiteManager для добавления этой функциональности непосредственно в модель:
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.db import models
class Photo(models.Model):
photo = models.FileField(upload_to="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.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
from django.db import models
class Photo(models.Model):
photo = models.FileField(upload_to="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.
Middleware для сайтов¶
Если вы часто используете подобный шаблон:
from django.contrib.sites.models import Site
def my_view(request):
site = Site.objects.get_current()
...
то есть простой способ избежать дублирования кода. Добавьте django.contrib.sites.middleware.CurrentSiteMiddleware в MIDDLEWARE. Таким образом для каждого объекта запроса добавится атрибут site (request.site), который указывает на текущий сайт.
Как Django работает с сайтами¶
Хотя задавать сайты вовсе не обязательно, в то же время всё-таки рекомендуется, т.к. 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.LoginView()передаёт имя текущего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().
Объект RequestSite имеет интерфейс, аналогичный обычному объекту Site, за исключением того, что его метод __init__() принимает объект HttpRequest. Он способен определить домен и имя, просматривая домен запроса. Он имеет методы save() и delete(), соответствующие интерфейсу Site, но эти методы вызывают NotImplementedError.
сокращение get_current_site¶
Для обеспечения обратной совместимости Django предоставляет функцию django.contrib.sites.shortcuts.get_current_site.
- shortcuts.get_current_site(request)¶
Эта функция проверяет, что
django.contrib.sitesустановлен и возвращает текущий объектSiteилиRequestSite, который основан на запросе. При определении текущего сайта используетсяrequest.get_host(), если настройкаSITE_IDне определена.Метод
request.get_host()может вернуть домен и порт, если заголовокHostсодержит явно указанный порт, напримерexample.com:80. В этих случаях, если не найден ни один сайт в базе данных, порт будет обрезан и выполнится поиск только по домену. Это не относится кRequestSite, который всегда использует неизменное значение хоста.