Search code examples
djangodjango-tables2

Is it possible to render a column for each of a many-to-many relationship with django-tables2?


It's simple enough to get all the many-to-many relationships of a particular row - but they're rendered in the same column.

Is it possible to have a column created for each one?

For example, with a many-to-many relationship between Child and Year, and data for each child/year pair, I'd want a column for each Year in the table:

Child       1998  1999  2000   
----------------------------        
Eldest       155   162   170
Youngest      53    61    72
Middlest      80    91   103

(many to many in django-tables2 is the closest I've seen to a question about this, and I think the accepted answer is wrong; it doesn't render a column for each relationship at all.)


Solution

  • I was able to work out a solution.

    The approach

    In the example below, I have a Child class, and Year object. I also have a Height class:

    class Height(models.Model):
        # a Child's Height for each Year
        child = models.ForeignKey(Child)
        year = models.ForeignKey(Year)
        height = DecimalField()
    

    To get a column for each year containing the child's height that year, you can supply extra_columns to the Table class, defining the extra columns you want.

    The way to do this is through the get_table_kwargs() method of a table view.

    You also need a Column subclass, with a render() method that can query the database to find the right value for the row/column (realising that I needed a new subclass was the key to this problem for me).

    The solution

    Start by defining a view and pointing it at the model that holds the data:

    class ChildrenTableView(tables.SingleTableView):
        model = Children
    
        def get_table_kwargs(self):
            year_height_columns = [
                (year.name, YearColumn(attrs={"year": year}))
                for year in Year.objects.all()
            ]
            return {"extra_columns": (year_height_columns)}
    

    For each Year, we want a YearColumn.

    attrs={"year": year} is a convenient way to supply the Year object to the column for use later in a query. (Depending on your data, you might also want to add an empty_values=() to the definition, so that the column doesn't falsely think it's empty.)

    YearColumn is a very simple subclass of Column:

    import django_tables2 as tables
    
    
    class YearColumn(tables.Column):
    
        def render(self, record):
            return record.height_set.get(year=self.attrs["year"]).height
    

    Here, record is the child for the row, and height_set is all the heights that already have a foreign key to the child.