Фреймворк 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.py startproject. Если вам необходимо отключить фреймворк или добавить его вручную, просто удалите (или добавьте) в список INSTALLED_APPS приложение 'django.contrib.contenttypes'.

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

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

  • Django’s authentication framework uses it to tie user permissions to specific models.
  • Система комментариев Django (django.contrib.comments) использует contenttypes для “добавления” комментариев к моделям.

Модель ContentType

class ContentType

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

app_label

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

model

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

name

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

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

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

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

  • Атрибут name со значением '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
>>> user_type = ContentType.objects.get(app_label="auth", model="user")
>>> user_type
<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, представляющего данную модель.

get_for_models(*models[, for_concrete_models=True])

Принимает в качестве аргумента произвольное число классов модели и возвращает словарь с отображением класса модели на экземпляр 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
>>> user_type = ContentType.objects.get_for_model(User)
>>> user_type
<ContentType: user>
New in Django 1.5:

Prior to Django 1.5, get_for_model() and get_for_models() always returned the ContentType associated with the concrete model of the specified one(s). That means there was no way to retrieve the ContentType of a proxy model using those methods. As of Django 1.5 you can now pass a boolean flag – for_concrete_model and for_concrete_models respectively – to specify wether or not you want to retrieve the ContentType for the concrete or direct model.

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

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

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

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

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

    # On Python 3: def __str__(self):
    def __unicode__(self):
        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
New in Django 1.6.

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

Primary key type compatibility

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

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

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

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

Serializing references to ContentType objects

При сериализации данных модели (н-р, при создании fixtures) , которая имеет обобщенные связи, вам вероятно необходимо будет воспользоваться натуральным ключем, чтобы корректно определить связи с объектами ContentType. Смотрите натуральные ключи(natural keys) и 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

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

class Bookmark(models.Model):
    url = models.URLField()
    tags = generic.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>]

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

tags = generic.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 (н-р, в приложении django.contrib.comments используются ct_field="object_pk"), вам необходимо установить content_type_field и/или object_id_field таким образом, чтобы значения ct_field и fk_field в GenericRelation соответствовали значениям для ct_field и fk_field в модели содержащей GenericForeignKey:

comments = generic.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'))

Тем не менее, это не сработает так, как вы ожидаете. Обобщенные связи используют дополнительные фильтры в запросе(queryset), чтобы гарантировать непротиворечивость контентного типа, но метод aggregate() не принимает это в расчет. К настоящему моменту, если вам необходимо выполнить агрегирующие запросы с объектами с обобщенной связью, вы должны выполнить это другими методами, не используя возможности API для агрегации.

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

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

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

class GenericInlineModelAdmin

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

ct_field

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

ct_fk_field

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

class GenericTabularInline
class GenericStackedInline

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

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)

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

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

New in Django 1.6:

The for_concrete_model argument corresponds to the for_concrete_model argument on GenericForeignKey.