Dans le cadre des relations "One to many" ou "Many to Many", il est possible de présenter un sous-ensemble de votre modèle en fonction de critères de tri.

Codant une application de gestion de reverse proxys pour les sites web Vélos en libre service, il me faut gérer différents types d'url (une url publique, une url pour les abonnements, une url d'admin, les déclinaisons linguistiques et autres alias en tous genre) et ce pour chaque type d'environnement (VABF, PREPROD, PROD). J'aurais pu créer autant de modèles que j'ai de type d'url mais cela aurait été particulièrement contre-productif. J'ai donc un modèle unique avec les paramètres "type d'url" et "environnement". Dès lors, quand je définis les urls d'une ville, il me suffit de faire des relations "One to Many" ou "Many to many" vers mon objet Url.

Cela donne grosso modo la chose suivante pour l'objet Url:

 python
class Url(models.Model):
    """
    Url description
    """
    ENVT_CHOICES = (
        ('1', 'VABF'),
        ('2', 'PREPROD'),
        ('3', 'PROD'),
    )
    URL_CHOICES = (
        ('1', 'www'),
        ('2', 'subscription'),
        ('3', 'admin'),
        ('4', 'secondary'),
        ('5', 'translation'),        
    )
    name = models.CharField("DNS", help_text="Provide url of the site without http(s)://", max_length=100)
    description = models.CharField(help_text="You can provide extra information on the url", blank=True, max_length=100)
    url = models.URLField("URL", help_text="Provide url of the site *with* http(s)://", verify_exists=False)
    urltype = models.CharField("URL Type", help_text="What kind of url it is ?", max_length=20, choices=URL_CHOICES)
    envt = models.CharField("Environement", help_text="In which environment is used this url ?", max_length=100, choices=ENVT_CHOICES)
    
    def __unicode__(self):
        return self.name

Dans un premier temps, la classe Town est assez simple à monter :

 python
class Town(models.Model):
    """
    Town
    """
    ENVT_CHOICES = (
        ('1', 'VABF'),
        ('2', 'PREPROD'),
        ('3', 'PROD'),
    )
    STATE_CHOICES = (
        ('1', 'On construction'),
        ('2', 'On maintenance'),
        ('3', 'Open'),
        ('4', 'Closed'),
    )
    name = models.CharField(help_text="Provide the name of the town", max_length=100)
    description = models.CharField(help_text="You can provide extra information on the town", blank=True, max_length=100)
    prod_status = models.CharField("PROD status", help_text="State of the site on PROD environment", max_length=20, choices=STATE_CHOICES)
    prod_www = models.ForeignKey(Url, verbose_name="WWW Public URL", help_text="Public url of the site like www.velib.paris.fr", related_name="prod_www")
    prod_abo = models.ForeignKey(Url, verbose_name="Subscription URL", help_text="HTTPS URL used for all sensible process like subscribption, my account, etc. Is often like https://abo-<town>.cyclocity.fr", related_name="prod_abo",)
    prod_admin = models.ManyToManyField(Url, verbose_name="Internal administration URL(s)", help_text="URL used to access back-office", related_name="prod_admin") 
    prod_other = models.ManyToManyField(Url, verbose_name="Other URL(s)", help_text="Other urls related to the site like aliases related to the public url", related_name="prod_other", blank=True)
    prod_translation = models.ManyToManyField(Url, verbose_name="Translations URL", help_text="If site has translated versions, their public urls are listed here" ,related_name="prod_translation", blank=True)    

Cette modélisation, bien que remplissant le besoin a un désavantage certain : tous les objets de type Url sont présentés. Si on part sur 17 villes * 3 environnements * 3 urls "publiques" à minima, soit pas moins de 153 urls disponibles. Pas simple de s'y retrouver (et encore je vous dis pas tout, il y en a bien plus en fait ;-) )

Il faut alors être en mesure pour l'attribut prod_www de ne présenter que les urls de type "www" et pour l'environnement "prod".

Cela est possible très simplement grâce au filtre limit_choices_to qui adopte la syntaxe suivante :

 python
class Example(models.Model):
    foo = models.ForeignKey(<Modèle>, limit_choices_to = {'<attribut du modèle en question>': "<valeur>"}

Ce qui dans mon cas donne pour filtrer sur le type d'url et l'environnement :

 python
class Town(models.Model):
    """
    Town
    """
    ENVT_CHOICES = (
        ('1', 'VABF'),
        ('2', 'PREPROD'),
        ('3', 'PROD'),
    )
    STATE_CHOICES = (
        ('1', 'On construction'),
        ('2', 'On maintenance'),
        ('3', 'Open'),
        ('4', 'Closed'),
    )
    name = models.CharField(help_text="Provide the name of the town", max_length=100)
    description = models.CharField(help_text="You can provide extra information on the town", blank=True, max_length=100)
    prod_status = models.CharField("PROD status", help_text="State of the site on PROD environment", max_length=20, choices=STATE_CHOICES)
    prod_www = models.ForeignKey(Url, verbose_name="WWW Public URL", help_text="Public url of the site like www.velib.paris.fr", related_name="prod_www", limit_choices_to = {'urltype': 1, 'envt': 3})
    prod_abo = models.ForeignKey(Url, verbose_name="Subscription URL", help_text="HTTPS URL used for all sensible process like subscribption, my account, etc. Is often like https://abo-<town>.cyclocity.fr", related_name="prod_abo", limit_choices_to = {'urltype': 2, 'envt': 3})
    prod_admin = models.ManyToManyField(Url, verbose_name="Internal administration URL(s)", help_text="URL used to access back-office", related_name="prod_admin", limit_choices_to = {'urltype': 3, 'envt': 3} ) 
    prod_other = models.ManyToManyField(Url, verbose_name="Other URL(s)", help_text="Other urls related to the site like aliases related to the public url", related_name="prod_other", limit_choices_to = {'urltype': 4, 'envt': 3}, blank=True)
    prod_translation = models.ManyToManyField(Url, verbose_name="Translations URL", help_text="If site has translated versions, their public urls are listed here" ,related_name="prod_translation", limit_choices_to = {'urltype': 5, 'envt': 3}, blank=True)    

Comme le montre l'exemple de la documentation, ce filtre marche aussi de façon dynamique ou bien avec des objets Q pour les requêtes complexes :

 python
    limit_choices_to = {'pub_date__lte': datetime.now}

Du coup, le formulaire de création/édition d'une ville ne me présente plus qu'un nombre réduit d'urls pour un champ donné. Au pire, ma liste de choix aura un maximum d'éléments correspondant au nombre de villes déjà entrées dans le système. Il me faudrait affiner le filtre pour en plus ne présenter que les urls non déjà liées à une autre ville. Mais c'est moins évident et immédiat ;-)

En espérant que cela puisse être utile à d'autres...