Фреймворк contenttypes

В Django входит приложение contenttypes, которое позволяет отслеживать все модели вашего Django проекта. Это приложение предоставляет высокоуровневый, обобщенный интерфейс для работы с вашими моделями.

Обзор

В основе приложения contenttypes лежит модель ContentType, которая находится в django.contrib.contenttypes.models.ContentType. Экземпляр ContentType представляет и хранит информацию о моделях, использующихся в вашем проекте, и новые экземпляры модели ContentType создаются автоматически при добавлении новых моделей в проект.

У экземпляров ContentType есть методы, позволяющие получить класс модели, который они представляют или получить объект для этого класса модели. У модели ContentType имеется также собственный менеджер(custom manager), который предоставляет методы для работы с классом ContentType и для получения экземпляров ContentType для конкретной модели.

Взаимосвязь между ContentType и вашими моделями можно использовать для создания “обобщенных” (“generic” ) отношений между экземпляром вашей модели и экземпляром любой другой модели в проекте.

Установка и подключение contenttypes

Фреймворк contenttypes включен по умолчанию и находится в списке INSTALLED_APPS файла настроек, созданного вызовом команды django-admin startproject. Если вам необходимо отключить фреймворк или добавить его вручную, просто удалите (или добавьте) в список INSTALLED_APPS приложение 'django.contrib.contenttypes'.

Рекомендуется всегда подключать contenttypes фреймворк в проекте, поскольку его наличие требуется для работы ряда других встроенных приложений Django.

  • Встроенное приложение администрирования Django использует contenttypes для ведения логов по добавлению или изменению объектов через админку.

  • Django’s authentication framework uses it to tie user permissions to specific models.

Модель ContentType

class ContentType

Каждый экземпляр ContentType содержит два поля, которые вместе уникальным образом описывают каждую модель в приложении:

app_label

Первое, это имя приложения в которое входит данная модель. Данные берутся из атрибута app_label модели и включают в себя только последнюю часть пути, который используется для импорта модели. Н-р, в случае “django.contrib.contenttypes” используется значение атрибута app_label для “contenttypes”.

model

Имя модели класса

Также доступны следующие свойства:

name

“Читабельное” имя модели. Берется из атрибута verbose_name модели.

Изменено в Django 1.8:

До Django 1.8 свойство name было полем модели ContentType.

Покажем на примере как это все работает. Если приложение contenttypes уже установлено, то добавьте приложение sites в INSTALLED_APPS файла настроек и выполните команду manage.py migrate для создания таблиц и завершения установки модели django.contrib.sites.models.Site. Параллельно с этим будет создан новый экземпляр ContentType со следующими значениями:

  • Атрибут app_label со значением 'sites' (последняя часть the last part of the Python path “django.contrib.sites”).

  • Атрибут model со значением 'site'.

Методы экземпляра ContentType

Каждый экземпляр ContentType имеет методы, которые позволяют получить доступ к модели, представленной этим экземпляром ContentType , или получить объекты этой модели:

ContentType.get_object_for_this_type(**kwargs)

Принимает набор корректных фильтров полей(lookup arguments) для модели, представленной данным ContentType и выполняет метод get() этой модели, возвращая соответствующий объект.

ContentType.model_class()

Возвращает класс модели, представленной данным экземпляром ContentType.

Н-р, мы можем получить экземпляр ContentType для модели User следующим образом:

>>> from django.contrib.contenttypes.models import ContentType
>>> ContentType.objects.get(app_label="auth", model="user")
<ContentType: user>

А затем использовать полученный результат, чтобы получить модель User, или для непосредственного доступа к классу модели User:

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username='Guido')
<User: Guido>

