Django : limit_choices_to pour présenter un sous-ensemble des données d'un modèle
Par Nicolas Steinmetz. mardi 29 septembre 2009, 09:54. Python - Django django filtre limit_choices_to modèle python | Lien permanent.
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:
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 :
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 :
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 :
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 :
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...
Commentaires
En parlant de perfs, tu devrais utiliser des int dans tes déclarations de tuples pour les choices et après utiliser un SmallIntegerField pour ça
Et avec ta conf, il se passe quoi en base ? Ça crée une table pour chaque M2M déclarée ?
Pour les M2M, oui cela crée une table par relation mais ce n'est pas lié au limit_choices_to ; que cherches-tu à me faire dire ? Que d'un point de vue DB il aurait mieux value faire une table url_prod / url_preprod / url_vabf ?
Pour les int+SmallIntegerField, je suis pas sur de comprendre ce à quoi je dois aboutir. Un petit exemple sous le coude ?
> que cherches-tu à me faire dire ?
Euh, juste la réponse, c'était une vraie question que je me posais
> Un petit exemple sous le coude ?
VABF = 1 PREPROD = 2 PROD = 3 ENVT_CHOICES = ( (VABF, 'VABF'), (PREPROD, 'PREPROD'), (PROD, 'PROD'), ) envt = models.SmallIntegerField("Environement", choices=ENVT_CHOICES, default=VABF)Et voilà, commentaire débloqué de l'antispam
Pour la modélisation / exemple : Ah oui comme ça - j'y avais pas pensé