Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override ModelChoiceField / ModelMultipleChoiceField default widget with a template for each choice

Background

I have two models, Runs and Orders. One run will complete many orders, so I have a Many-to-one relation between my orders and runs, represented as a foreignkey on my orders.

I want to build a UI to create a run. It should be a form in which someone selects orders to run. I'd like to display a list of checkboxes alongside information about each order. I'm using django crispy forms right now.

views.py

class createRunView(LoginRequiredMixin, CreateView):
    model = Run
    form_class = CreateRunForm
    template_name = 'runs/create_run.html'

forms.py

class CreateRunForm(forms.ModelForm):
    class Meta:
       model = Run
       fields = ['orders',]

    orders = forms.ModelMultipleChoiceField(queryset=Order.objects.filter(is_active=True, is_loaded=False))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = 'post'
        self.helper.layout = Layout(
            Field('orders', template="runs/list_orders.html"),
            Submit('save', 'Create Run'),
            Button('cancel', 'Cancel'),
    )

Questions

  1. I'm not sure what locals are available to me in the list_orders.html template. It seems like there's {{ field }} and maybe form.visible_fields but if I dig to deeply into either I get a TypeError: 'SubWidget' object is not iterable, which is barely documented online.

  2. The above suggests I might still be getting a widget in the template, despite the fact that Field('orders', template="runs/list_orders.html"), should prevent that, per the crispy docs:

    Field: Extremely useful layout object. You can use it to set attributes in a field or render a specific field with a custom template. This way you avoid having to explicitly override the field’s widget and pass an ugly attrs dictionary:

  3. I've seen this answer which suggests using label_from_instance. However I'm not sure how to stuff a bunch of html into label_from_instance. Instead of having a different label, I really want to have a template which generates a bunch of html which shows details about the entire order object, so I'm not sure this approach will work.

  4. The answers in this question mostly confused me, but the accepted answer didn't work, it seems. (maybe a django version issue, or a crispy forms issue?)

TL;DR

How do I render templates with data from each model in ModelMultipleChoiceField?

like image 533
Alex Lenail Avatar asked Mar 17 '18 21:03

Alex Lenail


1 Answers

Widgets control how fields are rendered in HTML forms. The Select widget (and its variants) have two attributes template_name and option_template_name. The option_template_name supplies the name of a template to use for the select options. You can subclass a select widget to override these attributes. Using a subclass, like CheckboxSelectMultiple, is probably a good place to start because by default it will not render options in a <select> element, so your styling will be easier.

By default the CheckboxSelectMultiple option_template_name is 'django/forms/widgets/checkbox_option.html'.

You can supply your own template that will render the details of the orders how you want. IE in your forms.py

class MyCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
    option_template_name = 'myapp/detail_options.html'

class CreateRunForm(forms.ModelForm):
    ...
    orders = ModelMultipleChoiceField(..., widget=MyCheckboxSelectMultiple)

Suppose that myapp/detail_options.html contained the following

{# include the default behavior #}
{% include "django/forms/widgets/input_option.html" %} 
{# add your own additional div for each option #}
<div style="background-color: blue">
    <h2>Additional info</h2>
</div>

You would see that blue div after each label/input. Something like this

Custom option template

Now, the trick will be how you get the object available to the widget namespace. By default, only certain attributes are present on a widget, as returned by the widget's get_context method.

You can use your own subclass of MultipleModelChoiceField and override label_from_instance to accomplish this. The value returned by label_from_instance is ultimately made available to the widgets as the label attribute, which is used for the visible text in your model form, when it renders {{ widget.label }}.

Simply override label_from_instance to return the entire object then use this subclass for your field.

class MyModelMultipleChoiceField(forms.ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return obj

class CreateRunForm(forms.ModelForm):
    ...
    orders = MyModelMultipleChoiceField(..., widget=MyCheckboxSelectMultiple)

So now in the myapp/detail_options template you can use widget.label to access the object directly and format your own content as you please. For example, the following option template could be used

{% include "django/forms/widgets/input_option.html" %}
{% with order=widget.label %}
    <div style="background-color: blue">
        <h2>Order info</h2>
        <p style="color: red">Order Active: {{ order.is_active }}</p>
        <p style="color: red">Order Loaded: {{ order.is_loaded }}</p>
    </div>
{% endwith %}

And it would produce the following effect.

object hack

This also will not disrupt the default behavior of the widget label text wherever widget.label is used. Note that in the above image the label texts (e.g. Order object (1)) are the same as before we applied the change to label_from_instance. This is because the default in template rendering is to use str(obj) when presented with a model object; the same thing that would have previously been done by the default label_from_instance.

TL;DR

  • Make your own subclasses of ModelMultiplechoiceField to have label_from_instance return the object.
  • Make your own SelectMultiple widget subclass to specify a custom option_template_name.
  • The template specified will be used to render each option, where widget.label will be each object.
like image 77
sytech Avatar answered Oct 31 '22 15:10

sytech