Thursday, March 6, 2025

I’m working on #5964 (pm prep in avanti1 says ‘str’ object has no attribute ‘get_layout_handle’)

In lino_xl/lib/courses/models.py we had:

@dd.requestfield(_("Enrolments"))
def enrolments(self, ar):
    return self.get_enrolments(start_date=dd.today())

def get_enrolments(self, **pv):
    return rt.models.courses.EnrolmentsByCourse.create_request(self,
                                                        param_values=pv)

The issue went away when I added “if ar is None: return” in the enrolments virtual field before calling get_enrolments().

But why? And why did it start only now?

And the same error message happens when I say doctest docs/specs/avanti/courses.rst, this time it is when courses/Course/presence_sheet.weasy.html says:

{% for enrolment in obj.get_enrolments(
   start_date=ar.action_param_values.start_date,
   end_date=ar.action_param_values.end_date) %}

Note that get_enrolments() accesses rt.models.courses.EnrolmentsByCourse. This is an actor, more precisely a table. And avanti redefines this table.

Lino is trying to setup the actor handle for the EnrolmentsByCourse from lino_xl.lib.courses instead of lino_avanti.lib.courses.

Aha! Explanation: It’s an aftermath of #5956 (python-tools no longer works). One of those automatic formatters that came with pulsar-ide-python, which I installed to replace the orphaned python-tools, insists on moving all global imports to the top of the file. Including these ones:

from .ui import *

But these imports must come at the end of the models.py file, at least for lino_avanti.lib.courses.

No. They don’t need to be at the end of models.py file, it’s enough if they are after the:

from lino_xl.lib.courses.models import *

Should we stop using from .ui import * ?

Most people say that from x import * is bad code style. But we use from .ui import * as a feature in a well-defined context in order to follow another code style principle which is “don’t repeat yourself”. We deliberately merge database models and actors into a same pot (the plugin’s namespace) because it makes documentation more concise. For application developers it makes no sense to differentiate between models, tables, choicelists because these are implementation details. But the more I think about this I realize that it’s a controversial topic, and for a developer who is used to relying on their IDE it can be a serious obstacle for diving into Lino.

I started to move all from .ui import * lines from the bottom of the models.py file to its top. But actually I have no clear plan yet. Even if we would name them explicitly, pyflakes would still complain because we import them and then don’t use them.

There is another topic, related to this: we actually have two namespaces per plugin: dd.plugins.foo and rt.models.foo. Their difference is that dd.plugins is available as soon as the plugins have been installed while rt.models is available only when Django has populated its database models. The items of lino.api.dd.plugins are instances of lino.core.plugins.Plugin while the items of lino.api.rt.models are the models.py module objects.

Once upon a time we had the actors collected into a namespace rt.actors, separately from rt.models. This used a automagic behaviour that we copied from Django: Django, when starting up, loops over INSTALLED_APPS and tries to import a module appname + ".models".

I had a look at django.apps.registry and I am tempted to try to review the Lino startup process because Django has evolved during the last 10 years. Let lino.core.plugin.Plugin inherit from AppConfig and use apps.get_app_config() instead of dd.plugins. Override AppConfig.import_models() so that it also tries to import a “ui” module, similar to what it does with a “models” module.

That would be fun! I’m really tempted. But there are more urgent things to do right now.