Spec-Zone.ru › Django 3.2

Фреймворк «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 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/'

Включение фреймворка sites

Чтобы включить фреймворк sites, выполните следующие действия:

  1. Добавьте 'django.contrib.sites' в настройку INSTALLED_APPS.
  2. Определите настройку SITE_ID:

    SITE_ID = 1
    
  3. Запустите 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 framework, django.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), но получает данные из объекта Django HttpRequest, а не из базы данных.

__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/3.2/ref/contrib/sites/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API