O Framework contenttypes

O Django incluí uma applicação contenttypes que pode mapear todos os modelos instalados no seu projeto feito com o Django, provendo uma interface genérica de auto-nível para trabalhar com seus models.

Visão geral

O coração da aplicação contenttypes é o model ContentType, que reside em django.contrib.contenttypes.models.ContentType. As instâncias de ContentType representam e armazenam informações sobre os models instalados no seu projeto, e novas instâncias de ContentType são automaticamente criadas quando um novos models são instalados.

As instâncias do ContentType possuem métodos para retornar as classes de models que elas representam e para consultas de objetos para estes models. O ContentType também tem um gerenciador customizado que adiciona métodos par trabalhar com ContentType e para obtenção de instâncias de ContentType para um model em particular.

Os relacionamentos entre seus models e ContentType pode também ser usados para habilitar relacionamentos “genericos” entre uma instância de um de seus models e instâncias de qualquer model que você tenha instalado.

Instalando o framework contenttypes

O framework contenttypes vem incluído por padrão no INSTALLED_APPS quando é criado pelo django-admin.py startproject, mas se você o tiver removido ou se você manualmente setou seu INSTALLED_APPS, você pode habilitá-lo adicionando 'django.contrib.contenttypes' no INSTALLED_APPS.

Geralmente é uma boa idéia ter o framework contenttypes instaldo; vários outras applicações nativas do Django necessitam dele:

  • A aplicação admin o usa para logar a história de cada objeto adicionado ou modificado através da interface de administração.
  • O framework de autenticação do Django o usa para amarrar as permissões de usuários para models específicos.
  • O sistema de comentários do Django (django.contrib.comments) o usa para “atachar” comentário em qualquer model instalado.

O model ContentType

class models.ContentType

Cada instância do ContentType possui três campos, que juntos, descrevem a unicidade de um model instalado:

app_label

O nome da applicação da qual o model faz parte. Este é o atributo app_label do model, e incluí somente a última parte do caminho de import do Python da aplicação; “django.contrib.contenttypes”, por exemplo, tem um app_label “contenttypes”.

model

O nome da classe do model.

name

O nome, legível por humanos, do model. Este é o atributo verbose_name do model.

Vamos olhar um exemplo, para entender como isso funciona. Se você já tem a aplicação contenttypes instalada, e adicionou a aplicação sites no INSTALLED_APPS e executou o manage.py syncdb para instalá-lo, o model django.contrib.sites.models.Site será instalado dentro do seu banco de dados. Imediatamente uma nova instância de ContentType será criada com os seguintes valores:

  • app_label será setado como 'sites' (a última parte do caminho Python “django.contrib.sites”).
  • model será setada como 'site'.
  • name será setada como 'site'.

Métodos das instâncias do ContentType

class models.ContentType

Cada instância ContentType possui métodos que permitem você pegar através da instância do ContentType o model que ela representa, ou receber objetos daquele model:

models.ContentType.get_object_for_this_type(**kwargs)

Pega um conjunto de argumentos aparentes válidos para o model que ContentType representa, e faz um get() lookup sobre este model, retornando o objeto correspondente.

models.ContentType.model_class()

Retorna a classe de model representada por esta instância de ContentType.

Por exemplo, nós podemos procurar no ContentType pelo model User:

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

E então usá-lo para pesquisar por um User particular, ou para ter acesso a classe model do User:

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

Juntos, get_object_for_this_type() e model_class() habilitam dois casos extremamente importantes:

  1. Usando estes métodos, você pode escrever código genérico de auto-nível que faz consultas sobre qualquer model instalado -- ao invés de importar e usar uma classe de model específica, você passar um app_label e model dentro de um ContentType em tempo de execução, e então trabalhar com a classe model ou receber objetos dela.
  2. Você pode relacionar outro model com ContentType como uma forma de vinculá-lo a determinadas classes de model, e usar estes métodos para acessar essas classes.

Várias aplicações nativas do Django fazem uso desta última técnica. Por exemplo, o sistema de permissões <django.contrib.auth.models.Permission> no framework de autenticação usa um model Permission com uma chave estrangeira para ContentType; isso permite que o Permission represente conceitos como "pode adicionar entrada no blog" ou "pode apagar notícia".

O ContentTypeManager

class models.ContentTypeManager

O ContentType também tem um gerenciador personalizado, ContentTypeManager, que adiciona os seguintes métodos:

clear_cache()

Limpa um cache interno usado por instâncias do ContentType. Você provavelmente nunca precisará chamar este método você mesmo; O Django o chamará automaticamente quando for necessário.

get_for_model(model)

Pega ambos, um model ou uma instância de um model, e retorna a instância ContentType representando aquele model.

