Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add data-attribute to django modelform modelchoicefield

I have a django modelform 'Recipe' with a foreignkey field to a model 'Ingredient'.

When rendering the form I get a SELECT list that have an ID matching the ingredients ID and text display equal to the string representation of the field.

However, I want to add a data-attribute to the select list that matches the rendered option from the Ingredient queryset.

For example, lets say this is what is currently being rendered:

<option value="1158">Carrots</option>
<option value="1159">Strawberry</option>
<option value="1160">Onion</option>
<option value="1161">Spinach</option>

But I want to add a data attribute for the related objects:

<option value="1158" data-ingredient-type="vegetable">Carrots</option>
<option value="1159" data-ingredient-type="fruit">Strawberry</option>
<option value="1160" data-ingredient-type="vegetable">Onion</option>
<option value="1161" data-ingredient-type="vegetable">Spinach</option>
like image 497
corycorycory Avatar asked Aug 14 '16 17:08

corycorycory


People also ask

How to create a form from a model in Django?

For this reason, Django provides a helper class that lets you create a Form class from a Django model. The generated Form class will have a form field for every model field specified, in the order specified in the fields attribute. Each model field has a corresponding default form field.

What is a form field in Django?

When creating a form using Django, form fields are an essential part of creating the Django Form class. There are over 23 built-in field classes with build-in validations and clean () methods. We will cover commonly used form fields along with form widgets to quickly get you on your way to creating forms for any site.

How to save a model with missing fields in Django?

Django will prevent any attempt to save an incomplete model, so if the model does not allow the missing fields to be empty, and does not provide a default value for the missing fields, any attempt to save () a ModelForm with missing fields will fail.

How do I connect a Django form to a drop-down menu?

If you wish to connect a Django model to the form, import the model from the models.py file and use the ModelChoiceField () and specify the queryset in the form field. Set the initial to 0 if you wish for the first model object value to be the initial value of the drop-down menu.


3 Answers

One way is using a custom Select widget which allows passing individual attributes in options, through the label part of the widget choices: (code from this great answer)

class SelectWithOptionAttribute(Select):
   
"""
Use a dict instead of a string for its label. The 'label' key is expected
for the actual label, any other keys will be used as HTML attributes on
the option.
"""

def create_option(self, name, value, label, selected, index, 
                  subindex=None, attrs=None):
    # This allows using strings labels as usual
    if isinstance(label, dict):
        opt_attrs = label.copy()
        label = opt_attrs.pop('label')
    else: 
        opt_attrs = {}
    option_dict = super().create_option(name, value, 
        label, selected, index, subindex=subindex, attrs=attrs)
    for key,val in opt_attrs.items():
        option_dict['attrs'][key] = val
    return option_dict

To populate the individual options override label_from_instance method on a ModelChoiceField subclass(see django docs):

IngredientChoiceField(ModelChoiceField):
"""ChoiceField with puts ingredient-type on <options>"""

# Use our custom widget:
widget = SelectWithOptionAttribute

def label_from_instance(self, obj):
# 'obj' will be an Ingredient
    return {
        # the usual label:
        'label': super().label_from_instance(obj),
        # the new data attribute:
        'data-ingredient-type': obj.type
    }

Finally, simple use this field in a form:

RecipeModelForm(ModelForm):

class Meta:
    model = Recipe
    fields = [
        # other fields ...
        'ingredients',
    ]
    
    field_classes = {
        'ingredients': IngredientChoiceField
    }
like image 152
mglart Avatar answered Oct 01 '22 10:10

mglart


Why not render the fields manually?
It''ll be something like

<select>
  {% for option in form.ingredient.choices %}
     <option value="{{ option.id }}" data-ingredient-type={{ option.type }}>{{ option.name }}</option>
  {% endfor %}
</select>  

Or maybe in you model form class you add the attribute to it, but this must be a string (or probably a function)

widgets = { ...
     'ingredients' = forms.Select(attrs={'data-ingredient-type': 'fruit'}),
   ...}
like image 37
Resley Rodrigues Avatar answered Oct 01 '22 10:10

Resley Rodrigues


My solution was to create a custom widget that overrides create_option():

class IngredientSelect(forms.Select):
    def create_option(
        self, name, value, label, selected, index, subindex=None, attrs=None
    ):
        option = super().create_option(
            name, value, label, selected, index, subindex, attrs
        )
        if value:
            ingredient = models.Ingredient.objects.get(pk=value)
            option['attrs'].update({
                'data-type': ingredient.type
            })
        return option

Then you need to specify the widget to use for the ingredient field in your form:

class RecipeForm(forms.ModelForm):
    class Meta:
        model = models.Recipe
        fields = '__all__'
        widgets = {
            'ingredient': IngredientSelect,
        }

Thanks to In Django form, custom SelectField and SelectMultipleField to pointing me towards this solution.

I'm not entirely satisfied with this solution because it assumes that value is a pk for Ingredient and executes a direct database query to get the Ingredient option. It seems like the model object should be available from the ModelChoiceField, but I was unable to find a way to get it.

like image 36
Code-Apprentice Avatar answered Oct 01 '22 10:10

Code-Apprentice