2

I would like to be able to extract different information in my django form:

That's my form:

<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>

class InstanceForm(ModelForm):
    class Meta:
        model = models.BaseAsset
        widgets = {
            'labels': LabelIconCheckboxSelectMultiple()
        }

The model:

class AssetClass(models.Model):
    default_labels = models.ManyToManyField(Label, null=True, blank=True)
    pass

the M2M reference field

class Label(models.Model):
    explanation = models.CharField(null=True, max_length=63)
    svgpreview  = models.CharField(null=True, max_length=31)
    def __unicode__(self):
        return unicode(self.explanation)
    pass

Now, the HTML code generated by the {{ form.as_p }} is as follows:

<li><label for="id_labels_0"><input type="checkbox" name="labels" value="1" id="id_labels_0" /> Consult owner before using</label></li>
<li><label for="id_labels_1"><input type="checkbox" name="labels" value="2" id="id_labels_1" /> This item is broken</label></li>

Which means it's clearly using the __unicode__ rendering of the model 'Label'. How can I change that behavior in the Select widget, so that it would use a different function to populate it's choices? I'm trying to get it, in the reasonably portable way, to print '<img src="{{label.svgpreview}}" alt="{{label.explanation}}"...>' next to the checkbox?

qdot
  • 6,195
  • 5
  • 44
  • 95

5 Answers5

5

You will override forms.widgets.CheckboxSelectMultiple class:

This is CheckboxSelectMultiple class and its render function:

class CheckboxSelectMultiple(SelectMultiple):
    def render(self, name, value, attrs=None, choices=()):
        if value is None: value = []
        has_id = attrs and 'id' in attrs
        final_attrs = self.build_attrs(attrs, name=name)
        output = [u'<ul>']
        # Normalize to strings
        str_values = set([force_unicode(v) for v in value])
        for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
            # If an ID attribute was given, add a numeric index as a suffix,
            # so that the checkboxes don't all have the same ID attribute.
            if has_id:
                final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
                label_for = u' for="%s"' % final_attrs['id']
            else:
                label_for = ''

            cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
            option_value = force_unicode(option_value)
            rendered_cb = cb.render(name, option_value)
            option_label = conditional_escape(force_unicode(option_label))
            output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label))
        output.append(u'</ul>')
        return mark_safe(u'\n'.join(output))

So what you will do :

class MyCheckboxSelectMultiple(CheckboxSelectMultiple):
    def render(self, name, value, attrs=None, choices=()):
        #put your code to have custom checkbox control with icon
        #...
        output.append(u'<li><label%s>%s %s</label></li>' % (label_for, rendered_cb, option_label)) # especially you will be working on this line
        #...

Then where you are using widgets=CheckboxSelectMultiple() it will become widgets=MyCheckboxSelectMultiple()

asdf_enel_hak
  • 7,474
  • 5
  • 42
  • 84
  • Your answer shed some light onto what's going on inside - unfortunately, I would need to redefine what's inside self.choices - and that's plumbed all way down at `django.forms.models.ModelChoiceField`.. see my self-answer if you are curious, it's actually both hackish and cool! – qdot Jan 01 '12 at 22:34
3

Reading django.forms.models.ModelChoiceField gives a hint:

# this method will be used to create object labels by the QuerySetIterator.
# Override it to customize the label.
def label_from_instance(self, obj):
    """
    This method is used to convert objects into strings; it's used to
    generate the labels for the choices presented by this object. Subclasses
    can override this method to customize the display of the choices.
    """
    return smart_unicode(obj)

ok, but how do I override it per-instance of ModelForm - this gets overridden in few places throughout django.forms

Considering the following code:

class InstanceForm(ModelForm):
    class Meta:
        model = models.BaseAsset
        widgets = {
            'labels': forms.CheckboxSelectMultiple()
        }


    def __init__(self, *args, **kwargs):
        def new_label_from_instance(self, obj):
            return obj.svgpreview

        super(InstanceForm, self).__init__(*args, **kwargs)
        funcType = type(self.fields['labels'].label_from_instance)
        self.fields['labels'].label_from_instance = funcType(new_label_from_instance, self.fields['labels'], forms.models.ModelMultipleChoiceField)

This is somewhat creepy - basically, it's a more bizzare implementation of this: Override a method at instance level

Please read the comments in the referenced thread to understand why this might be a bad idea in general..

Community
  • 1
  • 1
