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)
True
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
subject
body
"""
insert_layout = """
user date company
subject
"""
parameters = mixins.ObservedDateRange(
entry_type=dd.ForeignKey(
EntryType,
blank=True,
null=True,
help_text=_("Show only entries of this type.")),
company=dd.ForeignKey(
Company,
blank=True,
null=True,
help_text=_("Show only entries of this company.")),
user=dd.ForeignKey(settings.SITE.user_model,
blank=True,
null=True,
help_text=_("Show only entries by this user.")))
params_layout = """
user start_date end_date
company entry_type
"""
@classmethod
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
@classmethod
def param_defaults(cls, ar, **kw):
kw = super(Entries, cls).param_defaults(ar, **kw)
kw.update(user=ar.get_user())
return kw
class EntriesByCompany(Entries):
master_key = 'company'
class CompanyDetail(dd.DetailLayout):
main = """
name
street city
EntriesByCompany
"""
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
@classmethod
def param_defaults(cls, ar, **kw):
kw = super(CompaniesWithEntryTypes, cls).param_defaults(ar, **kw)
kw.update(end_date=settings.SITE.today())
return kw
@classmethod
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,
end_date=ar.param_values.end_date)
if et is not None:
pv.update(entry_type=et)
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):
CompaniesWithEntryTypes.setup_columns()
# @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,
company=COMPANIES.pop(),
user=USERS.pop(),
subject=SUBJECTS.pop(),
entry_type=TYPES.pop())
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**
==================== ============== ============ ======= ========== ============= ========