Структура «сайтов»

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://, доменом и всем остальным - для объекта. Для этого вы можете использовать фреймворк сайтов. Пример:

>>> 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/'

Включение рамок сайтов

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

  1. Добавьте 'django.contrib.sites' к настройке INSTALLED_APPS.

  2. Определите настройку SITE_ID:

    SITE_ID = 1
    
  3. Выполнить 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), но получает свои данные из объекта 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, который всегда будет использовать немодифицированный хост.

Вернуться на верх