Loop over ManyToMany with through and IntegerChoices

I write an app where players can be added to events.

class Player(models.Model):
    name = models.CharField(max_length=30, unique=True)

class Event(models.Model):
    title = models.CharField(max_length=100, blank=True)
    players = models.ManyToManyField(Player, through='Participation', blank=True)

I started with the default ManyToManyField but added a Participation model to add a certainty to the relation.

class Participation(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    player = models.ForeignKey(Player, on_delete=models.CASCADE)

    class Certainty(models.IntegerChoices):
        CANCELLED = -1, _('cancelled')
        UNKNOWN = 0, _('unknown')
        UNCERTAIN = +1, _('uncertain')
        CONFIRMED = +2, _('confirmed')

    certainty = models.IntegerField(choices=Certainty.choices, default=Certainty.UNKNOWN)

Before the Participiaton model was introduced, I looped over event.players to print a list of players for that event using player.name in the template. After adding Participaton, the only way I could get the certainty was by looping over event.participation_set.all instead.

<ul class="player_list">
    {% for participation in event.participation_set.all %}
      <li class="participation_{{ participation.certainty }}">{{ participation.player.name }}</li>
    {% endfor %}
</ul>

  1. Is there a way to loop over the players as before but get the certainty through reverse relations? The problem is to filter out the entry for the specific event. Would that be less efficient compared to looping over the participation itself?
  2. The names of the players are to be formatted according to their certainty, e.g. red for cancelled, green for confirmed (probably with a tooltip that spells it out). I would like to use the labels defined in the IntegerChoices but only got the integer values working so far. Is there a built-in solution to get the label (within the template)? Alternatively, I could define a method, manipulate the context, or define a filter, I suppose.

Thanks for the help!

Referencing the Participation from the Player: This is a straight-forward reverse ForeignKey relationship. You should be able to access the Participation set as player.participation_set.all(). If you just want a certain category, you can filter on the participation set.

confirmed_events = a_player.participation_set.filter(certainty=2)

Generally speaking, crafting the proper query is going to be more efficient than looping through the data in the code.

For the IntegerChoices, The .label attribute lets you retrieve the label name instead of the integer value.

Thanks for your reply!


If you just want a certain category, you can filter on the participation set. Generally speaking, crafting the proper query is going to be more efficient than looping through the data in the code.

Sorry, I was probably unclear what I want.

I want for a specific event (which may be from a list of all events or some subset totally unrelated to players) to list all players and their certainty (via CSS and/or tooltips). For a given player from that event I would have to find the participation entry corresponding to both that player and the given event. I was so far unable to do so with template tools alone except by changing the loop itself. As far as I know one cannot user the filter method within a template because it requires arguments, is that right? I’m curious whether it is possible to go through player or not.


For the IntegerChoices, The .label attribute lets you retrieve the label name instead of the integer value.

I forgot to mention that the label attribute does not work. Or I’m using it wrong. As I understand it, participaton.certainty is just an integer and therefore does not have the label attribute. A possible way is to instantiate the inner class

certainty_label = Participation.Certainty(self.certainty).label

but I don’t know how to do that within the template. Using a registered filter maybe? Mostly, I have the feeling that there should be a natural way to get the label of an IntegerChoice in the template, I just cannot find it in the documentation.

(Just got the idea to look into the source of the choice widgets. They must get it somehow.)

The template is the wrong place to be performing any kind of that logic. The general principle is that all the “business logic” exists in the view - the template exists only to format the data to be rendered. You want to build all your logic / filters, building all your lists and sets in the view. Then create the page from that context data.

So the point is to not do any of what you’re looking to do in the template. That logic belongs in the view.

Thanks again, that is indeed something I must remember.

I find it difficult to realize what is business logic and what is not. For example, I would have thought that labels for an integer field are formatting and not business logic, and would therefore be appropriate in the template. But as I understand your answer, I must replace the integer values with their labels manually within the view. I would have thought that labels are the natural way of displaying such information and would have some automatism.

I’m sorry if I created any confusion - we’re talking about two different topics and I’m guessing I wasn’t clear about which topic I was addressing with my previous response.

Earlier on, you wrote:

It is that paragraph I was trying to address when I made my comment referring to a separation of responsibilities between the view and the templates. You don’t want to apply a filter in a template because the logic for that doesn’t belong in the template - it belongs in the view where the context is being created to be rendered by a template.

Regarding the use of an IntegerChoices field as an inner class within a model - I’ve never done that, so I don’t have any experience with retrieving the labels in a template. Since the certainty field itself just an IntegerField, I can understand why you can’t get a label from that - that makes sense.
For your template to be able to render the label, it would need to have an instance of the Certainty class, and then reference the value based on the certainty field. I don’t know how you would express that in the template reference syntax.

Ken

The closest I have come to a generalized solution is the addition of a model method to return the label.

Example:

class Participation(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    player = models.ForeignKey(Player, on_delete=models.CASCADE)

    class Certainty(models.IntegerChoices):
        CANCELLED = -1, _('cancelled')
        UNKNOWN = 0, _('unknown')
        UNCERTAIN = +1, _('uncertain')
        CONFIRMED = +2, _('confirmed')

    certainty = models.IntegerField(choices=Certainty.choices, default=Certainty.UNKNOWN)

    def certainty_label(self):
        return self.Certainty(self.certainty).label

If you then have an entity named participation in your context being passed to the template, you can then render participation.certainty_label

But to circle back around to this -

If instead of being an IntegerField the field were a PostgreSQL ENUM field (or the equivalent in a different database), I would agree with you. But if you look at this from the perspective of the database, the only information in the database is the Integer value being stored.

Keep in mind that within Django, the choices option within the field definition could be a callable! (Directly quoting the documentation: “Note that choices can be any sequence object – not necessarily a list or tuple.”) This makes the definition of the allowable choices to be dynamic, and not limited to a fixed set of options - and that makes the set of labels associated with any Integer value a “business logic” issue and not a static template issue.