Структура «сайто⻶
Django поставляется с дополнительным фреймворком «sites». Это крючок для привязки объектов и функциональности к определенным сайтам, а также место хранения доменных имен и «словесных» названий ваших сайтов, работающих на Django.
Используйте его, если ваша единая установка Django обслуживает несколько сайтов и вам нужно как-то различать эти сайты.
Рамки сайтов в основном основаны на этой модели:
-
class
models.
Site
¶ Модель для хранения атрибутов сайта
domain
иname
.-
domain
¶ Полное доменное имя, связанное с веб-сайтом. Например,
www.example.com
.
-
name
¶ Человекочитаемое «многословное» название веб-сайта.
-
Параметр SITE_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. Но это неэффективно для производителей сайтов, а хранить несколько копий одной и той же истории в базе данных излишне.
Более эффективное решение устраняет дублирование контента: Оба сайта используют одну и ту же базу данных статей, и статья связана с одним или несколькими сайтами. В терминологии моделей 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
Преимуществом этого способа является также проверка того, установлен ли фреймворк сайтов, и возврат экземпляра 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 get_absolute_url()
хорошо подходит для получения 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/'
Включение рамок сайтов¶
Чтобы включить каркас сайтов, выполните следующие действия:
Добавьте
'django.contrib.sites'
к настройкеINSTALLED_APPS
.Определите настройку
SITE_ID
:SITE_ID = 1
Выполнить
migrate
.
django.contrib.sites
регистрирует обработчик сигнала post_migrate
, который создает сайт по умолчанию с именем example.com
с доменом example.com
. Этот сайт также будет создан после того, как Django создаст тестовую базу данных. Чтобы задать правильное имя и домен для вашего проекта, вы можете использовать data migration.
Чтобы обслуживать разные сайты в продакшене, вы должны создать отдельный файл настроек с каждым 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
в вашей модели (моделях). Это модель 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()
вернет только 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
. Как объяснялось в manager documentation, если вы определите менеджер вручную, то 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 использует фреймворк для сайтов¶
Хотя использование фреймворка sites не является обязательным, оно настоятельно рекомендуется, поскольку Django использует его преимущества в нескольких местах. Даже если ваша установка Django поддерживает только один сайт, вы должны потратить две секунды на создание объекта site с помощью ваших domain
и name
, и указать его ID в настройках 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
для определения домена сайта, на который она будет перенаправлять.
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
, который всегда будет использовать немодифицированный хост.