pythondjangodjango-formwizard

Django Wizard Form : is it possible to have a search engine for a specific step?


I'm struggling with my Django application, to the point of asking my first question on StackOverflow.

To be short, I have a form where the user (a farmer) allows him to add a plant on a culture.

It'd be handy if instead of a boring select box, the farmer could just write down a few letters and every related results pop on the screen. The farmer would pick-up the plant and proceed to the next step. Since he had 330 different seeds, it's not just a fancy functionality.

I'm able to build a "simple" WizardForm, I already have the search engine and my field is populated with a ModelChoiceField()... I feel like I'm so close yet so far :(

I have also considered that WizardForm might not be the right approach for what I'm doing. But I feel like I'm just missing something.

Do any of you have any suggestion on it?

Below, you can read a few extracts from my code. I will try to clean-up the mess and provide you a readable code.

models.py

'''
From the model, the only field that interests this question is the second one, id_graine (graine means seed).
'''

class Lot(models.Model):
    id = models.AutoField(primary_key = True)
    id_graine = models.ForeignKey('Graine', on_delete = models.PROTECT, related_name = 'id_graine', null = True, blank = True)
    nom_culture_incubation = models.ForeignKey('Culture', on_delete = models.PROTECT, related_name = 'nom_culture_incubation', null = True, blank = True)
    nom_culture = models.ForeignKey('Culture', on_delete = models.PROTECT, related_name = 'nom_culture', null = True, blank = True)
    etat_lot = models.CharField('état', choices = EtatLotsChoix.choices, max_length = 50, null = True, blank = True)
    quantite_lot = models.PositiveSmallIntegerField('quantité', null = True, blank = True)
    semis_date = models.DateField('date de semis', null = True, blank = True)
    phase_lunaire_semis = models.CharField('phase lunaire de semis', max_length = 4, blank = True)
    constellation_semis = models.CharField('constellations de semis', max_length = 7, blank = True)
    germination_date = models.DateField('date de germination', null = True, blank = True)
    plantaison_date = models.DateField('date de plantaison', null = True, blank = True)
    phase_lunaire_plantation = models.CharField('phase lunaire de plantation', choices = PhasesLunairesChoix.choices, max_length = 4, blank = True)
    constellation_plantation = models.CharField('constellations de plantation', choices = ConstellationChoix.choices, max_length = 7, blank = True)
    culture_introduction_date = models.DateField('date d\'introduction', null = True, blank = True)
    floraison_date = models.DateField('date de floraison', null = True, blank = True)
    recolte_date = models.DateField('date de récolte', null = True, blank = True)

    def __int__(self):
        return self.id

    class Meta:
        verbose_name = 'Lot'
        verbose_name_plural = 'Lots'
views.py

class AjoutLotWizard(SessionWizardView):
    template_name = 'wizardforms/ajout_lot_seme.html'

    def get_context_data(self, form, **kwargs):

        '''
        I told you that I was able to provide choices through a ModelChoiceField. But this approach 
        didn't pay off well. I also had a successful attempt by providing a context to the template
        with those informations.
        '''

        if self.request.GET.get('search_value') != None:

            search_value = self.request.GET.get('search_value')

            search_results = Graine.objects.filter(espece_graine__contains = search_value)

            context = super(AjoutLotWizard, self).get_context_data(form = form, **kwargs)

            if (self.steps.current == '0') & (self.request.GET.get('search_value') != None):
                context.update({'search_results': search_results})

                return context
            else:

                return context

Within views.py, I also tried to pass data with get_form_kwargs(), get_form_step_data(), overwrite get() with a custom attribute.

The approach with get_form_kwargs was promising once I was able to store my values in variables inside unfortunately, I couldn't manipulate them as I wanted within the WizardView.

forms.py

class AddLotSemeStep1(forms.ModelForm):

    '''
    This file might not be accurate. I did a mess on that part by rewriting __init__().
    But from what I remember, there wasn't more within the form when I use get_context_data
    to provide choices anyway with the template.
    '''

    class Meta:
        model = Lot
        fields = ['id_graine']
        labels = {
            'id_graine': 'Graine'
        }
        widgets = {
            'id_graine': forms.RadioSelect()
        }
template.html

{% load static %}
{% load i18n %}
{% block content %}
    {% include 'components/_base.html' %}
    <main class="wizard-form-main">
        <h2>Ajout de lot semé - le 1</h2>
        <p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>

        <form action="" method="GET" name="searchbar">
            <input type="text" name="search_value">
            <button type="submit">Rechercher</button>
        </form>

        <form action = "" method = "POST" name="wizardform">
        {% csrf_token %}
            <table>
                {{ wizard.management_form }}
                {% if wizard.form %}
                    {{ wizard.form.management_form }}
                    {{ wizard.form }}
                {% else %}
                    {% for result in search_results %}
                        <p>
                            <label>
                                <input type="radio" name="chope_id" value="{{result.id}}">
                                {{result.espece_graine}} {{result.variete_graine}}, ({{result.provenance}} {{result.annee_de_recolte|cut:".0"}})
                            </label>
                            <input type="submit" value="Ajouter cette graine">
                        </p>
                    {% endfor %}
                {% endif %}
            </table>
            {% if wizard.steps.prev %}
            <!--<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.first }}">{% trans "first step" %}</button>-->
            <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
            {% endif %}
            <input type = "submit" value = "étape suivante">
        </form>
    </main>
{% endblock %}

