Django RadioSelect widget, add additional information

Viewed 785

I have this simplified model and form:

class Books(models.Model):
    name = models.CharField(max_length=500)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    default = models.BooleanField(default=False)

    def __str__(self):
        return self.name



class BookForm(forms.Form):

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

        self.fields['Field'] = forms.ModelChoiceField(queryset=None, empty_label=None, widget=forms.RadioSelect)
        self.fields['Field'].queryset = Books.objects.all()
        self.fields['Field'].initial = Books.objects.filter(default=True).first()

This will result in a RadioSelect-Form, like this:

(x) Book1
( ) Book2
( ) Book3

My problem is, how can I add the price in the RadioSelect form, that it's just visible. It should appear after the name of the book, ideally even in a different font, which I set over a bootstrap class (e.g "text-primary") (this is not mandatory)

(x) Book1 (10 €)
( ) Book2 (20 €)
( ) Book3 (30 €)

I know i can return the name and price in the model, like

class Books(models.Model):
    name = models.CharField(max_length=500)
    price = models.DecimalField(max_digits=6, decimal_places=2)
    default = models.BooleanField(default=False)

    def __str__(self):
        return '%s (%s €)' % (self.value, str(self.price))

But because of other reasons, i can not do this. I just need to return the name. Are there other ways to do this?

I even read into django-crispy-forms, but couldnt find a solution.

1 Answers

You can use .label_from_instance.

From the documentation:

The __str__() method of the model will be called to generate string representations of the objects for use in the field’s choices. To provide customized representations, subclass ModelChoiceField and override label_from_instance.

You can define a function that gives you the representation that you want and then set .label_from_instance on your field.

Your BookForm then looks like this:

class BookForm(forms.Form):

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

        self.fields['Field'] = forms.ModelChoiceField(queryset=None, empty_label=None, widget=forms.RadioSelect)
        self.fields['Field'].queryset = Books.objects.all()
        self.fields['Field'].initial = Books.objects.filter(default=True).first()
        # customize how your option is rendered in the template
        self.fields["Field"].label_from_instance = lambda item: f"{item} ({item.price} €)"

To apply CSS on the label add HTML. Instead of using style='...' you can also use classes so it works with Bootstrap.

self.fields["Field"].label_from_instance = lambda item: f"{item} <span style='color:red;'>({item.price} €)</span>"

For Python versions before 3.7:

self.fields["Field"].label_from_instance = lambda item: str(item) + "<span style='color:red;'>(" + str(item.price) + " €)</span>"

Then render your form in the template like this:

<form action="{% url 'books' %}" method="post">
    {% csrf_token %}
    {% for hidden_field in form.hidden_fields %}
        {{ hidden_field }}
    {% endfor %}
    <ul>
    {% for choice in form.Field %}
        <li>{{ choice.choice_label|safe }}</li>
    {% endfor %}
    </ul>
    <input class="btn btn-dark" type="submit" value="Submit">
</form>

Iterate over the choices for your field and then you can get your customized labels with:

{{ choice.choice_label|safe }}

The safe filter is needed so your HTML is not escaped.

Related