Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Comment travailler avec les generic foreign key dans Django ?

+8 votes

J'utilise des Generic Foreign Key sur un de mes projets, et je rencontre quelques difficultés pour travailler avec, dès qu'il s'agit de faire des requêtes pourtant peu complexes.

J'ai en gros deux questions assez basique :

  • Comment faire une requête de type monmodel.contenttype_set.all() comme on les retrouves sur une FK classique ?
  • Pourquoi, lorsque je set une limit sur les content types admis, je peux quand même, en console interactive par exemple, lier la FK à un type en dehors de cette limite ?

Histoire d'être complet, j'ai développé un petit exemple.

Imaginons le schéma suivant :

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

class City(models.Model):
    name = models.CharField(max_length=255)


class People(models.Model):
    """ Des êtres humains, avec plus de trucs que ce qui est écrit ici bien entendu.
    """
    name = models.CharField(max_length=255)
    specific_attribute = models.CharField('human specific attribute')
    city = models.ForeignKey(City)


class Company(models.Model):
    """ Des sociétés, avec une logique métier qui leur est propre.
        Bien entendu dans l'exemple on a l'impression que c'est pareil...
    """
    name = models.CharField(max_length=255)
    specific_attribute = models.CharField('human specific attribute')
    city = models.ForeignKey(City)


class Invoice(models.Model):
    """ Les factures peuvent être adressées soit à des sociétés,
        soit à des humains.
    """
    number = models.IntegerField()
    limit = models.Q(app_label='my_app',
                     model='people') | models.Q(app_label='my_app',
                                                model='company')
    content_type = models.ForeignKey(ContentType,
                                     verbose_name='adressed to',
                                     limit_choices_to=limit,
                                     null=False,
                                     blank=False,)
    object_id = models.PositiveIntegerField(
        verbose_name='related entity',
        null=False,
    )
    content_object = GenericForeignKey('content_type', 'object_id')

OK, situation assez basique. Imaginons que j'ai en base la ville de Nantes. Je pourrais trouver tous les gens qui y sont installés grâce à :

nantes = City.object.get(pk=1)
nantes.people_set.all()

Et de l'autre côté, c'est encore plus simple :

bertrand = People.object.get(pk=1)
bertrand.city  # on a directement accès à l'objet

Maintenant, dans le cas de ma generic FK. C'est encore simple quand on part de la facture :

facture = Invoice.object.get(pk=1)
facture.content_object  # directement l'objet lié
facture.content_type  # le content type, histoire de savoir à quoi l'instance est liée
facture.object_id  # l'identifiant de l'object stocké dans content type. Useless

En revanche, imaginons que je veuille chercher rapidement toutes les factures émises à notre ami Bertrand, déclaré un peu plus haut. Comment faire ? Contrairement à nantes et son peopleset, je n'ai pas accès à un invoiceset dans l'object people ou dans l'object company.

Quelle est la meilleure manière de faire, par exemple, la requête pour la page de listing des factures pour un particulier ?

demandé 2-Jan-2015 par tominardi (200 points)

Je suis agréablement surpris par la qualité des premières questions : intéressantes, pragmatiques, avec du code et des phrases complètes. C'est top.

Si la réponse est la bonne solution pour toi, alors clique sur le signe "check" sous le compteur d vote pour l'accepter. Cela la met en avant, et donne des points à l'auteur

2 Réponses

+5 votes
 
Meilleure réponse
class InvoiceMixin(object):
  ctype = ContentType.objects.get_for_model(self.__class__)
  @property
  def invoice(self):
    try:
      invoices = Invoice.objects.get(content_type__pk = ctype.id, object_id=self.id)
    except Invoice.DoesNotExist:
      return None
    return invoices



class People(models.Model, InvoiceMixin):
        ....

Quelque chose dans cet esprit là devrait faire l'affaire.

UPDATE:
En ce qui concerne l'option limit_choices_to, je viens de lire ceci:

If limitchoicesto is or returns a Q object, which is useful for
complex queries, then it will only have an effect on the choices
available in the admin when the field is not listed in rawidfields
in the ModelAdmin for the model.

Faudras que tu nous le confirmes.

répondu 2-Jan-2015 par Nsukami_ (1,976 points)
edité 2-Jan-2015 par Nsukami_

A noter que si le content type ne change jamais, l'appel à

ctype = ContentType.objects.get_for_model(self.__class__)

peut être mis en dehors de la méthode pour gagner un peu en perfs.

Ça me semble très bien effectivement.

J'attends encore une demi journée histoire de voir si quelqu'un pense à ajouter un petit commentaire concernant l'autre question, ou d'autre réponse, puis je check, et enfin je promet de ne plus jamais poser 2 questions dans le même thread.