Thank you for your attention if you managed to read all of it !


Solution

  • Hello fellow that browsed toward this page. If you were also looking for a multi-page form with specific step like mine who has a search bar, you can do like so.

    I will just deposit my raw code in here. It should be helpful already. Once my Django app is finished, I'll take the time to explain.

    views.py
    
    def ajout_lot(request, nom_culture):
    
        # Initialization
    
        template_name = 'forms/ajout_lot.html'
    
        context = {
    
        }
    
        instance = Lot()
        model = Lot
    
        form_list = [AddLotStep1, AddLotStep2, AddLotStep3]
    
        fields = ['etat_lot', 'id_graine', 'quantite_lot', 'seme_date', 'plante_date', 'culture_introduction_date', 'phase_lunaire_seme', 'phase_lunaire_plante', 'constellation_seme', 'constellation_plante', 'nom_culture', 'nom_culture_incubation']
    
        form = {
            'step': 1,
            'count': len(form_list)
        }
    
        form.update({
            'form_fields': form_list[form['step'] -1]
        })
    
        context.update({'form': form})
    
        # Handling GET requests
    
        if request.GET.get('search_value') != None:
    
            search_value = request.GET.get('search_value')
            
            search_results = Graine.objects.filter(
                Q(espece_graine__contains = search_value) |
                Q(variete_graine__contains = search_value)
            )
    
            form.update({'step': 1})
    
            context.update({
                'form': form,
                'search_results': search_results
            })
    
        # Navigation throught steps
    
        if request.POST.get('next_step') != None:
    
            for field in fields:
                if request.POST.get(field) != None:
                    request.session.update({field: request.POST.get(field)})
                else:
                    pass
    
            form.update({
                'step': int(request.POST.get('next_step'))
            })
    
            form.update({
                'form_fields': form_list[form['step'] -1]()
            })
    
            context.update({'form': form})
    
        elif request.POST.get('previous_step') != None:
    
            form.update({
                'step': int(request.POST.get('previous_step'))
            })
            form.update({
                'form_fields': form_list[form['step'] -1]()
            })
    
            context.update({'form': form})
    
        # step 3
        if (request.POST.get('etat_lot') != None) and (len(request.POST) < 6):
    
            form_list[form['step'] -1] = AddLotStep3(request.POST or None, etat_lot = request.POST.get('etat_lot'))
    
            form.update({
                'form_fields': form_list[form['step'] -1],
                'step': 3
            })
    
            context.update({
                'form': form,
                'lunar_calendar': 'lunar_calendar'
            })
    
        # last step
        elif request.POST.get('confirmation_step') != None:
    
            for field in fields:
                if request.POST.get(field) != None:
                    request.session.update({field: request.POST.get(field)})
                else:
                    pass
    
            request.session.update({'nom_culture': nom_culture})
    
            # Now, every data from forms have been stored within request.session dictionnary.
    
            # Transfering data to a dictionnary, becausee it'd be bad practice to process data from there
    
            '''
            From data we have :
            - data_front that will display a summary of the form for the user
            - data_back that will save data in the database
    
            Once the user is satisfied with the summary, he can save data. Otherwise, he starts again from zero.
            '''
    
            data = {key: value for key, value in request.session.items() if key in fields}
    
            for key, value in data.items():
                if key == 'id_graine':
                    data.update({key: Graine.objects.get(id = value)})
                elif key == 'nom_culture':
                    data.update({key: Culture.objects.get(nom = value)})
    
            labels = []
    
            for datum in data:
                labels.append(getattr(model, datum).field.verbose_name)
    
            data_front = {label: value for label, value in zip(labels, data.values())}
    
            context.update({'data_front': data_front})
    
        elif request.POST.get('recommencer') != None:
    
            # Since we restart the form, we better have to clean request.session before
    
            for field in fields:
                try:
                    del request.session[field]
                except KeyError:
                    pass
    
            return redirect('ajout_lot', nom_culture = nom_culture)
    
        elif request.POST.get('sauvegarder') != None:
    
            data = {key: value for key, value in request.session.items() if key in fields}
    
            for key, value in data.items():
                if key == 'id_graine':
                    data.update({key: Graine.objects.get(id = value)})
                elif (key == 'quantite_lot') and (value == ''):
                    data.update({key: None})
                elif key == 'nom_culture':
                    data.update({key: Culture.objects.get(nom = value)})
                elif (key.endswith('date')) and (key != None):
                    data.update({key: dt.strptime(value, '%d-%m-%Y').strftime('%Y-%m-%d')})
    
            data_back = {}
    
            for key, value in data.items():
                if key == 'nom_culture':
                    data_back.update({
                        key: value,   
                        'nom_culture_incubation': value
                    })
                elif (key == 'seme_date') or (key == 'plante_date'):
                    data_back.update({
                        key: value,
                        'culture_introduction_date': value
                    })
                else:
                    data_back.update({key: value})
    
            for key, value in data_back.items():
                setattr(instance, key, value)
            instance.save()
    
            # Once data are saved, we can clean request.session
    
            for field in fields:
                try:
                    del request.session[field]
                except KeyError:
                    pass
            
            context.update({
                'sauvegarder': 'sauvegarder',
                'nom_culture': nom_culture
            })
    
        return render(request, template_name, context)
    
    forms.py
    
    class AddLotStep1(forms.ModelForm):
    
        class Meta:
            model = Lot
            fields = ['id_graine']
            labels = {
                'id_graine': 'Choisissez la graine du lot'
            }
    
    class AddLotStep2(forms.ModelForm):
    
        class Meta:
            model = Lot
            fields = ['quantite_lot']
            labels = {
                'quantite_lot': 'Indiquez la quantité'
            }
            widgets = {
                'quantite_lot': forms.TextInput
            }
    
    class AddLotStep3(forms.Form):
    
        etat_lot = forms.ChoiceField(
            label = 'Le lot a-t-il été semé ou planté ?',
            choices = [
                ('Semé', 'Semé'),
                ('Planté', 'Planté')
            ],
            widget = forms.RadioSelect(),
            required = False
        )
    
        def __init__(self, *args, **kwargs):
    
            etat_lot = kwargs.pop('etat_lot', None)
    
            super(AddLotStep3, self).__init__(*args, **kwargs)
    
            if etat_lot == 'Semé':
                self.fields['seme_date'] = forms.DateField(
                    label = 'Date de semaison',
                    widget = DatePicker(),
                    required = False
                )
                self.fields['phase_lunaire_seme'] = forms.ChoiceField(
                    label = 'Phase lunaire',
                    choices = PhasesLunairesChoix.choices,
                    required = False
                )
                self.fields['constellation_seme'] = forms.ChoiceField(
                    label = 'Constellation',
                    choices = ConstellationChoix.choices,
                    required = False
                )
            elif etat_lot == 'Planté':
                self.fields['plante_date'] = forms.DateField(
                    label = 'Date de plantaison',
                    widget = DatePicker(),
                    required = False
                )
                self.fields['phase_lunaire_plante'] = forms.ChoiceField(
                    label = 'Phase lunaire',
                    choices = PhasesLunairesChoix.choices,
                    required = False
                )
                self.fields['constellation_plante'] = forms.ChoiceField(
                    label = 'Constellation',
                    choices = ConstellationChoix.choices,
                    required = False
                )
    
    app/urls.py
    
    path('etat_jardin/<str:nom_culture>/ajout_lot', views.ajout_lot, name = 'ajout_lot')
    
    template
    
    {% block base %}
        {% include 'components/_base.html' %}
    {% endblock %}
    
    {% block content %}
        {% if sauvegarder %}
            <p>Les données ont bien été sauvegardées</p>
            <a href="{% url 'ajout_lot' nom_culture %}">Ajouter un nouveau lot</a>
            <a href="{% url 'etat_jardin_detail' nom_culture %}">Revenir à {{nom_culture}}</a>
        {% elif data_front %}
            <p>Récapitulatif</p>
            {% for label, value in data_front.items %}
                {% if label == "Graine" %}
                    <p>{{label}} : {{value.espece_graine}} {{value.variete_graine}}</p>
                {% else %}
                    <p>{{label}} : {{value|default:"Aucune données enregistrées"}}</p>
                {% endif %}
            {% endfor %}
            <form action="" method="POST">{% csrf_token %}
                <button name="recommencer" type="submit">Je me suis trompé, recommencer</button>
                <button name="sauvegarder" type="submit">Je confirme le lot, sauvegarder</button>
            </form>
        {% elif form %}
            {% if form.step == 1 %}
                <p>Étape {{ form.step }}/{{ form.count }}</p>
                <p>Choisissez la graine du lot</p>
                <form action="" method="GET" name="barre de recherche">
                    <input type="text" name="search_value">
                    <button type="submit" value="1">Valider</button>
                </form>
                <form action="" method="POST">{% csrf_token %}
                    {% for result in search_results %}
                        <p>
                            <label>
                                <input type="radio" name="id_graine" value="{{result.id}}">
                                {{result.espece_graine}} {{result.variete_graine}}, ({{result.provenance}} {{result.annee_de_recolte|cut:".0"}})
                            </label>
                        </p>
                    {% endfor %}
                    <button name="next_step" type="submit" value="{{ form.step|add:'1' }}">Suivant</button>
                </form>
            {% elif form.step == form.count %}
                <p>Étape {{ form.step }}/{{ form.count }}</p>
                {% if lunar_calendar %}
                    {% include 'components/_lunarCalendar.html' %}
                {% endif %}
                <form action="" method="POST">{% csrf_token %}
                    {{ form.form_fields }}
                    <button name="previous_step" type="submit" value="{{ form.step|add:'-1' }}">Précédent</button>
                    <button name="confirmation_step" type="submit">Résumer</button>
                </form>
            {% else %}
                <p>Étape {{ form.step }}/{{ form.count }}</p>
                <form action="" method="POST">{% csrf_token %}
                    {{ form.form_fields }}
                    <button name="previous_step" type="submit" value="{{ form.step|add:'-1' }}">Précédent</button>
                    <button name="next_step" type="submit" value="{{ form.step|add:'1' }}">Suivant</button>
                </form>
            {% endif %}
        {% endif %}
    {% endblock %}