django

Зачем вам может понадобиться SITE_ID в настройках Django

  • среда, 10 февраля 2021 г. в 00:40:09
https://habr.com/ru/post/530766/
  • Python
  • Django


КДПВ

Если вы не используете все возможности Django, то, очень вероятно, вы не пользуетесь SITE_ID. Этому способствуют как убогая официальная документация Sites framework, так и несогласованное с Sites развитие кода Django.
Предположу, что Sites скоро будет бездумно снесен свежими «разработчиками» Django, как это уже произошло с модулями Comments (Dj 1.6) или Formtools (Dj 1.8). А, пока этого не произошло, предлагаю вам поразмышлять о возможностях Django Sites framework.

Вспомните, в своем первом проекте 1.xxx версий Django, скорее всего, вы даже не обратили внимания на автоматически созданные строчки, в settings.py:

 'django.contrib.sites',  # включаем framework Sites
........
SITE_ID = 1  # задаем значение SITE_ID по умолчанию

В новых версиях Django 3.хх упоминание о SITES_ID вы встретите, только когда захотите включить еще одного кандидата на вылет «flatpages app». Читайте об этом в разделе установка.
На работу самих «Flatpages», Sites особо не влияет, но без миграции из django.contrib.sites не обойтись.

Так зачем вообще может понадобиться настройка SITES_ID?

Если читать документацию по «Sites framework», которая не поменялась c Django 1.4, то вам расскажут, как можно настроить одну панель администратора для управления содержимым нескольких сайтов. Скажу больше, разумное использование Sites позволяет ограничивать доступ к данным на уровне запросов к базе данных, когда права доступа на уровне объектов Python/Django неизвестны, т.к. объектов еще не создано.

Вы можете попробовать сделать это самостоятельно:
Для этого запустите несколько раз django server своего проекта для разных портов с указанием атрибута --settings=every_time_another_settings_name, в каждом новом settings.py укажите другой SITE_ID.

image
И вы увидите, что ничего не изменилось. Пока.

Чтобы ощутить разницу, вам предстоит внести изменения в проект.

  • Добавить поле site=ForeignKey(Site) к любой вашей модели.
  • Унаследовать менеджера данных этой модели от CurrentSiteManager
  • Обновить схему модели и выполнить миграцию

from django.db.models import Model, CharField, ForeignKey
from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site
from django.conf import settings

class MyModelManager(CurrentSiteManager):
    pass

class MyModel(Model):
    title = CharField(max_length=255)
    site = ForeignKey(Site, default=settings.SITE_ID, editable=False, on_delete=CASCADE)
    objects = MyModelManager()  # можно сразу тут передать имя поля. 

Опять запустите несколько раз сервер на разных портах, запустите Админ панель, создайте объекты в администраторе измененной модели.

image

Как видите, администратор модели отображает только объекты для своего SITE_ID

image

Обобщим первый опыт:
Менеджер, унаследованный от CurrentSiteManager дает предварительную автоматическую фильтрацию данных своей модели по определенному признаку: obj.site_id=settings.SITE_ID

Если вы добрались до этого момента, то вы встретите первую недоработку SITES framework.

в CurrentSiteManager жестко зашита проверка наличия ForeignKey(Site) в текущей модели. Похоже, что для использования всех удобств Sites в старых версиях Django мы вынуждены иметь ForeignKey(Site) в каждой модели.
Но если подумать, то при доработке CurrentSiteManager напильником можно все сделать как надо:
class MyModelManager(CurrentSiteManager):
    site_field_name = 'othermymodel__site'

    def __init__(self, *args, **kwargs):
        super(MyModelManager, self).__init__(*args, **kwargs)
        if self.site_field_name:
            self._CurrentSiteManager__is_validated = True  #для старых Django
            self._CurrentSiteManager__field_name = self.site_field_name

    def _check_field_name(self):  #для новых Django
        return []
