IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Django Python Discussion :

Partie admin: désactiver le contrôle d'unicité [Python 3.X]


Sujet :

Django Python

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre prolifique
    Avatar de Sve@r
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Février 2006
    Messages
    12 870
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Oise (Picardie)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Février 2006
    Messages : 12 870
    Billets dans le blog
    1
    Par défaut Partie admin: désactiver le contrôle d'unicité
    Bonjour à tous. Je débute sur django (mais je me débrouille en Python).
    Mon souci: j'ai un modèle avec un champ déclaré unique. Toutefois j'ai surchargé la méthode save() pour que si on entre une valeur déjà existante, la valeur soit automatiquement changée. Ca marche parfaitement quand je l'appelle en test direct.

    Maintenant j'intègre mon modèle dans la partie admin via @admin.register. Et quand je veux tester, là ça ne marche pas. A ce qui semble, le formulaire détecte que la valeur que je rentre est déjà présente et refuse donc d'appeler le save de mon modèle.
    Et donc je me dis que peut-être il y aurait moyen de contourner. Soit en surchargeant la méthode appelée par le formulaire (sauf que je ne sais pas laquelle il s'agit), soit demander au module d'admin de ne pas contrôler ce champ.

    Voici le modèle
    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Chapitre(models.Model):
    	titre = models.CharField(max_length=50, null=False, blank=False, unique=True)
    	slug = models.SlugField(max_length=50, null=False, blank=False, unique=True)
    	order = models.PositiveSmallIntegerField(null=False, blank=True, unique=True)
    	description = models.TextField(null=True, blank=True, default=None)
     
    	def __str__(self): return "%s (%d)" % (self.titre, self.order)
     
    	def save(self, *args, **kwargs):
    		self.slug = slugify(self.titre)
     
    		# Vérifier si numéro d'ordre déjà pris
    		print("ici")
    		previous=Chapitre.objects.all()
    		final=len(previous)+1
    		for p in previous:
    			if p.order == self.order:
    				p.order=final
    				super(Chapitre, p).save(*args, **kwargs)
    				break
    			# if
    		else: self.order=final
    		super().save(*args, **kwargs)
    	# save()
    # class Chapitre

    Et son homologue d'admin
    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    @admin.register(models.Chapitre)
    class Chapitre(admin.ModelAdmin):
    	list_display = ('__str__', 'titre', 'order', 'description')
    	list_editable = ('titre', 'order', 'description')
    	fields = (('titre', 'order'), 'description')
    	ordering = ('order',)
    # class Chapitre

    Après peut-être que je pars dans un problèmeXY et dans ce cas merci de me recadrer. Mon idée c'est que si l'administrateur crée deux chapitres avec le même "order", le premier chapitre créé parte à la fin (son order devient la valeur la plus élevée) tandis que le second chapitre le remplace dans la liste. C'est le but de la boucle placée dans le save() permettant de trouver un order déjà existant et si c'est le cas, le changer avant d'enregistrer le chapitre en cours.

    Merci de votre attention.
    Mon Tutoriel sur la programmation «Python»
    Mon Tutoriel sur la programmation «Shell»
    Sinon il y en a pleins d'autres. N'oubliez pas non plus les différentes faq disponibles sur ce site
    Et on poste ses codes entre balises [code] et [/code]

  2. #2
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 752
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 752
    Par défaut
    Bienvenue dans le monde du web

    Le formulaire d'administration de Django bloque la sauvegarde si un champ unique est dupliqué, avant même d'exécuter la logique personnalisée de la méthode save() du modèle.

    L'idée est de déplacer ta logique de réorganisation du fichier models.py vers le fichier admin.py.

    Une solution que je propose consiste à intercepter la valeur pendant la validation du formulaire pour gérer le conflit, puis à effectuer la sauvegarde de manière sécurisée.

    Pour commencer on simplifie la méthode save, qui généralement ne devrait pas trop être manipulée autrement que par sa méthode standard... mais générer le slug est une excellente pratique !

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from django.db import models
    from django.utils.text import slugify
     
     
    class Chapitre(models.Model):
        titre = models.CharField(max_length=50, null=False, blank=False, unique=True)
        slug = models.SlugField(max_length=50, null=False, blank=False, unique=True)
        order = models.PositiveSmallIntegerField(null=False, blank=False, unique=True)
        description = models.TextField(null=True, blank=True, default=None)
     
     
        def __str__(self): return "%s (%d)" % (self.titre, self.order)
     
     
        def save(self, *args, **kwargs):
            if not self.slug or self.slug != slugify(self.titre):
                self.slug = slugify(self.titre)
     
            super().save(*args, **kwargs)
    Ensuite on va créer un formulaire personnalisé et surcharger la méthode de sauvegarde de l'admin.

    Dans admin.py,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    from django.contrib import admin
    from django.db import transaction, models
    from django import forms
    from .models import Chapitre
     
     
     
    class ChapitreAdminForm(forms.ModelForm):
        class Meta:
            model = Chapitre
            fields = '__all__'
     
     
        def clean(self):
            """
            Cette méthode est appelée pendant la validation du formulaire.
            C'est ici que nous détectons le conflit d'ordre sans bloquer la sauvegarde.
            """
            cleaned_data = super().clean()
            order = cleaned_data.get('order')
     
     
            if order is not None:
                # On cherche si un AUTRE chapitre utilise déjà ce numéro d'ordre.
                query = Chapitre.objects.filter(order=order)
     
                # Si on modifie un chapitre existant, il faut l'exclure de la recherche
                if self.instance and self.instance.pk:
                    query = query.exclude(pk=self.instance.pk)
     
                chapitre_conflictuel = query.first()
     
                if chapitre_conflictuel:
                    # Conflit trouvé ! Au lieu de retourner une erreur,
                    # Il sera traité plus tard dans ModelAdmin.save_model().
                    self.displaced_chapitre = chapitre_conflictuel
     
            return cleaned_data
     
     
     
    @admin.register(Chapitre)
    class ChapitreAdmin(admin.ModelAdmin):
        # On dit à l'admin d'utiliser notre formulaire personnalisé.
        form = ChapitreAdminForm
     
        list_display = ('__str__', 'titre', 'order', 'description')
        list_editable = ('titre', 'order', 'description')
        fields = (('titre', 'order'), 'description')
        ordering = ('order',)
     
     
        def save_model(self, request, obj, form, change):
            """
            Cette méthode est appelée juste avant la sauvegarde.
            Elle orchestre les écritures en base de données de manière atomique et sécurisée.
            """
            # On utilise une transaction pour s'assurer que soit TOUT réussit,
            # soit TOUT est annulé.
            with transaction.atomic():
                # On vérifie si notre formulaire a trouvé un chapitre à déplacer.
                if hasattr(form, 'displaced_chapitre'):
                    displaced = form.displaced_chapitre
     
                    # On calcule le nouvel `order` le plus élevé.
                    max_order_result = self.model.objects.aggregate(max_order=models.Max('order'))
                    max_order = max_order_result['max_order'] or 0
     
                    # On déplace l'ancien chapitre à la fin.
                    displaced.order = max_order + 1
                    displaced.save()
     
     
                # On sauvegarde l'objet principal (celui que l'admin voulait créer/modifier).
                super().save_model(request, obj, form, change)
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  3. #3
    Membre prolifique
    Avatar de Sve@r
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Février 2006
    Messages
    12 870
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Oise (Picardie)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Février 2006
    Messages : 12 870
    Billets dans le blog
    1
    Par défaut
    Merci c'est super sympa. J'ai parfaitement tout compris
    1) tu hérites du formulaire qui permet de surcharger la fonction dédiée à sa vérification
    2) si la vérification trouve un souci sur le "order", tu gères le souci et tu positionne un attribut spécial dans le formulaire
    3) dans la fonction qui enregistre la saisie (save_model, c'est elle que je cherchais) tu récupères le formulaire te permettant ainsi de vérifier s'il y a eu souci (donc si attribut spécial) et de gérer

    Malheureusement ça ne fonctionne pas. J'ai recopié à l'identique et rajouté quelques print(). Le save_model est bien appelé mais pas le clean du formulaire.
    J'ai toutefois cherché avant de revenir pleurer (vu que je ne supporte pas ceux qui ne font pas d'effort...). Déjà mon sujet principal de préoccupation était ce nom "clean" (nettoyer) qui me paraissait incongru pour valider (oui désolé j'ai douté de toi quoi). J'ai donc récupéré les méthodes du ModelForm pour tenter de les surcharger afin de voir s'il n'y avait pas d'erreur et si une autre était appelée. Exemple il y a is_valid() qui me paraissait intéressant mais rien.
    Et puis j'ai trouvé ici https://www.geeksforgeeks.org/python...-using-django/ un exemple qui indique bien que c'est clean() qui valide et là je reste sec. On dirait que c'est form = ChapitreAdminForm qui n'est pas pris en considération. Je dis ça parce que quand je saisis des infos correctes sur la page d'admin puis les valide, je ne vois pas les print() que j'ai mis dans le clean(). Pourtant c'est exactement ainsi que c'est décrit ici https://docs.djangoproject.com/fr/5.2/ref/contrib/admin.
    Et là je me retrouve de nouveau dans une impasse.

    Merci toutefois de tes informations, j'ai appris plein de trucs.
    Mon Tutoriel sur la programmation «Python»
    Mon Tutoriel sur la programmation «Shell»
    Sinon il y en a pleins d'autres. N'oubliez pas non plus les différentes faq disponibles sur ce site
    Et on poste ses codes entre balises [code] et [/code]

  4. #4
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 752
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 752
    Par défaut
    Ah ok,

    Est-ce que le retrait de 'order' dans l'attribut list_editable change quelque chose ?
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  5. #5
    Membre prolifique
    Avatar de Sve@r
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Février 2006
    Messages
    12 870
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Oise (Picardie)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Février 2006
    Messages : 12 870
    Billets dans le blog
    1
    Par défaut
    Citation Envoyé par fred1599 Voir le message
    Est-ce que le retrait de 'order' dans l'attribut list_editable change quelque chose ?
    Je n'y ai alors plus accès et ne peux plus le modifier. C'est une solution aussi, plus réductrice mais qui évite les soucis. Et le clean() reste inutilisé.

    J'ai aussi tenté de mettre 'order' dans exclude (comme je le vois sur la page mentionnée précédemment) espérant qu'il ne serait pas vérifié.
    Je vais essayer les exemples que je vois écrits sur cette page https://docs.djangoproject.com/fr/5....contrib/admin/. Je dois forcément rater un truc...
    Mon Tutoriel sur la programmation «Python»
    Mon Tutoriel sur la programmation «Shell»
    Sinon il y en a pleins d'autres. N'oubliez pas non plus les différentes faq disponibles sur ce site
    Et on poste ses codes entre balises [code] et [/code]

  6. #6
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 752
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 752
    Par défaut
    Tu as deux modes d'édition,
    1. rapide
    2. complète


    La solution n'est donc pas de rendre le champ non-modifiable, mais de forcer l'utilisateur à passer par le mode d'édition complet pour des opérations complexes.

    Dans l'ordre,
    • affiche les chapitres, tu peux faire les modifs que tu veux mais pas changer l'ordre
    • clique sur le titre d'un chapitre
    • tu arrives normalement sur la page de détails de ton chapitre
    • là dans le champ order, tu modifies pour une autre valeur
    • puis tu sauvegardes les changements


    Après la sauvegarde tu devrais voir tes print de la méthode clean
    Normalement tu devrais voir les changements au niveau des ordres
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  7. #7
    Membre prolifique
    Avatar de Sve@r
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Février 2006
    Messages
    12 870
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Oise (Picardie)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Février 2006
    Messages : 12 870
    Billets dans le blog
    1
    Par défaut
    D'accord j'ai compris . Je me contentais de modifier mes chapitres dans la page principale, celle qui affiche tous les chapitres en liste. Je pensais que le formulaire se passait depuis cette page.
    En effet, en cliquant sur le nom du chapitre j'arrive dans l'édition détaillée d'un chapitre et là quand je valide, je vois le clean() s'exécuter.
    Merci tu as été super sympa

    Est-ce possible éventuellement de gérer aussi la validation de la page principale ? Sinon c'est pas grave, je mets le order en non modifiable et ça se fera depuis le mode complet. De toute façon à partir d'ici c'est résolu.
    Merci de ta patience
    Mon Tutoriel sur la programmation «Python»
    Mon Tutoriel sur la programmation «Shell»
    Sinon il y en a pleins d'autres. N'oubliez pas non plus les différentes faq disponibles sur ce site
    Et on poste ses codes entre balises [code] et [/code]

  8. #8
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 752
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 752
    Par défaut
    Oui tout est possible, mais c'est assez technique

    Déjà remet 'order' dans list_editable

    et ajoute cette méthode dans ChapitreAdmin

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    def save_formset(self, request, form, formset, change):        
        with transaction.atomic():
                changed_forms = [f for f in formset.forms if f.has_changed()]
     
                if not changed_forms:
                    super().save_formset(request, form, formset, change)
                    return
     
                edited_pks = {f.instance.pk for f in changed_forms}
                target_orders = {f.cleaned_data['order'] for f in changed_forms if 'order' in f.cleaned_data}
     
                conflicts = self.model.objects.filter(
                    order__in=target_orders
                ).exclude(
                    pk__in=edited_pks
                )
     
                if conflicts.exists():
                    max_order_result = self.model.objects.aggregate(max_order=models.Max('order'))
                    max_order = max_order_result['max_order'] or 0
     
                    for i, conflict_obj in enumerate(conflicts):
                        conflict_obj.order = max_order + 1 + i
                        conflict_obj.save()
     
                super().save_formset(request, form, formset, change)
    Je t'amène vers la doc -> https://docs.djangoproject.com/fr/5....n.save_formset
    Pourquoi j'utilise save_formset ? Parce-que list_editable, je te laisse regarder la doc pour faire la liaison et pourquoi je fais cela -> https://docs.djangoproject.com/fr/5....orms/formsets/
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  9. #9
    Membre prolifique
    Avatar de Sve@r
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Février 2006
    Messages
    12 870
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Oise (Picardie)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Février 2006
    Messages : 12 870
    Billets dans le blog
    1
    Par défaut
    Super sympa de m'avoir mis le pied à l'étrier. Grace à toi j'ai découvert plein de trucs. J'avais le même souci sur une autre table dont certains champs unique devaient être gérés par le formulaire en automatique, et aussi un champ order qui lui serait géré par mon "réorganisateur". J'ai donc surchargé la méthode validate_unique() du modèle pour qu'elle laisse passer le "order". Et ça marche tiptop.
    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    # Validation personnalisée du modèle: le champ "order" ne compte pas
    def validate_unique(self, *args, **kwargs):
    	try: super().validate_unique(*args, **kwargs)
    	except ValidationError as e:
    		if any(filter(lambda x: x != "order", e.error_dict.keys())): raise e
    # validate_unique()
    J'ai eu un peu de mal à comprendre son fonctionnement (je pensais qu'elle renvoyait une information alors qu'en réalité elle crée une exception ; et ensuite dans l'exception il m'a fallu trouver où étaient stockées les causes de l'erreur mais enfin j'ai fini par y arriver). Et le "transaction.atomic()" que tu m'as montré est proprement excellent (même si c'est élémentaire pour quiconque fait des modifs en base je n'aurais jamais songé que django offrait cette possibilité).
    Enfin merci encore.
    Mon Tutoriel sur la programmation «Python»
    Mon Tutoriel sur la programmation «Shell»
    Sinon il y en a pleins d'autres. N'oubliez pas non plus les différentes faq disponibles sur ce site
    Et on poste ses codes entre balises [code] et [/code]

  10. #10
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 752
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 752
    Par défaut
    C'est une solution, pas là plus propre, pas là plus sûre, mais fonctionnelle, jusqu'à...

    Gérer le conflit d'unicité de manière proactive en déplaçant les autres éléments, plutôt que de réagir à l'erreur en l'ignorant aurait été préférable tant conceptuellement que sur la sécurité des données.

    Et puis tu travailles avec python, là où je pense que travailler directement en base de données est bien plus efficace.

    Pour le moment c'est fonctionnel, tant que tu n'as pas de problème, c'est que le reste du code prévoit ce qu'il faut pour ne pas en avoir
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Conception partie Admin
    Par pruderic dans le forum UML
    Réponses: 3
    Dernier message: 26/05/2008, 11h23
  2. Aide pour partie ADMIN
    Par Mom's dans le forum Langage
    Réponses: 1
    Dernier message: 04/05/2007, 22h36
  3. [Sécurité] sécuriser ma partie admin
    Par dedel53 dans le forum Langage
    Réponses: 1
    Dernier message: 13/03/2007, 19h38
  4. [CKEditor] pb d'installation fckeditor dans ma partie admin
    Par dedel53 dans le forum Bibliothèques & Frameworks
    Réponses: 2
    Dernier message: 05/03/2007, 14h28

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo