Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Dynamic tables (Matrixes)

This tutorial shows how to use tables with dynamic columns. It also introduces a usage example for Introduction to actor parameters.

It extends the application created in Introduction to tables (so you might follow that tutorial before reading on here).

We add an EntryType model and a CompaniesWithEntryTypes table which is a matrix with one row per Company and one column per EntryType.


This table shows for each cell the number of entries (of that company and that type).

TODO: clicking on the number in every cell should open the list of these entries.

Doctest initialization:

>>> from lino import startup
>>> startup('lino_book.projects.watch2.settings')
>>> from lino.api.doctest import *

Check some permissions:

>>> pprint(list(rt.models.watch2.Companies.required_roles))
[<class 'lino.core.roles.SiteUser'>]
>>> from lino.core.roles import SiteUser
>>> albert = rt.models.users.User.objects.get(username="Albert")
>>> rt.models.watch2.Companies.get_view_permission(albert.user_type)

Here is our models.py module. We also introduce filter parameters for the Entries table.

# Copyright 2013-2018 Rumma & Ko Ltd
# License: GNU Affero General Public License v3 (see file COPYING for details)

from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _

from lino.api import dd
from lino import mixins
from lino.modlib.users.mixins import UserAuthored

# contacts = dd.resolve_app('contacts')

class Company(dd.Model):

    class Meta:
        verbose_name = _("Company")
        verbose_name_plural = _("Companies")

    name = models.CharField(_("Name"), blank=True, max_length=200)
    street = models.CharField(_("Street"), blank=True, max_length=200)
    city = models.CharField(_("City"), blank=True, max_length=200)

    def __str__(self):
        return self.name

class EntryType(mixins.BabelDesignated):

    class Meta:
        verbose_name = _("Entry Type")
        verbose_name_plural = _("Entry Types")

class EntryTypes(dd.Table):
    model = EntryType

class Entry(UserAuthored):

    class Meta:
        verbose_name = _("Entry")
        verbose_name_plural = _("Entries")

    date = models.DateField(_("Date"))
    entry_type = dd.ForeignKey(EntryType)
    subject = models.CharField(_("Subject"), blank=True, max_length=200)
    body = dd.RichTextField(_("Body"), blank=True)
    company = dd.ForeignKey(Company)

class Entries(dd.Table):
    model = Entry
    detail_layout = """
    id user date company
    insert_layout = """
    user date company
    parameters = mixins.ObservedDateRange(
            help_text=_("Show only entries of this type.")),
            help_text=_("Show only entries of this company.")),
                           help_text=_("Show only entries by this user.")))
    params_layout = """
    user start_date end_date
    company entry_type

    def get_request_queryset(cls, ar):
        qs = super().get_request_queryset(ar)
        if (pv := ar.param_values) is None: return qs
        if pv.end_date:
            qs = qs.filter(date__lte=pv.end_date)
        if pv.start_date:
            qs = qs.filter(date__gte=pv.start_date)
        if pv.user:
            qs = qs.filter(user=pv.user)
        if pv.entry_type:
            qs = qs.filter(entry_type=pv.entry_type)
        if pv.company:
            qs = qs.filter(company=pv.company)
        return qs

    def param_defaults(cls, ar, **kw):
        kw = super(Entries, cls).param_defaults(ar, **kw)
        return kw

class EntriesByCompany(Entries):
    master_key = 'company'

class CompanyDetail(dd.DetailLayout):
    main = """
    street city

class Companies(dd.Table):
    model = Company
    detail_layout = CompanyDetail()

class CompaniesWithEntryTypes(dd.VentilatingTable, Companies):
    label = _("Companies with Entry Types")
    hide_zero_rows = True
    parameters = mixins.ObservedDateRange()
    params_layout = "start_date end_date"
    editable = False
    auto_fit_column_widths = True

    def param_defaults(cls, ar, **kw):
        kw = super(CompaniesWithEntryTypes, cls).param_defaults(ar, **kw)
        return kw

    def get_ventilated_columns(self):

        def w(et):
            # return a getter function for a RequestField on the given
            # EntryType.

            def func(fld, obj, ar):
                #~ mi = ar.master_instance
                #~ if mi is None: return None
                pv = dict(start_date=ar.param_values.start_date,
                if et is not None:
                pv.update(company=obj, user=ar.get_user())
                return Entries.request(param_values=pv)

            return func

        for et in EntryType.objects.all():
            yield dd.RequestField(w(et), verbose_name=str(et))
        yield dd.RequestField(w(None), verbose_name=_("Total"))

@dd.receiver(dd.post_save, sender=EntryType)
def my_setup_columns(sender, **kw):

# @dd.receiver(dd.post_startup)
# def my_details_setup(sender, **kw):
#     self = sender
#     self.models.contacts.Companies.add_detail_tab(
#         'entries', 'watch2.EntriesByCompany')

You can play with this application as follows:

$ go watch2
$ python manage.py prep
$ python manage.py runserver

A known problem is that after changing or adding an EntryType, the server must restart before the modified column becomes visible.

Note also the fixtures/demo.py file which generates the demo entries using The Python serializer:

# Copyright 2013-2018 Rumma & Ko Ltd
# License: GNU Affero General Public License v3 (see file COPYING for details)

from lino.api import rt
from django.conf import settings

from lino.utils import Cycler

Entry = rt.models.watch2.Entry
EntryType = rt.models.watch2.EntryType
Company = rt.models.watch2.Company
User = rt.models.users.User

LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

def objects():

    kwargs = dict(user_type=rt.models.users.UserTypes.user)
    # kwargs = dict()

    yield User(username="Albert", **kwargs)
    yield User(username="Boris", **kwargs)
    yield User(username="Claire", **kwargs)

    yield Company(name="AllTech inc.")
    yield Company(name="BestTech inc.")
    yield Company(name="CoolTech inc.")

    yield EntryType(designation="Consultation")
    yield EntryType(designation="Evaluation")
    yield EntryType(designation="Test")
    yield EntryType(designation="Analysis")
    yield EntryType(designation="Observation")

    TYPES = Cycler(EntryType.objects.all())
    COMPANIES = Cycler(Company.objects.all())
    USERS = Cycler(User.objects.all())
    SUBJECTS = Cycler(LOREM_IPSUM.split())

    for i in range(200):
        d = settings.SITE.demo_date(-i)
        e = Entry(date=d,
        if i % 7:
            yield e
>>> rt.login("Albert").show(watch2.CompaniesWithEntryTypes)
==================== ============== ============ ======= ========== ============= ========
 Description          Consultation   Evaluation   Test    Analysis   Observation   Total
-------------------- -------------- ------------ ------- ---------- ------------- --------
 AllTech inc.         **3**          **3**        **4**   **2**      **2**         **14**
 BestTech inc.        **3**          **3**        **2**   **2**      **4**         **14**
 CoolTech inc.        **2**          **2**        **3**   **4**      **3**         **14**
 **Total (3 rows)**   **8**          **8**        **9**   **8**      **9**         **42**
==================== ============== ============ ======= ========== ============= ========