Mожете сами поискать с какой версии Django этот код останется работосособным, но станет избыточным.
В новых версиях Django код уже пробовали исправить, но так и не доделали. Причина в том, что django.contrib.sites — это уже неуловимый Джо для разработчиков Django.


Что должно получиться в итоге:
Вы вставили в ключевые модели поле ссылки на Site, в менеджерах связанных моделей ссылку на это поле в атрибуте site_field_name.
При заходе по адресу одного сайта вы видите и правите данные только этого сайта, при заходе на другой — видите и правите данные только другого сайта.
Сервер надо запустить несколько раз. Но база и код в единственном экземпляре.

В проектах моей фирмы Менеджеры «сайтовых данных» унаследованы от исправленного CurrentSiteManager с правильно прописанными site_field_name и несколько моделей имеют ссылку на ForeignKey(Site). При этом дополнительных JOIN в запросах удалось избежать.


Немногим позже вы заметите, что знания только site_id мало. Например, в шаблонах вы захотите отображать не цифру, как у меня на примере выше, а красивое имя сайта. Этому может поспособствовать CurrentSiteMiddleware из django.contrib.sites.middleware.
Благодаря ей любой request получит вычисленный атрибут site, хранящий объект из модели Sites. CurrentSiteMiddleware позволит не прописывать SITES_ID в settings, и вычисление атрибута site будет выполняться с учетом текущего запроса, и это очень круто. Только эта функция работает не всегда и не так, как ожидается:

  • Атрибут site вычисляется сразу. Не важно, используете вы его в дальнейшем, или нет. Да, в коде предприняты корявые попытки уменьшить количество обращений к базе, но непонятно, что мешало разработчикам Django использовать lazy-объект.
  • Если вы сильно заморочены на типизации, то заметите, что атрибут site может содержать instance двух абсолютно не связанных классов. Причем явно переопределить один из этих классов (RequestSite) вы не можете. Но если подумать, то подмена класса у instance возможна позже.
  • SITES_ID в settings не знает о модели Site и наоборот. Запуск сервера с новым SITES_ID, удаление строки таблицы — все это приведет к появлению Server Error. Я считаю это изначальной архитектурной ошибкой Sites framework
  • Хотя это и разрешено, не указывать SITES_ID в settings, но любой CurrentSiteManager и django.contrib.FlatPages перестанут работать.
  • Вычисленный на лету объект в site никак не влияет на работу «Current»SiteManager. Хотя название класса как бы обязывает.

Последний пункт превращает sites framework в тыкву бессмысленный атавизм из ветки Django 1.xxx. Но если подумать… то этих «если подумать» и так уже слишком много.

Разумеется, у вас уже мог появиться вопрос: да кому вообще нужен этот SITES_ID, это старье уже никто не использует! И я с этим не соглашусь.

Я знаю несколько современных проектов, использующих подобную структуру.
Пример из недавнего — это проект SHUUP, c доработкой Multivendor Marketplace. Его мы совсем недавно портировали на Python-3.9.1/Django-3.1.6

Ребята сделали copy-paste-find-replace в django.contrib.sites, у них вместо модели Site модель Shop, вместо SITES_ID стоит DEFAULT_SHOP, и вместо CurrentSiteMiddleware — ShuupMiddleware, в которой они к request крепят не site а shop.
Как по мне, так все же лучше использовать существующий код, чем повторять его еще раз. Но заново изобретенный разработчиками SHUUP «proxy model» подсказывает, что:
А в 2019 мы изобрели велосипед!!!


На этой ноте я завершу размышления о том, на что влияет SITES_ID в settings.

Какой же из всего этого можно сделать вывод:

В Django заложена возможность создания мультидоменных платформ, подобных wix.com, ucoz.ru, shopify.com с единым административным интерфейсом и простым и быстрым разграничением доступа к данным. Эта возможность заложена в CurrentSiteManager из django.contrib.sites, остается только правильно её реализовать.

Я готов рассказать в следующей статье, об этой доработке, если это будет интересно читателям.