qdot
  • 6,195
  • 5
  • 44
  • 95
0

You don't have to do the "creepy" instance-level override to take proper advantage of the documented django.forms.models.ModelChoiceField.label_from_instance() method.

Building on the AssetClass and Label objects in the original post:

class AssetSvgMultiField(forms.ModelMultipleChoiceField):
    """
    Custom ModelMultipleChoiceField that labels instances with their svgpreview.
    """
    def label_from_instance(self, obj):
        return obj.svgpreview


class InstanceForm(forms.ModelForm):
    default_labels = AssetSvgMultiField(queryset=Label.objects.all())

    class Meta:
        model = models.AssetClass
        widgets = {
            'default_labels': forms.CheckboxSelectMultiple()
        }
Jamie B
  • 451
  • 4
  • 5
0

This is explained in the Django documentation here: https://docs.djangoproject.com/en/1.9/ref/forms/fields/#django.forms.ModelChoiceField.to_field_name

You can see the ModelChoiceField class calling the method on the field here: https://github.com/django/django/blob/1155843a41af589a856efe8e671a796866430049/django/forms/models.py#L1174

If you're not overriding choices explicitly, then your code might look like this:

class RectificationAssetMultiField(forms.ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return '[{0.pk}] {0.label} ({0.location})'.format(obj)


class RectificationForm(forms.ModelForm):
    items = RectificationAssetMultiField(
        required=False,
        queryset=InspectionItem.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        label="Non-compliant Assets"
    )

    class Meta:
        model = Rectification
        fields = ('ref', 'items', 'status')

Be careful that this will only work if you're not setting choices directly (see _get_choices in the above URL).

If instead you wanted to override choices (for a more efficient result than a queryset, or something better expressed as a ValuesList) then you would have something like this:

class RectificationAssetMultiField(forms.ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return '[{0.pk}] {0.label} ({0.location})'.format(obj)


class RectificationForm(forms.ModelForm):
    items = RectificationAssetMultiField(
        required=False,
        queryset=InspectionItem.objects.none(),
        widget=forms.CheckboxSelectMultiple,
        label="Non-compliant Assets"
    )

    def __init__(self, *args, **kwargs):
        super(RectificationForm, self).__init__(*args, **kwargs)
        self.fields['items'].choices = (InspectionItem.objects
            .active()
            .noncompliant()
            .filter(property_id=self.instance.property_id)
            .values_list('pk', 'label') # pass a key value pair
        )

    class Meta:
        model = Rectification
        fields = ('ref', 'items', 'status')
Aidan
  • 4,150
  • 2
  • 20
  • 16
-1

Don't use {{ form.as_p }} if you don't like that rendering.

Loop over the form instead:

<form action="/contact/" method="post">
    {% for field in form %}
        <div class="fieldWrapper">
            {{ field.errors }}
            {{ field.label_tag }}: {{ field }}
        </div>
    {% endfor %}
    <p><input type="submit" value="Send message" /></p>
</form>

You are then free to use whatever HTML you want.

From: https://docs.djangoproject.com/en/dev/topics/forms/#looping-over-the-form-s-fields

Emil Stenström
  • 13,329
  • 8
  • 53
  • 75
  • Unfortunately, this approach doesn't solve my problem - I want to redefine what's extracted via a widget, inside a {{ field }}, few levels of plumbing down. In my question, I asked specifically how do I convince ModelForm, to take a specific field of a M2M relational mapping, instead of the default unicode rendering.. see my self-answer – qdot Jan 01 '12 at 22:38
  • Yes, I know what you're after, and I've been there, plumbing way down in Django's internals. But the thing is: you know what HTML you want, and you have all the fields you need to render that HTML. So I say just render it with ~5 lines of templates, instead of ~50 lines of python. Break out the template code in a template tag if you wish to make it more reusable. Sorry if my reply came up as confrontational, I just wanted to share my experience. – Emil Stenström Jan 01 '12 at 22:43
  • It would be somewhat tricky to extract this information out via templates directly - without sacrificing the form itself, and it's value labeling, etc. Basically, in my case, the list of labels is actually dynamic, and this logic really should belong in the Controller part of MVC and not being shoved into HTML templates - InstanceForm can still be inherited from.. and it's actually just 2 lines of python (a bit convoluted double-bubble OOP). Check out my self-answer, i think it could even be included in the docs themselves - why force the user to rely on __unicode__ caster? – qdot Jan 01 '12 at 23:04