Комбинация этих двух методов get_object_for_this_type() и model_class(), дает нам два крайне важных и полезных варианта их использования:

  1. Воспользовавшись этими методами, вы можете писать высокоуровневый , обобщенный(generic) код, и выполнять запрос к любой установленной в приложении модели. Вместо того, чтобы импортировать конкретные модели “по одиночке”, вы можете передать нужные параметры app_label и model в класс ContentType во время выполнения, и затем работать с полученной моделью класса или получить конкретные объекты этой модели.

  2. Вы можете использовать ContentType чтобы связать экземпляр вашей модели с любыми произвольными классами моделей проекта, и использовать указанные методы для доступа к этим моделям.

Некоторые из встроенных приложений Django используют последний подход. Н-р, в системе полномочий( permissions system), в фреймворке аутентификации Django, в модели Permission используется внешний ключ(foreign key) к ContentType; это позволяет создать обобщенную связь с различными моделями и реализовать концепцию ограничений, такую как “пользователь может добавить запись в блог” или “пользователь может удалить сообщение из новостей”.

The ContentTypeManager

class ContentTypeManager

Класс ContentType обладает собственным менеджером, ContentTypeManager, который включает в себя следующие методы:

clear_cache()

Очищает внутренний кэш, используемый ContentType для отслеживания моделей, для которых были созданы экземпляры ContentType. Вероятно, вам никогда не придется вызывать этот метод напрямую, поскольку Django вызывает его автоматически, при необходимости.

get_for_id(id)

Получить экземпляр ContentType по идентификатору(ID). Поскольку метод использует тот же разделяемый кэш, что и метод get_for_model(), предпочтительней пользоваться именно им, а не привычным запросом ContentType.objects.get(pk=id).

get_for_model(model, for_concrete_model=True)

Принимает в качестве аргумента либо класс модели, либо экземпляр модели, и возвращает экземпляр ContentType, представляющего данную модель. for_concrete_model=False позволяет получить ContentType для прокси-модели.

get_for_models(*models, for_concrete_models=True)

Принимает в качестве аргумента произвольное число классов модели и возвращает словарь с отображением класса модели на экземпляр ContentType, представляющего данную модель. for_concrete_model=False позволяет получить ContentType для прокси-модели.

get_by_natural_key(app_label, model)

Возвращает экземпляр ContentType, определенный уникальным образом для переданных аргументов: имя приложения(application label) и имя модели(model name). Главное назначение этого метода, дать возможность ссылаться на объекты ContentType посредством натуральных ключей(natural key) в процессе десериализации.

Метод get_for_model() особенно полезен, когда вам необходимо работать с ContentType, но вы не хотите “заморачиваться” с получением метаданных модели для поиска вручную:

>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>

Обобщенные связи(generic relations)

Вы можете создать в вашей модели внешний ключ на ContentType, что позволит связать вашу модель с любой другой моделью, как это было описано выше на примере Permission. Но можно пойти еще дальше и использовать ContentType для реализации абсолютно обобщенных (иногда говорят “полиморфных”) отношений между моделями.

Вот простой пример: реализуем систему тэгов(ярлычков), которая могла бы выглядеть так

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    def __str__(self):              # __unicode__ on Python 2
        return self.tag

Обычное поле ForeignKey может “указывать” только на одну модель, что означает, - если в модели TaggedItem есть поле ForeignKey, его можно “связать” с одной и только одной моделью, для которой и будут сохраняться тэги. Приложение contenttypes предоставляет нам поле специального типа (GenericForeignKey), которое решает обозначенную выше проблему и позволяет создать связь с любой моделью:

class GenericForeignKey

Существуют три правила по созданию и настройке GenericForeignKey:

  1. Создайте в вашей модели поле типа ForeignKey, указав в качестве внешней модели ContentType. Обычно такому полю дают имя “content_type”.

  2. Создайте в вашей модели поле, которое будет хранить значения первичных ключей экземпляров модели, с которой вы создаете связь. Для большинства моделей, это поле типа PositiveIntegerField. Обычно такому полю дают имя “object_id”.

  3. Создайте в вашей модели поле типа GenericForeignKey, и передайте ему в качестве аргументов, имена полей созданных ранее. Если эти поля названы “content_type” и “object_id”, вы можете не передавать их, – эти имена используются в GenericForeignKey по умолчанию.

