Django : créer un many-to-many self-referencing double way !

Je vous explique, et c’est bien plus facile en français en fait 😉
Je voulais mettre en place une relation style facebook mais en plus évolué : une personne peut avoir une ou plusieurs personnes ami(e)(s). Seulement, on ne gère qu’un seul type de relation par personne. Par exemple, Olivier est ami proche avec Elsa. Déjà, cela implique qu’Elsa est amie proche avec Olivier. Donc, il faut imaginer une table qui aura une relation qui va s’auto-référencer, via une table intermédiaire dans laquelle on précisera le type de relation.

Ce n’est pas très dur à imaginer :

Personne <-> PersonneRelation.

Déclaration de Personne :

class Personne(BaseModel):
    user = models.ForeignKey(User)
    relations = models.ManyToManyField('self',
                                       through='PersonneRelation',
                                       symmetrical=False)

Maintenant, là où le problème se pose c’est qu’au moment de l’ajout d’une relation dans un sens, par exemple mari femme, il faut que la relation soit aussi ajoutée dans l’autre sens, à la fois pour des questions de performance, mais aussi pour des questions d’affichage (« Simon est le mari de Arlette » mais dans l’autre sens, « Arlette est la femme de Simon », on constate que les phrases sont totalement différentes… et quand c’est mari femme, on peut imaginer ne pas se compliquer l’existence en regardant dans si la personne est un homme ou une femme et en déduire le sens, mais si jamais c’est une relation maître – élève ? HEIN ? COMMENT ON FAIT ? Si c’est un formateur qui forme des adultes ? HEIN ? ON DIT QUOI LA ? On fait moins le malin d’un coup HEIN ! Oui bon ok il faut que je décompresse un peu…). Donc l’idée est (1) de définir les relations possibles en dur (vous pourrez très facilement faire évoluer cela en une relation supplémentaire vers une table qui définit le type de relation et vous n’aurez plus de limites en termes de types de relations possibles, mais ce qui suit est déjà assez long à expliquer, je ne vais pas en plus l’alourdir avec du code supplémentaire) et de (2) gérer au moment où on insère un nouvel enregistrement : si jamais la relation opposée n’est pas encore présente, on l’ajoute. Ah. J’oubliais le (3) modifications = appliquer la même de l’autre côté et suppression : supprimer l’autre côté aussi.

Stop bullshit, du code :

@python_2_unicode_compatible
class PersonneRelation(BaseModel):

    TYPE_AMI = u'0'
    TYPE_CONNAISSANCE = u'1'
    TYPE_PARENT_ENFANT = u'2'
    TYPE_MARI_FEMME = u'3'
    TYPE_PROFESSEUR_ELEVE = u'4'
    TAB_TYPES = {
        TYPE_AMI: _(u'friend'),
        TYPE_CONNAISSANCE: _(u'relationship'),
        TYPE_PARENT_ENFANT: _(u'parent > child'),
        TYPE_MARI_FEMME: _(u'husband <> wife'),
        TYPE_PROFESSEUR_ELEVE: _(u'teacher > student'),
    }
    TAB_TYPES_REVERSE = {
        TYPE_AMI: _(u'friend'),
        TYPE_CONNAISSANCE: _(u'relationship'),
        TYPE_PARENT_ENFANT: _(u'child > parent'),
        TYPE_MARI_FEMME: _(u'wife <> husband'),
        TYPE_PROFESSEUR_ELEVE: _(u'student > teacher'),
    }
    type_relation = models.CharField(max_length=1,
                                     choices=[(a, b) for a, b in
                                              list(TAB_TYPES.items())],
                                     default=TYPE_AMI)
    src = models.ForeignKey('Personne', related_name='src')
    dst = models.ForeignKey('Personne', related_name='dst')
    opposite = models.ForeignKey('PersonneRelation',
                                 null=True, blank=True, default=None)
    is_reverse = models.BooleanField(default=False)

    def __str__(self):
        return _(u'n.{} {} --> {}').format(
                str(self.pk),
                self.TAB_TYPES[self.type_relation] if not self.is_reverse
                else self.TAB_TYPES_REVERSE[self.type_relation],
                str(self.dst))

    class Meta:
        verbose_name = _(u'Relation')
        verbose_name_plural = _(u'Relations')

@receiver(post_save, sender=PersonneRelation)
def signal_receiver(sender, **kwargs):
    created = kwargs['created']
    obj = kwargs['instance']
    if created and not obj.opposite:
        opposite = PersonneRelation(
            src=obj.dst, dst=obj.src, opposite=obj,
            type_relation=obj.type_relation, is_reverse=True)
        opposite.save()
        obj.opposite = opposite
        obj.save()
    elif not created and obj.type_relation != obj.opposite.type_relation:
        obj.opposite.type_relation = obj.type_relation
        obj.opposite.save()

Vous remarquerez que j’ai vraiment réindenté le code pour qu’il reste lisible ici !

J’espère qu’il vous servira !

Python : batteries included.
Django : La Plateforme de développement Web pour les perfectionnistes sous pression.

2 comments

Poster un commentaire

Vous devriez utiliser le HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>