6

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?

Alex Lenail
  • 12,992
  • 10
  • 47
  • 79
  • Have you tried to use a custom widget @AlexLenail? https://stackoverflow.com/questions/44675550/django-widget-override-template/44678736#44678736 – John Moutafis Mar 21 '18 at 07:48

1 Answers1

6

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.
sytech
  • 29,298
  • 3
  • 45
  • 86
  • I can't get the first part of this to work, unfortunately. I change `detail_options.html` but can't get what's in there to render. All that renders is the default ModelMultipleChoiceField widget. I'm having a hard time figuring out how to debug this, since I've followed this answer exactly, and can't even get an error to lead me somewhere. Maybe this has to do with django-crispy-forms? – Alex Lenail Apr 15 '18 at 15:43
  • what should the root of the path for `option_template_name` be? I tried `/myapp/templates/orders/detail_options.html` as well as just `/orders/detail_options.html` and neither worked. Is there some way I can _force_ the app to break somehow which would tell me why it seems to be ignoring these changes? – Alex Lenail Apr 15 '18 at 17:33
  • To make this solution work, I did : 1. Add 'django.forms' to INSTALLED_APPS. 2. Add FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' to settings.py. as explained [here](https://stackoverflow.com/a/69955313/15060764) – AlexisLP Dec 07 '22 at 15:55