Welcome | Get started | Dive into Lino | Contribute | Topics | Reference | More

Remote fields

This page explains a concept we call remote fields.

This page is a tested document and the following instructions are used for initialization:

>>> from lino import startup
>>> startup('lino_book.projects.apc.settings.demo')
>>> from lino.api.doctest import *
>>> from django.db.models import Q
>>> translation.activate("en")

Django has lookups that span relationships

Let's say you want to see your sales invoices to clients in Eupen.

For this you can do the following:

>>> eupen = countries.Place.objects.get(name="Eupen")
>>> qs = trading.VatProductInvoice.objects.filter(partner__city=eupen)
>>> qs.count()
44

Above code is equivalent to (but more efficient than) the following code:

>>> len([invoice for invoice in trading.VatProductInvoice.objects.all()
...    if invoice.partner.city == eupen])
44

This is plain Django knowledge, documented in Lookups that span relationships.

Lino extends this idea by allowing to also specify layout elements using this syntax.

For example if you want, in your trading.Invoices table, a column showing the city of the partner of each invoice, you can simply specify partner__city as a field name in your column_names.

>>> rt.show(trading.Invoices,
...   column_names="id partner partner__city total_incl", limit=5)
===== ================== ============= ==============
 ID    Partner            Locality      Total to pay
----- ------------------ ------------- --------------
 177   da Vinci David     4730 Raeren   1 314,03
 176   da Vinci David     4730 Raeren   647,35
 175   di Rupo Didier     4730 Raeren   338,80
 174   Radermacher Jean   4730 Raeren   822,57
 173   Radermacher Inge   4730 Raeren   2 468,18
                                        **5 590,93**
===== ================== ============= ==============
remote field

A field-like object that points to a field on a related model.

An instance of lino.core.fields.RemoteField, created during startup for each layout element with a name that contains at least one double underscore.

A chain of subfields to be walked through for each row.

The intermediate subfields of a remote field must be pointers (either ForeignKey or OneToOneField). The last subfield is called the leaf field.

The leaf field can be a normal database field or a virtual field.

leaf field

The last subfield of a remote field. This field determines the return type of the remote field itself.

remote virtual field

A remote field that points to a virtual field.

You can use remote fields also in a detail layout. When all their intermeditate subfields are OneToOneField, they are editable. For example the detail layout of partners in Lino Voga has a field salesrule__paper_type where you can set the paper type to be used for this partner in new invoices. In this case, Partner.salesrule is a OneToOneField pointing to the one and only lino_xl.lib.invoicing.SalesRule instance for this partner.

You can also use remote fields as actor parameters. This is useful mostly when the leaf field is either a ForeignKey or a ChoiceListField. For example, lino_xl.lib.orders.Order.get_simple_parameters() defines journal__room as simple actor parameter:

class Order(...):

    @classmethod
    def get_simple_parameters(cls):
        for f in super(Order, cls).get_simple_parameters():
            yield f
        yield 'journal__room'

You can even use a remote field pointing to a virtual field as actor parameter, But that virtual field must have a return_type of ForeignKey, and you must also write a lino.core.model.Model.setup_parameters() method on your model that defines this field. That's because remote fields are kind of volatile fields that get created on the fly when a layout asks for them. In Lino Presto we have a usage example where we add a parameter field project__municipality to tables on the cal.Event model. That why we extend the cal.Event model:

from lino.core.fields import make_remote_field

@classmethod
def setup_parameters(cls, params):
    super(Event, cls).setup_parameters(params)
    params['project__municipality'] = make_remote_field(cls, 'project__municipality')

@classmethod
def get_request_queryset(cls, ar, **filter):
    qs = super(Event, cls).get_request_queryset(ar, **filter)
    pv = ar.param_values
    if pv.project__municipality:
        places = pv.project__municipality.whole_clan()
        qs = qs.filter(project__isnull=False, project__city__in=places)
    return qs

catch_layout_exceptions

Some general documentation about catch_layout_exceptions.

This setting tells Lino what to do when it encounters a wrong field name in a layout specification. It will anyway raise an Exception, but the difference is the content of the error message.

The default value for this setting is True. In that case the error message reports only a summary of the original exception and tells you in which layout it happens. Because that's your application code and probably the place where the bug is hidden.

>>> settings.SITE.catch_layout_exceptions
True

For example:

>>> rt.show(trading.Invoices,
...   column_names="id partner foo total_incl")
Traceback (most recent call last):
  ...
Exception: Invalid data element 'foo' in lino.core.layouts.ColumnsLayout on lino_xl.lib.trading.ui.Invoices
>>> rt.show(trading.Invoices,
...   column_names="id partner partner__foo total_incl")
Traceback (most recent call last):
  ...
Exception: Invalid data element 'partner__foo' in lino.core.layouts.ColumnsLayout on lino_xl.lib.trading.ui.Invoices
>>> settings.SITE.catch_layout_exceptions = False
>>> rt.show(trading.Invoices,
...   column_names="id partner partner__foo total_incl")  
Traceback (most recent call last):
  ...
Exception: Invalid RemoteField contacts.Partner.partner__foo (no field foo in contacts.Partner)

Skipped because after 20200430 there is no longer a difference in the exception message.

Notes

Note that Lino's remote field has nothing to do with Django's remote_field of a ForeignKey field.