Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get ModelChoiceField instances in the template

I have a ModelForm that contains a ModelChoiceField using the RadioSelect widget.

class MyAForm(forms.ModelForm):
    one_property = models.ModelChoiceField(
        widget=forms.RadioSelect,
        queryset=MyBModel.objects.filter(visible=True),
        empty_label=None)
    class Meta:
        model = MyAModel

There are attributes on MyBModel that I want to display next to the radio button. I would override label_from_instance on a sub-class of ModelChoiceField but this does not allow me to do what I want as I want the radio button to appear inside a table which has a row for each selection item.

So somewhere in my template I want something like...

{% for field in form.visible_fields %}
    {% if field.name == "one_property" %}
    <table>
        {% for choice in field.choices %}
            <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 %}

Unfortunately field.choices returns a tuple of the object's id and the label and not an instance from the queryset.

Is there a simple way to get instances of the choices for a ModelChoiceField to use within a template?

like image 959
Stephen Paulger Avatar asked Apr 24 '12 15:04

Stephen Paulger


3 Answers

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 %}
like image 139
Stephen Paulger Avatar answered Sep 22 '22 10:09

Stephen Paulger


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>.

like image 4
Two-Bit Alchemist Avatar answered Sep 24 '22 10:09

Two-Bit Alchemist


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.

like image 2
Tomek Jurkiewicz Avatar answered Sep 22 '22 10:09

Tomek Jurkiewicz