[Django]-Django – How to display a matrix of data, with sums at the end of each row and column

3👍

You should avoid doing anything else than presentation in the templates. My suggestion is that you build a few convenient data structures in your view and just iterate over them in the template:

  • a list of “Persons” (to create the table header)
  • a dictionary of items, where the item is the key (you can use the string representation of the item to keep things simpler) and the value is an ordered list of items in the same order the list at the previous point is ordered; you could also append the total to this list.

This makes it easier to work with things in the template, you’d have something like:

<table>
    <thead>
        <tr>
            <td>Item name</td>
            {% for person in person_list %}
                <td>{{ person.name }}</td>
            {% endfor %}
            <td>Total</td>
        </tr>
    </thead>
    <tbody>
        {% for item, item_list in item_dict.items %}
            <tr>
                <td>{{ item }}</td>
                {% for i in item_list %}
                    <td>{{ i }}</td>
                {% endfor %}
            </tr>
        {% endfor %}
    <tbody>
</table>

In order to integrate your categories, the dictionary structure can be further nested in a category dictionary that has data (the dict above) and also a list of totals.

Bottom line to what I’m suggesting is that you should build all your structures inside your view in such a way that displaying them is a breeze in the template, and not the other way around

0👍

I made a working example as per your models. Here are excerpts with explanations.

This is what your view would look like:

from django.shortcuts import render
from .models import *

def index(request):
  all_selections = Selection.objects.all()
  all_people = set([selection.person for selection in all_selections])
  blank_person_counts = {person: 0 for person in all_people}
  all_categories = set([selection.item.category for selection in all_selections])

  categories = []

  for category in all_categories:
    selections = [s for s in all_selections if s.item.category == category]

    total_by_person = blank_person_counts.copy()

    unique_item_details = set([s.item for s in selections])

    items = [{'detail': item_details} for item_details in unique_item_details]
    total_by_item = {item['detail']: 0 for item in items}
    total_by_item_person = {item['detail']: blank_person_counts.copy() for item in items}

    for selection in selections:
      total_by_item_person[selection.item][selection.person] = selection.quantity
      total_by_item[selection.item] += selection.quantity
      total_by_person[selection.person] += selection.quantity

    for item in items:
      item['people_totals'] = [total_by_item_person[item['detail']][person] for person in all_people]
      item['total'] = total_by_item[item['detail']]

    people_totals = [total_by_person[person] for person in all_people]
    category_total = sum(people_totals)

    categories.append({
      'detail': category.name,
      'people_totals': people_totals,
      'total': category_total,
      'items': items
    })

  context = {'people': all_people, 'categories': categories}
  return render(request, 'matrix.html', context)

I started by getting all_selections because I assumed if an item was never selected or if a person never selected a category, they shouldn’t be included, so the selections are the core of the data.

From the obtained selections, I generated a list of people (all_people). Having this defined before we go into items helps us make sure that every person is accounted for in each item. Even if a person didn’t select an item, we will assign them a quantity of zero for that item. It also sets the order in which people are displayed as columns and we will use the same order when returning totals by people for each item and for each category.

Since the data is divided by category, we generate a list of categories from the list of selections. We then loop over each category, filter selections by that category, create a blank slate in which each item was selected by each person zero times. When then go over the actual selections and update the counts with the quantities. We also maintain running totals per person per category. In the end, we sum up the running totals per person as that will be the grand total for the category. We repeat the same process for each category.

In the template, it’s basically a matter of looping over this data and displaying it:

<h1>The Grocery Matrix</h1>

<table>
  <tr>
    <th></th>
    {% for person in people %}
      <th>{{ person }}</th>
    {% endfor %}
    <th>Total</th>
  </tr>

  {% for category in categories %}
    {% for item in category.items %}
    <tr>
      <td>{{ item.detail }}</td>
      {% for person_total_for_item in item.people_totals %}
        <td>{{ person_total_for_item }}</td>
      {% endfor %}
      <td>{{ item.total }}</td>
    </tr>
    {% endfor %}


    <tr>
      <td>{{ category.detail }}</td>
      {% for person_total_for_category in category.people_totals %}
        <td>{{ person_total_for_category }}</td>
      {% endfor %}
      <td>{{ category.total }}</td>
    </tr>

  {% endfor %}
</table>

</body>
</html>

Leave a comment