Datalist with free text error "Select a valid choice. That choice is not one of the available choices."
Asked Answered
D

2

6

I am building a Create a Recipe form using crispy forms and I am trying to use a datalist input field for users to enter their own ingredients, like 'Big Tomato' or select from GlobalIngredients already in the database like 'tomato' or 'chicken'. However, regardless of whether I enter a new ingredient or select a pre-existing one, I am getting the following error: "Select a valid choice. That choice is not one of the available choices.". How do I fix this error?

Visual: Form html

models.py

class Recipe(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    websiteURL = models.CharField(max_length=200, blank=True, null=True)
    image = models.ImageField(upload_to='image/', blank=True, null=True)
    name = models.CharField(max_length=220) # grilled chicken pasta
    description = models.TextField(blank=True, null=True)
    notes = models.TextField(blank=True, null=True)
    serves = models.CharField(max_length=30, blank=True, null=True)
    prepTime = models.CharField(max_length=50, blank=True, null=True)
    cookTime = models.CharField(max_length=50, blank=True, null=True)


class Ingredient(models.Model):
    name = models.CharField(max_length=220)

    def __str__(self):
        return self.name

class GlobalIngredient(Ingredient):
    pass # pre-populated ingredients e.g. salt, sugar, flour, tomato

class UserCreatedIngredient(Ingredient): # ingredients user adds, e.g. Big Tomatoes
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

class RecipeIngredient(models.Model):
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
    ingredient = models.ForeignKey(Ingredient, null=True, on_delete=models.SET_NULL)
    description = models.TextField(blank=True, null=True)
    quantity = models.CharField(max_length=50, blank=True, null=True) # 400
    unit = models.CharField(max_length=50, blank=True, null=True) # pounds, lbs, oz ,grams, etc

forms.py

class RecipeIngredientForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
    
        super(RecipeIngredientForm, self).__init__(*args, **kwargs)

        self.helper = FormHelper()
        #self.helper.form_id = 'id-entryform'
        #self.helper.form_class = 'form-inline'
        self.helper.layout = Layout(
            Div(
                Div(Field("ingredient", placeholder="Chickpeas - only write the ingredient here"), css_class='col-6 col-lg-4'),
                Div(Field("quantity", placeholder="2 x 400"), css_class='col-6 col-md-4'),
                Div(Field("unit", placeholder="grams"), css_class='col-5 col-md-4'),
                Div(Field("description", placeholder="No added salt tins - All other information, chopped, diced, whisked!", rows='3'), css_class='col-12'),
            
            css_class="row",
           ),
           
        )
        
    class Meta:
        model = RecipeIngredient
        fields = ['ingredient', 'quantity', 'unit', 'description']
        labels = {
            'ingredient': "Ingredient",
            "quantity:": "Ingredient Quantity",
            "unit": "Unit",
            "description:": "Ingredient Description"}
        widgets={'ingredient': forms.TextInput(attrs={
            'class': 'dropdown',
            'list' : 'master_ingredients',
            'placeholder': "Chickpeas - only write the ingredient here"
        })}

views.py

@login_required
def recipe_create_view(request):
    ingredient_list = Ingredient.objects.all()
    form = RecipeForm(request.POST or None)
    # Formset = modelformset_factory(Model, form=ModelForm, extra=0)
    RecipeIngredientFormset = formset_factory(RecipeIngredientForm)
    formset = RecipeIngredientFormset(request.POST or None)
    RecipeInstructionsFormset = formset_factory(RecipeInstructionForm, extra=0)
    instructionFormset = RecipeInstructionsFormset(request.POST or None, initial=[{'stepName': "Step 1"}], prefix="instruction")
    
    context = {
        "form": form,
        "formset": formset,
        "instructionFormset": instructionFormset,
        "ingredient_list": ingredient_list
    }
    if request.method == "POST":
        print(request.POST)
        if form.is_valid() and formset.is_valid() and instructionFormset.is_valid():
            parent = form.save(commit=False)
            parent.user = request.user
            parent.save()
            # formset.save()
            #recipe ingredients
            for form in formset:
                child = form.save(commit=False)
                print(child.ingredient)
                globalIngredient = Ingredient.objects.filter(name=child.ingredient.lower()) # not truly global as this will return user ingredients too
                if (globalIngredient):
                    pass
                else:
                    newIngredient = UserCreatedIngredient(user=request.user, name=child.ingredient.lower())
                    newIngredient.save()
                if form.instance.ingredient.strip() == '':
                    pass
                else:
                    child.recipe = parent
                    child.save()
            # recipe instructions
            for instructionForm in instructionFormset:
                instructionChild = instructionForm.save(commit=False)
        
                if instructionForm.instance.instructions.strip() == '':
                    
                    pass
                else:
                   
                    instructionChild.recipe = parent
                    instructionChild.save()
            context['message'] = 'Data saved.'
            
            return redirect(parent.get_absolute_url())
    else:
        form = RecipeForm(request.POST or None)
        formset = RecipeIngredientFormset()
        instructionFormset = RecipeInstructionsFormset()
    return render(request, "recipes/create.html", context)

create.html

<!--RECIPE INGREDIENTS-->
{% if formset %}
<h3 class="mt-4 mb-3">Ingredients</h3>
{{ formset.management_form|crispy }}

<div id='ingredient-form-list'>
    {% for ingredient in formset %}

            <div class='ingredient-form'>
                
                {% crispy ingredient %}
               
            </div>
    {% endfor %}

    <datalist id="master_ingredients">
        {% for k in ingredient_list %}
            <option value="{{k.name|title}}"></option>
        {% endfor %}
    </datalist>
</div>

<div id='empty-form' class='hidden'>
    <div class="row mt-4">
        <div class="col-6">{{ formset.empty_form.ingredient|as_crispy_field }}</div>
        <div class="col-6">{{ formset.empty_form.quantity|as_crispy_field }}</div>
        <div class="col-6">{{ formset.empty_form.unit|as_crispy_field }}</div>
        <div id="ingredientIdForChanging" style="display: none;"><div class="col-12">{{ formset.empty_form.description|as_crispy_field }}</div><button type="button"
            class="btn btn-outline-danger my-2" onclick="myFunction('showDescription')"><i class="bi bi-dash-circle"></i> Hide
            Description</button></div><button type="button"
            class="btn btn-outline-primary col-5 col-md-3 col-lg-3 col-xl-3 m-2" id="ingredientIdForChanging1"
            onclick="myFunction('showDescription')"><i class="bi bi-plus-circle"></i> Add a
            Description Field</button>
        
    </div>
</div>
<button class="btn btn-success my-2" id='add-more' type='button'>Add more ingredients</button>
{% endif %}
Delcine answered 8/12, 2021 at 1:29 Comment(1)
I believe the problem is that you replace the ingredient ModelChoiceField with a TextInput. Django expects a valid Ingredient pk but receives the name of an ingredient that may not exist. A simple solution would be to remove the ingredient field and add an ingredient_name = forms.CharField to the form, the logic you have written in your view would be similar.Denbighshire
M
4

You can create your own TextInput and TypedModelListField field to handle this. I think what you're looking for is something which allows the user to both search and provide a recommended selection of choices but validate the input against a model (Ingredient).

I've created one here:

class TypedModelListField(forms.ModelChoiceField):

    def to_python(self, value):
        if self.required:
            if value == '' or value == None:
                raise forms.ValidationError('Cannot be empty')
            
        validate_dict = {self.validate_field: value}
        try:
            value = type(self.queryset[0]).objects.get(**validate_dict))
        except:
            raise forms.ValidationError('Select a valid choice. That choice is not one of the available choices.')
        value = super().to_python(value)
        return value

    def __init__(self, *args, **kwargs):
        self.validate_field= kwargs.pop('validate_field', None)
        super().__init__(*args, **kwargs)