for_concrete_model

При False, поле может ссылаться на прокси-модель. Отображает аргумент for_concrete_model метода get_for_model(). По умолчанию равно True.

Тип первичного ключа

Поле “object_id” не обязательно должно быть того же типа, что и у первичного ключа в привязанной модели, но должно соблюдаться условие, что значения первичного ключа могут быть приведены к тому же типу, что и у поля “object_id ” методом get_db_prep_value().

Н-р, если вы хотите создать обобщенные отношения с моделями, использующими в качестве первичных ключей типы IntegerField или CharField, вы можете использовать тип CharField для вашего поля “object_id”, поскольку целочисленные значения могут быть корректно приведены к строковым методом get_db_prep_value().

Для максимальной гибкости можно использовать тип TextField, который не накладывает ограничения на длину строки, но такое решение может негативно отразиться на производительности, в зависимости от используемой базы данных.

Не существует универсального решения для выбора наилучшего типа поля. Вы должны изучить модели с которыми собираетесь работать, и на основании этого принять решение о выборе типа для поля, руководствуясь принципами эффективности.

Сериализация связей с ContentType

При сериализации данных модели (например, при создании fixtures) , которая имеет обобщенные связи, вам вероятно необходимо будет воспользоваться натуральным ключом, чтобы корректно определить связи с объектами ContentType. Смотрите натуральные ключи и dumpdata --natural для дополнительной информации.

После создания связи мы можем использовать API, похожий на тот, что используется в обычном ForeignKey; каждый TaggedItem содержит поле content_object, которое возвращает связанный с ним объект. Мы можем присвоить этому полю произвольный объект, или указать этот объект при создании TaggedItem:

>>> from django.contrib.auth.models import User
>>> guido = User.objects.get(username='Guido')
>>> t = TaggedItem(content_object=guido, tag='bdfl')
>>> t.save()
>>> t.content_object
<User: Guido>

Из-за особенностей реализации GenericForeignKey, вы не можете использовать такое поле с фильтрами (filter() и exclude(), н-р) в запросах API базы данных. Поскольку GenericForeignKey это не совсем “обычное” поле,примеры ниже не будут работать:

# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)

Также GenericForeignKeys не отображется в ModelForms.

Обратная обобщенная связь(reverse generic relations)

class GenericRelation
related_query_name

По умолчанию обратная связь не создается. Чтобы создать такую связь, укажите параметр related_query_name поля. Это позволять получить связанные объекты и использовать поле для фильтрации результатов запроса.

Если модель, с которой предстоит работать наиболее часто, известна заранее, вы можете добавить “обратную” обобщенную связь между моделями и использовать дополнительные возможности API. Н-р:

from django.db import models
from django.contrib.contenttypes.fields import GenericRelation

class Bookmark(models.Model):
    url = models.URLField()
    tags = GenericRelation(TaggedItem)

Каждый экземпляр Bookmark имеет атрибут tags, который можно использовать чтобы получить доступ к связанному с ним TaggedItems:

>>> b = Bookmark(url='https://www.djangoproject.com/')
>>> b.save()
>>> t1 = TaggedItem(content_object=b, tag='django')
>>> t1.save()
>>> t2 = TaggedItem(content_object=b, tag='python')
>>> t2.save()
>>> b.tags.all()
[<TaggedItem: django>, <TaggedItem: python>]

Создав GenericRelation с related_query_name, можно использовать связь в запросах:

tags = GenericRelation(TaggedItem, related_query_name='bookmarks')

Это позволяет фильтровать, сортировать и выполнять запросы по Bookmark из TaggedItem:

