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.

Queryset trop important dans un formulaire (django)

+2 votes

Dans mon forms.py (django) j'ai un champs ville (foreignkey) contenant toutes les villes de France, soit environ 36000 !
Lorsque j'appelle le template de ce formulaire forcément ça rame.
J'ai donc essayé de mettre le queryset de cette façon --> queryset=Ville.objects.none(). Pour pouvoir afficher les villes j'utilise ensuite REST en sélectionnant un département. Le problème c'est que lorsque je valide le formulaire, j'ai une erreur sur le champs ville m'informant que ce choix ne fait pas partie de ceux disponibles (alors que je lui ai bien rentré une ville, si si !!!).
Le code actuel de mon champs ville dans forms.py :

ville = forms.ModelChoiceField(
    label="Ville *",
    queryset=Ville.objects.all(),
    widget = forms.Select(
        attrs={'required':'True', 'class':'form-control', 'id':'villes', 'disabled':'true'}
        ),
    required=True,
    )

Ma question : Comment dois-je définir le queryset de 'ville' pour que ça ne rame pas tout en ayant la possibilité de rentrer une ville (sachant qu'avec none ça ne passe pas) ?

demandé 18-Fev-2015 par ronan (184 points)

2 Réponses

+3 votes
 
Meilleure réponse

Rempli ton drop down uniquement avec les 100 villes les plus importantes. Ensuite avec Javascript et un widget de ce type http://ivaynberg.github.io/select2/, fait de la recherche ajax au fur et à mesure que l'utilisateur tape les lettres de la ville pour lui permettre de rajouter les villes moins connues si il en a besoin.

L'autre solution, c'est de faire une page pre_formulaire ou la personne choisi une ville depuis un listing ou une carte (type la page d'acceuil le bon coin). De là, tu passe l'ID quand il clique sur le lien pour aller vers ton formulaire, et tu en fais un hidden text field avec.

répondu 30-Mar-2015 par Sam (4,974 points)
sélectionné 31-Mar-2015 par ronan

Yes, c'est ce que j'ai fini par faire.

Pour ma part j'ai pris une variante de la seconde solution car certains noms de villes ont des noms trop proches (villes commençant par saint par exemple). En proposant une liste je limite le nombre d'erreurs selon moi.

J'ai donc créé un champs hidden pour la ville. Dans mon formulaire, j'ai un champs département, lorsque je clique sur un département, une fonction ajax est appelée et me renvoie toutes les villes du département dans une liste. Puis quand je clique sur une ville, je transfère son id sur le champs hidden ville (avec Jquery). Et ça marche nikel !

Pour ceux et celles que ça intéressent :

forms.py

ville = forms.CharField(
    label="Ville *",
    widget = forms.HiddenInput(
        attrs={'required':'True', 'class':'form-control', 'id':'id_ville'}
        ),
    required=True,
    )

le template

<div class="form-group">
                <label class="control-label col-md-4" for="input01">Département</label>
                <div class="controls col-md-8">
                    <div class="input-group">
                    <span class="input-group-addon" id="sizing-addon8"><i class="fa fa-globe couleurbleue"></i></span>
                    <select class="form-control" name="departement" id="choix_departement" required="true" aria-describedby="sizing-addon8">
                        <option disabled selected hidden>Choisissez un département</option>
                        {% for unDepartement in mesDepartements %}
                            <option value="{{unDepartement.0}}">{{unDepartement.1}}</option>
                        {% endfor %}
                    </select>
                    <!-- <p class="help-block">{{form.identifiant.help_text}}</p> -->
                    </div>
                </div>
                </div>



                <div class="form-group">
                <label class="control-label col-md-4" for="input01">{{form.ville.label}}</label>
                <div class="controls col-md-8">
                <div class="input-group">
                    <span class="input-group-addon" id="sizing-addon3"><i class="fa fa-road couleurbleue"></i></span>
                    <select aria-describedby="sizing-addon4" class="form-control" disabled="true" id="villes" name="ville_choix" required="True">
                        <option value="" selected="selected">Choisissez une ville</option>
                    </select>


                </div>
                {% if form.ville.errors %}
                    <div class="alert alert-info alert-dismissible" style="padding:0;margin-top:5px">{{form.ville.errors}}</div>
                {% endif %}
                </div></div>

                {{form.ville}}

Le javascript

$(document).ready(function(){ 


        var $departement = $('#choix_departement');
        var $villes = $('#villes');
        var $clubs = $('#clubs');
        $clubs.empty();





        $departement.change(function(){

            var lien = "/propositions-ville/";
            var numero_departement = $departement.val();
            // Création du lien de l'API de récupération de proposition de ville
            lien = lien.concat(numero_departement);
            $villes.empty();


            // Récupération des villes et remplissage du select des villes
            $.getJSON(lien, function(data) {
                $villes.append('<option>Choisissez une ville</option>');
                $.each(data, function (i, ville) {

                    $villes.append('<option value="'+ ville.pk+'">'+ville.fields.libelle+'</option>');
                });

            });

            $villes.prop("disabled", false);
        });

        $villes.change(function(){

            var lien = "/propositions-club/";
            var numero_ville = $villes.val();
            // Création du lien de l'API de récupération de proposition de ville

            // On affecte à l'input hidden la valeur texte de la ville
            $('#id_ville').val(numero_ville);

            // On change le club

            $('#id_club').val('');


            lien = lien.concat(numero_ville);
            $clubs.empty();


            // Récupération des villes et remplissage du select des villes
            $.getJSON(lien, function(data) {

                //$clubs.append("<option selected=selected>Aucun club ou club non référencé</option>");

                $.each(data, function (i, club) {
                    $clubs.append('<option required="false" value="'+ club.pk+'">'+club.fields.libelle+'</option>');
                });

                $clubs.append('<option value="" selected="selected">---------</option>');

            });

            $clubs.prop("disabled", false);
        });


        $clubs.change(function(){

            var numero_club = $clubs.val();
            $('#id_club').val(numero_club);

        });
    });
+2 votes

D'après Google et la doc Django tu peux essayer

iterator()

mais ca reste assez lourd niveau DB (pas de caching, comme l'a gentillement signalé foxmask). Sinon j'ai trouvé cet article qui link vers ce snippet
qui semblerait faire ce que tu souhaites
.

répondu 18-Fev-2015 par c0da (2,746 points)
edité 18-Fev-2015 par c0da

La doc dit bien que c'est pour du one shot , récupération de données une fois et basta, pas besoin du caching db

(Sorry pour pour le signalement, je voulais commenter et non pas signaler...)

Dans le cas de ronan ça dépend effectivement du nombre de queries qu'il compte faire, a lui de voir si il a besoin de caching ou pas. Le snippet a l'air d'être plus intéressent car c'est une solution vraiment viable.

Je plussois ;)

Merci pour vos réponses.

En utilisant la snippet, j'ai donc modifié le champs 'ville' du forms.py comme ci-dessous :

ville = forms.ModelChoiceField(
    label="Ville *",
    queryset=queryset_iterator(Ville.objects.all()),
    widget = forms.Select(
        attrs={'required':'True', 'class':'form-control', 'id':'villes', 'disabled':'true'}
        ),
    required=True,
    )

Et j'obtiens l'erreur suivante :

'generator' object has no attribute 'all' (l'erreur pointe sur {{form.ville}} dans le template). J'ai dû oublié un truc ...

pour un ModelChoiceField je gère comme ça :

def __init__(self, *args, **kwargs):
     super(UserServiceForm, self).__init__(*args, **kwargs)
     self.fields['name'].choices = self.activated_services(self.initial['user'])

activated_services est une fonction qui me retourne une liste

"dans la vraie vie" le code est là

adapter à tes models ça devrait donner qqchose comme :

class AdresseForm(forms.ModelForm):

    def queryset_iterator(self, queryset, chunksize=1000):
        """le code du snipset"""

    def communes(self):
        return queryset_iterator(Ville.objects.all())

     def __init__(self, *args, **kwargs):
         super(AdresseForm, self).__init__(*args, **kwargs)
         self.fields['ville'].choices = self.communes()

Dans mon forms.py, j'ai donc

def queryset_iterator(self, queryset, chunksize=1000):
    '''
    Iterate over a Django Queryset ordered by the primary key

    This method loads a maximum of chunksize (default: 1000) rows in it's
    memory at the same time while django normally would load all rows in it's
    memory. Using the iterator() method only causes it to not preload all the
    classes.

    Note that the implementation of the iterator does not support ordered query sets.
    '''
    pk = 0
    last_pk = queryset.order_by('-pk')[0].pk
    queryset = queryset.order_by('pk')
    while pk < last_pk:
        for row in queryset.filter(pk__gt=pk)[:chunksize]:
            pk = row.pk
            yield row
        gc.collect()

def communes(self):
    return self.queryset_iterator(Ville.objects.all())

def __init__(self, *args, **kwargs):
    super(AjouterClubForm, self).__init__(*args, **kwargs)
    self.fields['ville'].choices = self.communes()

Dans mon template, j'avais :

<div class="form-group">
                <label class="control-label col-md-4" for="input01">{{form.ville.label}}</label>
                <div class="controls col-md-8">
                    <div class="input-group">
                    <span class="input-group-addon" id="sizing-addon6"><i class="fa fa-road couleurbleue"></i></span>

                    {{form.ville}}

                    </div>
                    {% if form.ville.errors %}
                    <div class="alert alert-info alert-dismissible" style="padding:0;margin-top:5px">{{form.ville.errors}}</div>
                    {% endif %}
                    <p class="help-block">{{form.ville.help_text}}</p>
                </div>
                </div>

Ca plante au niveau de {{form.ville}} dans le template, et l'erreur est : Ville 'object' is not iterable.
J'ai changé le template tel que ci-dessous (en remplaçant {{form.ville}} par un code en dur :

<div class="form-group">
                <label class="control-label col-md-4" for="input01">{{form.ville.label}}</label>
                <div class="controls col-md-8">
                    <div class="input-group">
                    <span class="input-group-addon" id="sizing-addon6"><i class="fa fa-road couleurbleue"></i></span>


                    <select id="villes" name="ville" required class="form-control" disabled><option disabled selected hidden>Choisissez une ville</option></select>

                    </div>
                    {% if form.ville.errors %}
                    <div class="alert alert-info alert-dismissible" style="padding:0;margin-top:5px">{{form.ville.errors}}</div>
                    {% endif %}
                    <p class="help-block">{{form.ville.help_text}}</p>
                </div>
                </div>

Et là ça fonctionne et je peux enregistrer la ville que je souhaite dans le formulaire (à l'aide de REST), seulement le temps de chargement est toujours aussi long (c'est-à-dire un peu plus de 20 secondes de chargement, et autant lorsque le formulaire comporte des erreurs)

...