Фреймворк «sites»
Django поставляется с необязательным фреймворком «sites». Это механизм для связывания объектов и функциональности с конкретными веб-сайтами, а также хранилище доменных имён и «полных» названий ваших сайтов, работающих на Django.
Используйте его, если ваша установка Django обслуживает более одного сайта и вам нужно как-то различать эти сайты.
Фреймворк «sites» в основном основан на этой модели:
-
class models.Site -
Модель для хранения
domainиnameатрибутов веб-сайта.-
domain -
Полное доменное имя, связанное с веб-сайтом. Например,
www.example.com.
-
name -
«Полное» имя веб-сайта, понятное для человека.
-
Настройка SITE_ID указывает идентификатор базы данных объекта Site, связанного с данным файлом настроек. Если настройка опущена, функция get_current_site() попытается получить текущий сайт, сравнив domain с именем хоста из метода request.get_host().
Как вы будете его использовать – решать вам, но Django автоматически использует его несколькими способами, следуя некоторым соглашениям.
Примеры использования
Зачем использовать «sites»? Лучше всего это объясняется на примерах.
Связывание контента с несколькими сайтами
Сайты LJWorld.com и Lawrence.com управляются одной новостной организацией – газетой Lawrence Journal-World в Лоуренсе, штат Канзас. LJWorld.com фокусировался на новостях, а Lawrence.com – на местном развлечении. Но иногда редакторы хотели опубликовать статью на обоих сайтах.
Примитивный способ решения проблемы заключался в требовании от создателей сайта публиковать одну и ту же историю дважды: один раз для LJWorld.com и снова для Lawrence.com. Но это неэффективно для создателей сайта и избыточно хранить несколько копий одной и той же истории в базе данных.
Лучшее решение устраняет дублирование контента: оба сайта используют одну и ту же базу данных статей, а статья связана с одним или несколькими сайтами. В терминологии модели 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).
- Это означает, что одна и та же история не должна публиковаться дважды в базе данных; она хранится только в одной записи в базе данных.
-
Это позволяет разработчикам сайта использовать один и тот же код представления 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)
Это даёт те же преимущества, что и в предыдущем разделе.
Включение текущего сайта из представлений
Вы можете использовать фреймворк «sites» в ваших представлениях Django, чтобы выполнять определённые действия в зависимости от сайта, на котором вызывается представление. Например:
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
Это также имеет преимущество проверки, установлен ли фреймворк «sites», и возвращает экземпляр 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 в этом электронном письме тема – «Спасибо за подписку на оповещения lawrence.com». На LJWorld.com тема письма – «Спасибо за подписку на оповещения LJWorld.com». То же самое касается и тела электронного письма.
Обратите внимание, что ещё более гибким (но более ресурсоёмким) способом сделать это было бы использование шаблонов 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 файлы шаблонов как для директорий шаблонов LJWorld.com, так и Lawrence.com. Это даёт больше гибкости, но также и более сложно.
Желательно использовать объекты Site по возможности, чтобы избежать ненужной сложности и избыточности.
Получение текущего домена для полных URL-адресов
Соглашение Django по получению URL-адресов ваших объектов без доменного имени неплохо, но в некоторых случаях вам может понадобиться отобразить полный URL-адрес – с http:// и доменным именем – для объекта. Для этого можно использовать фреймворк «sites». Пример:
>>> 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/'
Включение фреймворка «sites»
Чтобы включить фреймворк «sites», выполните следующие действия:
- Добавьте
'django.contrib.sites'в настройкуINSTALLED_APPS. -
Определите настройку
SITE_ID:SITE_ID = 1
- Запустите
migrate.
django.contrib.sites регистрирует обработчик сигнала post_migrate, который создаёт сайт по умолчанию под названием example.com с доменом example.com. Этот сайт также будет создан после того, как Django создаст тестовую базу данных. Чтобы установить правильное имя и домен для вашего проекта, вы можете использовать миграцию данных.
Для обслуживания различных сайтов в рабочей среде вы создадите отдельный файл настроек для каждого SITE_ID (возможно, импортируя из общего файла настроек, чтобы избежать дублирования общих настроек), а затем укажите соответствующее DJANGO_SETTINGS_MODULE для каждого сайта.
Кэширование объекта текущего Site
Поскольку текущий сайт хранится в базе данных, каждый вызов Site.objects.get_current() может привести к запросу к базе данных. Но Django немного хитрее: при первом запросе текущий сайт кэшируется, а любые последующие вызовы возвращают кэшированные данные вместо обращения к базе данных.
Если по какой-либо причине вам нужно принудительно выполнить запрос к базе данных, вы можете сказать 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 в вашей модели(ях). Это менеджер модели менеджер, который автоматически фильтрует запросы, включая только объекты, связанные с текущим 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() вернёт только Photo объекты, связанные с текущим сайтом, согласно настройке SITE_ID.
Другими словами, эти два оператора эквивалентны:
Photo.objects.filter(site=settings.SITE_ID) Photo.on_site.all()
Как CurrentSiteManager узнал, какой поле Photo было Site? По умолчанию, CurrentSiteManager ищет ForeignKey с именем site или ManyToManyField с именем sites для фильтрации. Если вы используете поле с другим именем, отличным от site или 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 и передадите имя поля, которого не существует, Django выдаст ValueError.
Наконец, обратите внимание, что, вероятно, вам понадобится стандартный (не специфичный для сайта) Manager в вашей модели, даже если вы используете CurrentSiteManager. Как объясняется в документации по менеджерам, если вы вручную определяете менеджер, Django не создаст автоматического менеджера objects = models.Manager() за вас. Также обратите внимание, что некоторые части Django, в частности сайт администрирования 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. Средство миддлевера устанавливает атрибут site на каждом объекте запроса, поэтому вы можете использовать request.site для получения текущего сайта.
Как Django использует фреймворк сайтов
Хотя использование фреймворка сайтов не является обязательным, его крайне рекомендуется, так как Django использует его в нескольких местах. Даже если ваша установка Django обслуживает только один сайт, вы должны потратить две секунды на создание объекта сайта с вашим domain и name, и указать его ID в настройке SITE_ID.
Вот как Django использует фреймворк сайтов:
- В
redirects frameworkкаждый объект перенаправления связан с определённым сайтом. Когда Django ищет перенаправление, он учитывает текущий сайт. - В
flatpages frameworkкаждый плоский документ связан с конкретным сайтом. При создании плоского документа вы указываете егоSite, иFlatpageFallbackMiddlewareпроверяет текущий сайт при получении плоских документов для отображения. - В
syndication frameworkшаблоны дляtitleиdescriptionавтоматически имеют доступ к переменной{{ site }}, которая является объектомSite, представляющим текущий сайт. Также, обработчик для предоставления URL-адресов элементов будет использоватьdomainиз текущего объектаSite, если вы не укажете полный домен. - В
authentication frameworkdjango.contrib.auth.views.LoginViewпередаёт имя текущегоSiteв шаблон как{{ site_name }}. - Краткое представление (
django.contrib.contenttypes.views.shortcut) использует домен текущего объектаSiteпри расчёте URL-адреса объекта. - В фреймворке администрирования ссылка «просмотреть на сайте» использует текущий
Site, чтобы определить домен сайта, на который будет перенаправление.
RequestSite объекты
Некоторые приложения django.contrib используют фреймворк сайтов, но разработаны таким образом, что не требуют установки фреймворка сайтов в вашей базе данных. (Некоторые люди не хотят или просто не могут установить дополнительную таблицу базы данных, которую требует фреймворк сайтов.) Для таких случаев фреймворк предоставляет класс django.contrib.sites.requests.RequestSite, который может быть использован как резервный вариант, когда фреймворк сайтов, поддерживаемый базой данных, недоступен.
-
class requests.RequestSite -
Класс, который разделяет основной интерфейс
Site(то есть, он имеет атрибутыdomainиname), но получает свои данные из объекта DjangoHttpRequest, а не из базы данных.-
__init__(request) -
Устанавливает атрибуты
nameиdomainв значениеget_host().
-
Объект RequestSite имеет интерфейс, аналогичный обычному объекту Site, за исключением того, что его метод __init__() принимает объект HttpRequest. Он может определить domain и name сайта, анализируя домен запроса. Он имеет методы save() и delete() для соответствия интерфейсу Site, но эти методы генерируют исключение NotImplementedError.
get_current_site сокращение
Наконец, чтобы избежать повторяющегося кода обработки исключений, фреймворк предоставляет функцию 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, который всегда использует исходный хост без изменений.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.1/ref/contrib/sites/