O método get_for_model() e especialmente usual quando você sabe que precisa trabalhar com um ContentType mas não quer ter o trabalho de obter os metadados do model para executar um lookup manual:

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

Relações genéricas

Adicionando uma chave estrangeira em um de seus models para ContentType permite seu model vincular-se efetivamente em outro model, como no exemplo da classe Permission acima. Mas isto é possível ir além e usar ContentType para habilitar um relacionamento verdadeiramente genérico (algumas vezes chamado "polimórfico") entre os models.

Um simples exemplo é um sistema de tags, que pode parecer com isto:

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

   def __unicode__(self):
       return self.tag

Uma ForeignKey normal pode somente "apontar para" um outro model, que significa que se o model TaggedItem usou uma ForeignKey ele teria de escolher somente um model para armazenar as tags. A aplicação contenttypes provê um tipo de campo especial -- django.contrib.contenttypes.generic.GenericForeignKey -- que funciona contornando isto e permitindo o relacionamento ser feito com qualquer model. Há três configurações para uma GenericForeignKey:

  1. Dê a seu model uma ForeignKey para ContentType.

  2. Dê a seu model um campo que consiga armazenar um valor de chave primária dos models que você irá relacionar. (Para maioria dos models, isto significa um IntegerField ou PositiveIntegerField.)

    Este campo deve ser do mesmo tipo da chave primária dos models envolvidos no relacionamento genérico. Por exemplo, se você usa IntegerField, você não será capaz de formar uma relação genérica com um model que utiliza um CharField como uma chave primária.

  3. Dê a seu model uma GenericForeignKey, e passe os nomes dos campos descritos acima para ela. Se estes campos são nomeados "content_type" e "object_id", você pode omitir isto -- estes são nomes padrão dos campos que GenericForeignKey irá procurar.

Isto habilitará uma API similar a que é usada por uma ForeignKey normal; cada TaggedItem terá um campo content_type que retorna o objeto ao qual está relacionado, e você pode também atribuir ao campo ou usá-lo quando estiver criando uma 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>

Devido a forma como o GenericForeignKey é implementado, você não pode usar campos estes campos diretamente com filtros (filter() e exclude(), por exemplo) via API de banco de dados. Eles não são campos normal de objetos. Estes exemplos não irão funcionar:

# Isto falhará
>>> TaggedItem.objects.filter(content_object=guido)
# Isto também falhará
>>> TaggedItem.objects.get(content_object=guido)

Reverso de relações genéricas

Se você sabe quais models serão usados com mais frequência, você pode também adicionar um "reverso" de relações genéricas para habilitar uma API adicional. Por exemplo:

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

Cada instância Bookmark terá um atributo tags, que pode ser usado para receber seus TaggedItems associados:

>>> b = Bookmark(url='http://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>]

Assim como django.contrib.contenttypes.generic.GenericForeignKey aceita os nomes dos campos content-type e object-ID como argumentos, para construir uma GenericRelation; se o model que possui a chave estrangeira genérica está usando nomes diferentes do padrão, para estes campos, você deve passar os nomes dos campos quando estiver configurando uma GenericRelation. Por exemplo, se o model TaggedItem referido acima usasse campos com nomes content_type_fk e object_primary_key para criar suas chaves primárias genéricas, então uma GenericRelation teria de ser definida como:

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

É claro, Se você não adicionar o reverso de relacionamento, você pode fazer os mesmos tipo de busca manualmente:

>>> b = Bookmark.objects.get(url='http://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>]

Note que se o model com uma GenericForeignKey que você está referenciando usa um valor diferente do padrão para ct_field ou fk_field (e.g. a aplicação django.contrib.comments usa ct_field="object_pk"), você precisará passar content_type_field e object_id_field para GenericRelation.:

comments = generic.GenericRelation(Comment, content_type_field="content_type", object_id_field="object_pk")

Note que se você deleta um objeto que tem uma GenericRelation, quaisquer objetos que possuem uma GenericForeignKey apontando para ele, serão deletados também. No exemplo acima, isto significa que se um objeto Bookmark foi deletado, qualquer objeto TaggedItem relacionado com ele, será deletado ao mesmo tempo.

Relacionamentos genéricos em formulários e admin

O django.contrib.contenttypes.generic provê ambos, GenericInlineFormSet e GenericInlineModelAdmin. Isso permite o uso de relacionamentos genéricos nos formulários e no admin. Veja a documentação do model formset e admin para mais informações.

class generic.GenericInlineModelAdmin

A classe GenericInlineModelAdmin herda todas as propriedades da classe InlineModelAdmin. No entanto, ela adiciona alguns próprios para funcionar com relacionamento genérico.

ct_field

O nome do campo chave estrangeira ContentType para o model. O padrão é content_type.

ct_fk_field

O nome do campo inteiro que representa o ID do objeto relacionado. O padrão é object_id.