[Fixed]-How to get ModelChoiceField instances in the template

12đź‘Ť

âś…

After delving into the django source for ModelChoiceField I discovered it has a property “queryset”.

I was able to use something like…

{% for field in form.visible_fields %}
    {% if field.name == "one_property" %}
    <table>
        {% for choice in field.queryset %}
            <tr>
                <td><input value="{{choice.id}}" type="radio" name="one_property" />{{choice.description}}</td>
                <td><img src="{{choice.img_url}}" /></td>
            </tr>
        {% endfor %}
    </table>
    {% endif %}
{% endfor %}

4đź‘Ť

I wanted to do something nearly identical to OP’s question (table and all), was similarly frustrated by Django’s lack of cooperation, and similarly ended up delving into the source to come up with my own implementation. What I came up with is a bit different than the accepted answer, and I liked it better because I was using a simple {{ form.as_table }} in my template and didn’t want to have to loop through visible_fields needlessly or hard-code a radio button in my template that merely looks similar to Django’s current implementation (which could change). Here’s what I did instead:

RadioInput and RadioFieldRenderer

Django’s RadioSelect widget uses RadioFieldRenderer to yield a generator of RadioInputs, which do the actual work of rendering the radio buttons. RadioSelect seems to have an undocumented feature where you can pass it a different renderer than this default, so you can subclass both of these to get what OP wants.

from django import forms
from django.utils.safestring import mark_safe

class CustomTableRadioInput(forms.widgets.RadioInput):

    # We can override the render method to display our table rows
    def render(self, *args, **kwargs):
        # default_html will hold the normally rendered radio button
        # which we can then use somewhere in our table
        default_html = super(CustomTableRadioInput, self).render(*args, **kwargs)
        # Do whatever you want to the input, then return it, remembering to use
        # either django.utils.safestring.mark_safe or django.utils.html.format_html
        # ...
        return mark_safe(new_html)

class CustomTableFieldRenderer(forms.widget.RadioFieldRenderer):
    # Here we just need to override the two methods that yield RadioInputs
    # and make them yield our custom subclass instead
    def __iter__(self):
        for i, choice in enumerate(self.choices):
            yield CustomTableRadioInput(self.name, self.value,
                                          self.attrs.copy(), choice, i)

    def __getitem__(self, idx):
        choice = self.choices[idx] # Let the IndexError propogate
        return CustomTableRadioInput(self.name, self.value,
                                       self.attrs.copy(), choice, idx)

With that done, we just need to tell the RadioSelect widget to use our custom renderer whenever we call it somewhere in our form code:

...
radio = forms.ChoiceField(widget=forms.RadioSelect(renderer=CustomTableFieldRenderer),
                          choices=...)
...

And that’s it!

Do note that to use this in the template, you’ll probably want to loop over the field rather than calling it directly, i.e. this:

<table>
  <tbody>
  {% for tr in form.radio %}
    <tr>{{ tr }}</tr>
  {% endfor %}
  </tbody>
</table>

rather than this:

<table>
  <tbody>{{ form.radio }}</tbody>
</table>

If you do the latter, it will try to wrap your table elements in <ul><li>...</li></ul>.

2đź‘Ť

Usually you don’t need the actual object, but its rendition.

Consider this code:

class LabelledHiddenWidget(forms.HiddenInput):

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

    def render(self, name, value, attrs=None):
        s = super(LabelledHiddenWidget, self).render(name, value, attrs)
        if value:
            s += SafeUnicode("<span>%s</span>" % self.get_object(value))
        return s

Then you can use it like this:

class SomeForm(forms.Form):
    object = forms.ModelChoiceField(
         SomeModel.objects.all(), 
         widget=LabelledHiddenWidget(get_object=lambda id: get_object_or_404(SomeModel, id=id)))

Then in the template code, {{ form.object }} will output a hidden field with the object id, concatenated with some label. Of course your SomeModel should implement __unicode__ or some other method that returns a nice, human readable label.

Leave a comment