Putain ces gens sont super polis, bordel de merde.

faut attendre une semaine et le naturel va refaire surface :D

Au sens stricte du terme, cette réponse est la plus directe.
Mais la contribution de Sam est hyper cohérente et mérite une attention particulière.

+5 votes

Pour ajouter quelques infos à la réponse précédente...

En revanche, imaginons que je veuille chercher rapidement toutes les factures émises à notre ami Bertrand, déclaré un peu plus haut. Comment faire ? '

# On chope le content type. On a besoin de le faire qu'une fois pour toutes
# les requêtes concernant People.
ctype = ContentType.objects.get_for_model(People)
# A faire pour chaque personne
person = People.objects.get(name='Bertrand')
invoices = Invoice.objects.filter(content_type__pk=ctype.id, object_id=person.id)

On peut néanmoins se faciliter la tache en utilisant GenericRelation, un field spéciale pour ce genre de cas :

from django.contrib.contenttypes.fields import GenericRelation

class People(models.Model):
    """ Des êtres humains, avec plus de trucs que ce qui est écrit ici bien entendu.
    """
    name = models.CharField(max_length=255)
    specific_attribute = models.CharField(max_length=255)
    city = models.ForeignKey(City)
    invoices = GenericRelation(Invoice)

Ce qui permet de faire des trucs comme :

People.objects.get(name='Bertrand').invoices
People.objects.filter(name='Bertrand', invoices__number=1)

C'est plus simple, même si ça fait la même chose derrière au final, et donc ça bouffe en ressources.

Mais ce n'est pas très naturel comme organisation de pensée. C'est pour ça qu'en général je déconseille généralement les relations génériques car elles sont souvent juste le reflet d'un modèle de données qui a besoin d'être plus creusé.

Par exemple dans ton cas, il manque un intermédiaire entre invoice, people et company. People et Company peuvent faire des invoices, parce que ce sont des clients. C'est cette notion qu'il faut insérer dans ton modèle. Soit par une table parente à people et company qui serait "client", soit par une foreignkey vers un modèle ClientAccount.

Pourquoi, lorsque je set une limit sur les content types admis, je peux quand même, en console interactive par exemple, lier la FK à un type en dehors de cette limite ?

Je pense que tu veux dire, si tu entres manuellement l'ID ? Dans ce cas, il n'y a pas de check sur l'ID. A mon avis, uniquement si tu passe un content object complet. Le but de ce check est de permettre à l'admin de trouver ses petits, pas d'empêcher le dev de faire des bêtises.

répondu 2-Jan-2015 par Sam (4,974 points)

Bim! Là ça commence à rentrer un peu plus loin dans le débat, et c'est bien ce genre de réponse que j'espérais trouver.

Si j'ai pris l'exemple des factures, c'est parce que c'est exactement cet exemple là qu'on m'avais donné pour l'application des GenericFK. Par le passé, j'avais fait des modèles de facture avec un champs entreprise et un champs particulier, qui pouvaient être à None mais pas les deux en même temps.

En prenant un modèle intermédiaire Client, on se poserais pourtant la même question : un client peut être une entreprise ou un particulier (avec une logique métier forcément différente derrière). Du coup les Generic FK paraissent une bonne solution, même si effectivement c'est loin d'être intuitif, c'est coûteux, et ça pue niveau modélisation.

ps: on dirait que le front de indexerror est hyper lourd, ça me freeze mon navigateur...

Du coup comme c'est lent je complète dans un autre commentaire...

Donc oui, le ClientAccount serait une solution, je pense que le OneToOneField est fait pour ça non ?

Ouai le JS de l'éditeur est bien velu. A mon avis il recalcule la preview à chaque touche.

Yes, un One2one marche bien ici.

Bon allez, un dernier cas d'utilisation qui me gêne encore et après je fais un choix de meilleure réponse :

Mon principal cas à moi, c'est que je veux avoir une messagerie qui permette d'envoyer des messages à des entitées, que les utilisateurs liés aux entitées puissent consulter.

Par exemple, un message peut être envoyé à un utilisateur directement, mais aussi à un groupe (dans ce cas, tous les utilisateurs liés au groupe peuvent consulter le message), aux gens inscrits à un événement, etc.

Dan ce cas précuis, la GenericFK semble mieux adaptée, non ?

Tu peux aussi voir les choses ainsi : tu envoies toujours un message à une conversation. Cette conversation peut être entre deux personnes, ou plus. Ca te permet de mettre un sujet à la conversation, un historique, des permissions, etc.

...