Displaying checked and unchecked ManyToMany choices in DetailView

Viewed 56

I'm looking for an elegant way to show all available items (not just checked ones) in a Django DetailView, while stylizing the checked ones.

Given following model:

class Topping(models.Model):    
    title = models.CharField(max_length=200)

class Pizza(models.Model):
   topping = models.ManyToManyField(Topping, blank=True, null=True)

I'm rendering toppings in my form with a CheckboxSelectMultiple (=rendered as multiple checkboxes), like:

toppings = forms.ModelMultipleChoiceField(
    required=False,
    queryset=Toppings.objects.all(),
    widget=forms.CheckboxSelectMultiple,
)

It is also relevant which toppings were not selected, hence I'd like to render something in my DetailView like the following (showing a checked checkbox when selected; showing an empty checkbox and light gray text when not selected, respecting the same order as seen in the form, i.e. alphabetically in this example):

Chosen pizza toppings:
☑ Bluecheese
☐ Green peppers
☐ Onions
☑ Red peppers

I currently go through the toppings as follows, but this only shows the selected ones:

{% for option in object.toppings.all %}☑ {{ option }}<br />{% endfor %}

I have a whole bunch of different ManyToManyFields on my main model.

1 Answers

You can pass in the DetailView the toppings annotated with a boolean that determines if the Toppings have been selected. We can do this for example with an Exists subquery [Django-doc]:

from django.db.models import Exists, OuterRef
from django.views.generic import DetailView

class PizzaDetailView(DetailView):
    model = Pizza
    # …
    
    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        item = self.object
        context['toppings'] = Topping.objects.annotate(
            has_topping=Exists(
                Pizza.topping.through.objects.filter(
                    topping_id=OuterRef('pk'),
                    pizza_id=self.object.pk
                )
            )
        )
        return context

Then in the template, you can render this with:

{% for topping in toppings %}
    {% if topping.has_topping %}&#x2611;{% else %}&#x2610;{% endif %} {{ topping }}<br/>
{% endfor %}
Related