class ListTextWidget(forms.TextInput):

    def __init__(self, dataset, name, *args, **kwargs):
        super().__init__(*args)
        self._name = name
        self._list = dataset
        self.attrs.update({'list':'list__%s' % self._name,'style': 'width:100px;'})
        if 'width' in kwargs:
            width = kwargs['width']
            self.attrs.update({'style': 'width:{}px;'.format(width)})
        if 'identifier' in kwargs:
            self.attrs.update({'id':kwargs['identifier']})

    def render(self, name, value, attrs=None, renderer=None):
        text_html = super().render(name, value, attrs=attrs)
        data_list = '<datalist id="list__%s">' % self._name
        for item in self._list:
            data_list += '<option value="%s">' % item
        data_list += '</datalist>'
        return (text_html + data_list)

Within the RecipeIngredientForm add the following definition:

ingredient = TypedModelListField(
                    queryset=Ingredient.objects.all(),
                    validate_field='name')

And then in RecipeIngredientForm within the __init__ function. Include the following after the super() is called.

self.fields['ingredient'].widget = ListTextWidget(
        dataset=Ingredient.objects.all(), 
        name='ingredient_list')

Machos answered 12/12, 2021 at 14:40 Comment(0)
D
0

With ecogels comment I was able to understand what was causing the issue and with a combination of Lewis answer and this answer I managed to get this working with the following code.

