Фреймворк для сайтов¶
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 предоставляет несколько способов взаимодействия через соглашения.
Пример использования¶
Для наглядности продемонстрируем сей механизм на нескольких примерах.
Связь контента с несколькими сайтами¶
The LJWorld.com and Lawrence.com sites are operated by the same news organization – the Lawrence Journal-World newspaper in Lawrence, Kansas. LJWorld.com focused on news, while Lawrence.com focused on local entertainment. But sometimes editors wanted to publish an article on both sites.
Решение в лоб - заставлять контент-менеджеров публиковать статьи дважды: и в 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’s get_absolute_url() convention is nice for getting your objects“
URL without the domain name, but in some cases you might want to display the
full URL – with http:// and the domain and everything – for an object.
To do this, you can use the sites framework. An example:
>>> 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. Это позволяет вычислить 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, который основан на запросе. При определении текущего сайта используетсяrequest.get_host(), если настройкаSITE_IDне определена.Метод
request.get_host()может вернуть домен и порт, если заголовокHostсодержит явно указанный порт, напримерexample.com:80. В этих случаях, если не найден ни один сайт в базе данных, порт будет обрезан и выполнится поиск только по домену. Это не относится кRequestSite, который всегда использует неизменное значение хоста.