Даталист с ошибкой свободного текста "Выберите допустимый вариант. Этот выбор не является одним из доступных вариантов".

Я создаю форму Create a Recipe с помощью crispy forms и пытаюсь использовать поле ввода datalist, чтобы пользователи могли вводить свои собственные ингредиенты, например, 'Big Tomato' или выбирать из GlobalIngredients, уже имеющихся в базе данных, например, 'tomato' или 'chicken'. Однако, независимо от того, ввожу ли я новый ингредиент или выбираю уже существующий, я получаю следующую ошибку: "Выберите правильный вариант. Этот выбор не является одним из доступных вариантов". Как устранить эту ошибку?

Визуально: 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 %}

Вы можете создать свое собственное TextInput для обработки этого. Я думаю, что вам нужно поле ввода типизированной модели, которое позволяет пользователю как искать, так и предоставлять рекомендуемый выбор вариантов.

Я создал свой собственный здесь:

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)
        

А затем в RecipeIngredientForm внутри функции __init__. Включите следующее после вызова функции super().

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

С помощью комментария ecogels я смог понять, что вызывает проблему, и с помощью комбинации ответа Lewis и этого ответа мне удалось заставить это работать со следующим кодом.

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
    }
Вернуться на верх