Spec-Zone.ru › Django 6.0

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

    # ...

В этом случае для каталогов шаблонов LJWorld.com и Lawrence.com нужно будет создать файлы шаблонов subject.txt и message.txt. Это даёт больше гибкости, но также усложняет решение.

Рекомендуется как можно шире использовать объекты Site, чтобы избежать ненужной сложности и избыточности.

Получение текущего домена для полных URL-адресов

Соглашение get_absolute_url() в Django удобно для получения URL-адреса объекта без доменного имени, но в некоторых случаях может потребоваться отобразить полный URL-адрес объекта — с https://, доменом и всем остальным. Для этого можно использовать фреймворк 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 используется поле с именем, отличным от site или sites, необходимо явно передать пользовательское имя поля параметром в 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.

Наконец, обратите внимание: даже при использовании CurrentSiteManager, скорее всего, вам понадобится обычный (не зависящий от сайта) Manager в модели. Как объясняется в документации по менеджерам, если определить менеджер вручную, Django не создаст автоматически менеджер objects = models.Manager(). Также обратите внимание, что некоторые части Django, в частности административный сайт и обобщённые представления Django, используют менеджер, определённый в модели первым. Поэтому, если административный сайт должен иметь доступ ко всем объектам, а не только к объектам, относящимся к конкретному сайту, укажите в модели objects = models.Manager() перед определением CurrentSiteManager.

Промежуточное ПО Site

Если вы часто используете такой шаблон:

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

Хотя использовать фреймворк sites необязательно, настоятельно рекомендуется это делать, поскольку Django применяет его в нескольких местах. Даже если ваша установка Django обслуживает только один сайт, потратьте пару секунд, чтобы создать объект сайта с правильными domain и name, а затем укажите его идентификатор в настройке SITE_ID.

Вот как Django использует фреймворк sites:

  • В 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 используют фреймворк sites, но устроены так, что его установка в базе данных не является обязательной. (Некоторые пользователи не хотят или просто не могут установить дополнительную таблицу базы данных, необходимую фреймворку sites.) Для таких случаев фреймворк предоставляет класс django.contrib.sites.requests.RequestSite, который можно использовать как запасной вариант, если фреймворк sites, работающий с базой данных, недоступен.

class requests.RequestSite

Класс, предоставляющий основной интерфейс Site (то есть имеющий атрибуты domain и name), но получающий данные из объекта Django HttpRequest, а не из базы данных.

__init__(request)

Задаёт атрибутам name и domain значение, полученное из get_host().

Объект RequestSite имеет интерфейс, похожий на интерфейс обычного объекта Site, но его метод __init__() принимает объект HttpRequest. Он определяет domain и name, анализируя домен запроса. Чтобы соответствовать интерфейсу Site, у него есть методы save() и delete(), но эти методы вызывают исключение NotImplementedError.

Ярлык get_current_site

Наконец, чтобы избежать повторения кода для резервного варианта, фреймворк предоставляет функцию django.contrib.sites.shortcuts.get_current_site().

shortcuts.get_current_site(request)

Функция проверяет, установлено ли django.contrib.sites, и возвращает либо текущий объект Site, либо объект RequestSite на основе запроса. Если настройка SITE_ID не задана, текущий сайт определяется на основе request.get_host().

Если в заголовке Host явно указан порт, например example.com:80, метод request.get_host() может вернуть и домен, и порт. В таких случаях, если поиск не удаётся, поскольку хост не совпадает ни с одной записью в базе данных, порт отбрасывается и поиск повторяется только по доменной части. Это не относится к RequestSite, который всегда использует хост без изменений.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/ref/contrib/sites/

Spec-Zone.ru

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