fields.py

    from django import forms

class ListTextWidget(forms.TextInput):
    def __init__(self, data_list, name, *args, **kwargs):
        super(ListTextWidget, self).__init__(*args, **kwargs)
        self._name = name
        self._list = data_list
        self.attrs.update({'list':'list__%s' % self._name})

    def render(self, name, value, attrs=None, renderer=None):
        text_html = super(ListTextWidget, self).render(name, value, attrs=attrs)
        data_list = '<datalist id="list__%s">' % self._name
        for item in self._list:
            data_list += '<option value="%s">' % str(item).title()
        data_list += '</datalist>'

        return (text_html + data_list)

forms.py

from .fields import ListTextWidget

class RecipeIngredientForm(forms.ModelForm):
    ingredientName = forms.CharField(required=True)

    def __init__(self, *args, **kwargs):
        super(RecipeIngredientForm, self).__init__(*args, **kwargs)

        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                Div(Field("ingredientName", placeholder="Chickpeas - only write the ingredient here"), css_class='col-6 col-lg-4'),
                Div(Field("quantity", placeholder="2 x 400"), css_class='col-6 col-md-4'),
                Div(Field("unit", placeholder="grams"), css_class='col-5 col-md-4'),
                Div(Field("description", placeholder="No added salt tins - All other information, chopped, diced, whisked!", rows='3'), css_class='col-12'),
            
            css_class="row",
           ),
           
        )
        self.fields['ingredientName'].widget = ListTextWidget(data_list=Ingredient.objects.all(), name='ingredient-list')
    class Meta:
        model = RecipeIngredient
        fields = ['ingredientName', 'quantity', 'unit', 'description']
        labels = {
            'ingredientName': "Ingredient",
            "quantity:": "Ingredient Quantity",
            "unit": "Unit",
            "description:": "Ingredient Description"}

create.html:

<!--RECIPE INGREDIENTS-->
                {% if formset %}
                    <h3 class="mt-4 mb-3">Ingredients</h3>
                    {{ formset.management_form|crispy }}
                    
                    <div id='ingredient-form-list'>
                        {% for ingredient in formset %}
                    
                                <div class='ingredient-form'>
                                    
                                    {% crispy ingredient %}
                                    
                                </div>
                        {% endfor %}
                    </div>

                    <div id='empty-form' class='hidden'>
                        <div class="row mt-4">
                            <div class="col-6">{{ formset.empty_form.ingredientName|as_crispy_field }}</div>
                            <div class="col-6">{{ formset.empty_form.quantity|as_crispy_field }}</div>
                            <div class="col-6">{{ formset.empty_form.unit|as_crispy_field }}</div>
                            <div id="ingredientIdForChanging" style="display: none;"><div class="col-12">{{ formset.empty_form.description|as_crispy_field }}</div><button type="button"
                                class="btn btn-outline-danger my-2" onclick="myFunction('showDescription')"><i class="bi bi-dash-circle"></i> Hide
                                Description</button></div><button type="button"
                                class="btn btn-outline-primary col-5 col-md-3 col-lg-3 col-xl-3 m-2" id="ingredientIdForChanging1"
                                onclick="myFunction('showDescription')"><i class="bi bi-plus-circle"></i> Add a
                                Description Field</button>
                            
                        </div>
                    </div>
                    <button class="btn btn-success my-2" id='add-more' type='button'>Add more ingredients</button>
                {% endif %}

views.py changes:

form = RecipeForm(request.POST or None)
    # Formset = modelformset_factory(Model, form=ModelForm, extra=0)
    RecipeIngredientFormset = formset_factory(RecipeIngredientForm)
    formset = RecipeIngredientFormset(request.POST or None)
    RecipeInstructionsFormset = formset_factory(RecipeInstructionForm, extra=0)
    instructionFormset = RecipeInstructionsFormset(request.POST or None, initial=[{'stepName': "Step 1"}], prefix="instruction")
    URLForm = RecipeIngredientURLForm(request.POST or None)
    context = {
        "form": form,
        "formset": formset,
        "URLForm": URLForm,
        "instructionFormset": instructionFormset
    }
Delcine answered 13/12, 2021 at 23:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.