>>> # Get all tags belonging to books containing `django` in the url
>>> TaggedItem.objects.filter(bookmarks__url__contains='django')
[<TaggedItem: django>, <TaggedItem: python>]

Также как GenericForeignKey, GenericRelation принимает аргументами имена полей content-type и object-ID . Если модель, имеющая обобщенный внешний ключ не использует имена по-умолчанию для этих полей, а любые другие, – вы должны передать эти имена полей в GenericRelation при его инициализации. Н-р, если бы мы использовали в модели TaggedItem поля с именами content_type_fk и object_primary_key при создании внешнего ключа, то поле GenericRelation следовало бы определить таким образом:

tags = GenericRelation(TaggedItem,
                       content_type_field='content_type_fk',
                       object_id_field='object_primary_key')

Ну и конечно, если вы не захотите добавить обратную связь, вы можете получить доступ к объекту и “обходным путем”:

>>> b = Bookmark.objects.get(url='https://www.djangoproject.com/')
>>> bookmark_type = ContentType.objects.get_for_model(b)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id,
...                           object_id=b.id)
[<TaggedItem: django>, <TaggedItem: python>]

Обратите внимание, если в модели с GenericRelation не используются значения по умолчанию для ct_field или fk_field для GenericForeignKey (например, у вас есть модель Comment с ct_field="object_pk"), вам необходимо установить content_type_field и/или object_id_field таким образом, чтобы значения ct_field и fk_field в GenericRelation соответствовали значениям для ct_field и fk_field в модели содержащей GenericForeignKey:

comments = fields.GenericRelation(Comment, object_id_field="object_pk")

Также обратите внимание, что в случае удаления объекта, имеющего поле GenericRelation, все объекты у которых GenericForeignKey указывает на этот удаляемый объект, тоже будут удалены. Для примера выше это значит, что если удалить объект Bookmark, то любые TaggedItem, связанные с ним, будут удалены вместе с ним.

В отличии от обычного ForeignKey, тип GenericForeignKey не принимает аргумент on_delete для расширения поведения модели; если это необходимо, вы можете избежать каскадного удаления связанных объектов просто не используя GenericRelation, и указать необходимое поведение с помощью сигнала pre_delete.

Обобщенные связи и агрегация

API агрегации Django для баз данных работает с GenericRelation. Например, вы можете получить количество тегов для всех закладок:

>>> Bookmark.objects.aggregate(Count('tags'))
{'tags__count': 3}

Обобщенные связи в формах

Модуль django.contrib.contenttypes.forms предлагает нам следующее:

class BaseGenericInlineFormSet
generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False)

Возвращает GenericInlineFormSet, используя modelformset_factory().

Вы должны предоставить имена для ct_field и object_id если они отличаются от значений по умолчанию, - content_type и object_id. Прочие параметры аналогичны тем, что описаны в modelformset_factory() и inlineformset_factory().

Аргумент for_concrete_model соответствует параметру for_concrete_model для GenericForeignKey.

Обобщенные связи в админке

Модуль django.contrib.contenttypes.admin предоставляет GenericTabularInline и GenericStackedInline (дочерние классы GenericInlineModelAdmin)

Эти классы и функции позволяют использовать обобщенные отношения объектов при создании форм и в админке Django. За дополнительной информацией обратитесь к модель набора форм и admin .

class GenericInlineModelAdmin

Класс GenericInlineModelAdmin наследует все свойства класса InlineModelAdmin. Но также имеет ряд собственных атрибутов для работы с обобщенными связями:

ct_field

Имя поля внешнего ключа ContentType модели. По умолчанию content_type.

ct_fk_field

Имя целочисленного поля, которое хранит идентификатор конкретного объекта связанной модели. По умолчанию object_id.

class GenericTabularInline
class GenericStackedInline

Подклассы GenericInlineModelAdmin позволяющие настраивать отображение данных в сложенном(stacked) или табличном(tabular) виде, соответственно.