Django CheckboxSelectMultiple override 'choices' from ModelForm

怎甘沉沦 提交于 2020-01-04 15:15:31

问题


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?


回答1:


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()




回答2:


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




回答3:


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()
        }



回答4:


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')



回答5:


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



来源:https://stackoverflow.com/questions/8693251/django-checkboxselectmultiple-override-choices-from-modelform

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!