web-dev-qa-db-fra.com

Créer une classe de formulaire Django avec un nombre dynamique de champs

Je travaille sur quelque chose comme un magasin en ligne. Je crée un formulaire dans lequel la cliente achète un article et elle peut choisir le nombre d'articles qu'elle souhaite acheter. Mais, sur chaque article qu'elle achète, elle doit choisir sa couleur. Il y a donc un nombre non constant de champs: si le client achète 3 articles, il doit obtenir 3 cases <select> pour choisir une couleur; s'il en achète 7, il doit en obtenir 7 telles <select>.

Je ferai apparaître et disparaître les champs du formulaire HTML à l'aide de JavaScript. Mais comment puis-je gérer cela dans ma classe de formulaires Django? Je vois que les champs de formulaire sont des attributs de classe, alors je ne sais pas comment gérer le fait qu’une instance de formulaire devrait avoir 3 champs de couleur et d’autres 7.

Un indice?

46
Ram Rachum

Jacob Kaplan-Moss a beaucoup écrit sur les champs de formulaire dynamiques: http://jacobian.org/writing/dynamic-form-generation/

Essentiellement, vous ajoutez d'autres éléments au dictionnaire self.fields du formulaire lors de l'instanciation.

66
GDorn

Voici une autre option: que diriez-vous d’un formset ? Puisque vos champs sont tous identiques, c’est précisément à quoi servent les formulaires.

L’administrateur Django utilise FormSets + un peu de javascript pour ajouter des inlines de longueur arbitraire.

class ColorForm(forms.Form):
    color = forms.ChoiceField(choices=(('blue', 'Blue'), ('red', 'Red')))

ColorFormSet = formset_factory(ColorForm, extra=0) 
# we'll dynamically create the elements, no need for any forms

def myview(request):
    if request.method == "POST":
        formset = ColorFormSet(request.POST)
        for form in formset.forms:
            print "You've picked {0}".format(form.cleaned_data['color'])
    else:
        formset = ColorFormSet()
    return render(request, 'template', {'formset': formset}))

JavaScript

    <script>
        $(function() {
            // this is on click event just to demo.
            // You would probably run this at page load or quantity change.
            $("#generate_forms").click(function() {
                // update total form count
                quantity = $("[name=quantity]").val();
                $("[name=form-TOTAL_FORMS]").val(quantity);  

                // copy the template and replace prefixes with the correct index
                for (i=0;i<quantity;i++) {
                    // Note: Must use global replace here
                    html = $("#form_template").clone().html().replace(/__prefix_/g', i);
                    $("#forms").append(html);
                };
            })
        })
    </script>

Modèle

    <form method="post">
        {{ formset.management_form }}
        <div style="display:none;" id="form_template">
            {{ formset.empty_form.as_p }}
        </div><!-- stores empty form for javascript -->
        <div id="forms"></div><!-- where the generated forms go -->
    </form>
    <input type="text" name="quantity" value="6" />
    <input type="submit" id="generate_forms" value="Generate Forms" />
30

tu peux le faire comme

 def __init __ (self, n, * arguments, ** kwargs): 
 super (your_form, self) .__ init __ (* arguments, ** kwargs) 
 pour i dans la plage (0, n): 
 self.fields ["nom de champ% d"% i] = forms.CharField () 

et lorsque vous créez une instance de formulaire, vous ne faites que

 forms = your_form (n) 

c'est juste l'idée de base, vous pouvez changer le code comme bon vous semble. :RÉ

16
owenwater

La façon dont je le ferais est la suivante:

  1. Créez une classe "vide" qui hérite de froms.Form, comme ceci:

    class ItemsForm(forms.Form):
        pass
    
  2. Construisez un dictionnaire des objets de formulaires, qui sont les formulaires réels, dont la composition dépend du contexte (vous pouvez par exemple les importer à partir d'un module externe). Par exemple:

    new_fields = {
        'milk'  : forms.IntegerField(),
        'butter': forms.IntegerField(),
        'honey' : forms.IntegerField(),
        'eggs'  : forms.IntegerField()}
    
  3. Dans les vues, vous pouvez utiliser la fonction "type" native de python pour générer dynamiquement une classe Form avec un nombre variable de champs.

    DynamicItemsForm = type('DynamicItemsForm', (ItemsForm,), new_fields)
    
  4. Passez le contenu au formulaire et rendez-le dans le modèle:

    Form = DynamicItemsForm(content)
    context['my_form'] = Form
    return render(request, "demo/dynamic.html", context)
    

Le "contenu" est un dictionnaire de valeurs de champs (par exemple, même request.POST suffirait) . Vous pouvez voir tout mon exemple expliqué ici .

2
OZ13

Une autre approche: Plutôt que d'interrompre le flux normal d'initialisation de champ, nous pouvons remplacer les champs par un mixin, renvoyer un OrderedDict de champs dynamiques dans generate_dynamic_fields, qui sera ajouté à chaque fois qu'il est défini.

from collections import OrderedDict

class DynamicFormMixin:
    _fields: OrderedDict = None

    @property
    def fields(self):
      return self._fields

    @fields.setter
    def fields(self, value):
        self._fields = value
        self._fields.update(self.generate_dynamic_fields())

    def generate_dynamic_fields(self):
        return OrderedDict()

Un exemple simple:

class ExampleForm(DynamicFormMixin, forms.Form):
    instance = None

    def __init__(self, instance = None, data=None, files=None, auto_id='id_%s', prefix=None, initial=None,
                 error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None,
                 use_required_attribute=None, renderer=None):
        self.instance = instance
        super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order,
                         use_required_attribute, renderer)

    def generate_dynamic_fields(self):
        dynamic_fields = OrderedDict()
        instance = self.instance
        dynamic_fields["dynamic_choices"] = forms.ChoiceField(label=_("Number of choices"),
                                                              choices=[(str(x), str(x)) for x in range(1, instance.number_of_choices + 1)],
                                                              initial=instance.initial_choice)
        return dynamic_fields
0
bob