lino 25.2.2__py3-none-any.whl → 25.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. lino/__init__.py +8 -3
  2. lino/api/dd.py +11 -35
  3. lino/api/doctest.py +49 -17
  4. lino/api/selenium.py +1 -1
  5. lino/core/actions.py +25 -23
  6. lino/core/actors.py +52 -23
  7. lino/core/choicelists.py +10 -8
  8. lino/core/dbtables.py +1 -1
  9. lino/core/elems.py +47 -31
  10. lino/core/fields.py +19 -9
  11. lino/core/kernel.py +26 -20
  12. lino/core/model.py +27 -16
  13. lino/core/renderer.py +2 -2
  14. lino/core/requests.py +103 -56
  15. lino/core/site.py +5 -5
  16. lino/core/store.py +5 -2
  17. lino/core/utils.py +12 -7
  18. lino/help_texts.py +7 -8
  19. lino/mixins/duplicable.py +6 -4
  20. lino/mixins/sequenced.py +17 -6
  21. lino/modlib/__init__.py +0 -2
  22. lino/modlib/changes/models.py +21 -10
  23. lino/modlib/checkdata/models.py +59 -24
  24. lino/modlib/comments/fixtures/demo2.py +12 -3
  25. lino/modlib/comments/models.py +7 -7
  26. lino/modlib/comments/ui.py +8 -5
  27. lino/modlib/export_excel/models.py +7 -5
  28. lino/modlib/extjs/__init__.py +2 -2
  29. lino/modlib/extjs/views.py +66 -22
  30. lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
  31. lino/modlib/jinja/mixins.py +73 -0
  32. lino/modlib/jinja/models.py +6 -0
  33. lino/modlib/linod/__init__.py +1 -0
  34. lino/modlib/linod/choicelists.py +21 -0
  35. lino/modlib/linod/consumers.py +13 -4
  36. lino/modlib/linod/fixtures/__init__.py +0 -0
  37. lino/modlib/linod/fixtures/linod.py +32 -0
  38. lino/modlib/linod/management/commands/linod.py +6 -2
  39. lino/modlib/linod/mixins.py +18 -14
  40. lino/modlib/linod/models.py +4 -2
  41. lino/modlib/memo/mixins.py +2 -1
  42. lino/modlib/memo/parser.py +1 -1
  43. lino/modlib/notify/models.py +19 -11
  44. lino/modlib/printing/actions.py +47 -42
  45. lino/modlib/printing/choicelists.py +17 -15
  46. lino/modlib/printing/mixins.py +22 -20
  47. lino/modlib/publisher/models.py +5 -5
  48. lino/modlib/summaries/models.py +3 -2
  49. lino/modlib/system/models.py +28 -29
  50. lino/modlib/uploads/__init__.py +14 -11
  51. lino/modlib/uploads/actions.py +2 -8
  52. lino/modlib/uploads/choicelists.py +10 -10
  53. lino/modlib/uploads/fixtures/std.py +17 -0
  54. lino/modlib/uploads/mixins.py +20 -8
  55. lino/modlib/uploads/models.py +62 -38
  56. lino/modlib/uploads/ui.py +15 -9
  57. lino/utils/__init__.py +0 -1
  58. lino/utils/jscompressor.py +4 -4
  59. lino/utils/media.py +45 -23
  60. lino/utils/report.py +5 -4
  61. lino/utils/restify.py +2 -2
  62. lino/utils/soup.py +26 -8
  63. lino/utils/xml.py +19 -5
  64. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/METADATA +1 -1
  65. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/RECORD +68 -65
  66. lino/mixins/uploadable.py +0 -3
  67. lino/utils/requests.py +0 -55
  68. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/WHEEL +0 -0
  69. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/AUTHORS.rst +0 -0
  70. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/COPYING +0 -0
lino/__init__.py CHANGED
@@ -26,7 +26,7 @@ defines no models, some template files, a series of :term:`django-admin commands
26
26
 
27
27
  """
28
28
 
29
- __version__ = '25.2.2'
29
+ __version__ = '25.3.0'
30
30
 
31
31
  # import setuptools # avoid UserWarning "Distutils was imported before Setuptools"?
32
32
 
@@ -64,14 +64,19 @@ import warnings
64
64
 
65
65
  warnings.filterwarnings(
66
66
  "error",
67
- "DateTimeField .* received a naive datetime (.*) while time zone support is active.",
67
+ r"DateTimeField .* received a naive datetime (.*) while time zone support is active.",
68
68
  RuntimeWarning,
69
69
  "django.db.models.fields",
70
70
  )
71
71
 
72
+ # TODO: Is it okay to ignore the followgin warning? It's because e.g.
73
+ # lino.modlib.excerpts has a pre_analyze receiver set_excerpts_actions(), which
74
+ # accesses the database to set actions before Lino analyzes the models. It
75
+ # catches OperationalError & Co because --of course-- it fails e.g. for
76
+ # admin-commands like "pm prep".
72
77
  warnings.filterwarnings(
73
78
  "ignore",
74
- "Accessing the database during app initialization is discouraged\. To fix this warning, avoid executing queries in AppConfig\.ready\(\) or when your app modules are imported\.",
79
+ r"Accessing the database during app initialization is discouraged\. To fix this warning, avoid executing queries in AppConfig\.ready\(\) or when your app modules are imported\.",
75
80
  RuntimeWarning,
76
81
  "django.db.backends.utils",
77
82
  )
lino/api/dd.py CHANGED
@@ -1,7 +1,15 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2011-2023 Rumma & Ko Ltd
2
+ # Copyright 2011-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
+ # from lino.modlib.linod.choicelists import background_task, schedule_often, schedule_daily
6
+ from lino.core.roles import SiteStaff, SiteUser, SiteAdmin, login_required
7
+ from django.utils import translation
8
+ from importlib import import_module
9
+ from lino.modlib.system.choicelists import Genders, PeriodEvents, YesNo
10
+ from lino.utils.mldbc.fields import BabelCharField, LanguageField
11
+ from lino.utils.mldbc.fields import BabelTextField
12
+ from lino.utils.format_date import fdl as dtosl
5
13
  from lino import logger
6
14
 
7
15
  # import logging ; logger = logging.getLogger(__name__)
@@ -96,7 +104,8 @@ from lino.core.fields import CharField
96
104
  from lino.core.utils import babelkw
97
105
 
98
106
  # from lino.core.utils import babelattr
99
- from lino.core.utils import babel_values # alias for babelkw for backward compat
107
+ # alias for babelkw for backward compat
108
+ from lino.core.utils import babel_values
100
109
 
101
110
  from lino.utils.choosers import chooser, action_chooser
102
111
 
@@ -156,18 +165,11 @@ def fds(d):
156
165
 
157
166
  # backward compatibility
158
167
  dtos = fds
159
- from lino.utils.format_date import fdl as dtosl
160
168
 
161
169
  babelitem = settings.SITE.babelitem
162
170
  field2kw = settings.SITE.field2kw
163
171
  # urlkwargs = settings.SITE.urlkwargs
164
172
 
165
- from lino.utils.mldbc.fields import BabelTextField
166
- from lino.utils.mldbc.fields import BabelCharField, LanguageField
167
-
168
- from lino.modlib.system.choicelists import Genders, PeriodEvents, YesNo
169
-
170
- from importlib import import_module
171
173
 
172
174
  decfmt = settings.SITE.decfmt
173
175
  str2kw = settings.SITE.str2kw
@@ -199,35 +201,9 @@ babelattr = settings.SITE.babelattr
199
201
  plugins = settings.SITE.plugins
200
202
  format_currency = settings.SITE.format_currency
201
203
 
202
- from django.utils import translation
203
204
 
204
205
  get_language = translation.get_language
205
206
 
206
- from lino.core.roles import SiteStaff, SiteUser, SiteAdmin, login_required
207
-
208
- from lino.modlib.linod.choicelists import Procedures
209
-
210
-
211
- def background_task(**kwargs):
212
- if "class_name" not in kwargs:
213
- kwargs["class_name"] = "linod.SystemTask"
214
-
215
- def decorator(func):
216
- Procedures.add_item(func, **kwargs)
217
- return func
218
-
219
- return decorator
220
-
221
-
222
- def schedule_often(every=10, **kwargs):
223
- kwargs.update(every_unit="secondly", every=every)
224
- return background_task(**kwargs)
225
-
226
-
227
- def schedule_daily(**kwargs):
228
- kwargs.update(every_unit="daily", every=1)
229
- return background_task(**kwargs)
230
-
231
207
 
232
208
  def auto_height(n):
233
209
  """
lino/api/doctest.py CHANGED
@@ -1,28 +1,45 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2015-2024 Rumma & Ko Ltd
2
+ # Copyright 2015-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """
5
- A selection of names to be used in tested documents.
5
+ A selection of names to be used in tested documents as follows:
6
+
7
+ >>> from lino.api.doctest import *
8
+
9
+ This is by convention everything we want to have in the global namespace of a
10
+ tested document. It includes
11
+
12
+ - well-known Python standard modules like os, sys, datetime and collections.
13
+ - A variable :data:`test_client`
14
+
6
15
  """
7
16
 
17
+ import os
18
+ import sys
19
+ import datetime
20
+ import collections
8
21
  import six # TODO: remove here and then run all doctests
9
22
  import logging
10
23
  import sqlparse
24
+ import json
25
+ import textwrap
26
+
27
+ from bs4 import BeautifulSoup
28
+ from pprint import pprint, pformat
11
29
  from urllib.parse import urlencode
30
+
12
31
  import django
13
32
  django.setup()
33
+
14
34
  from lino.core.constants import *
15
35
  from lino.api.shell import *
16
36
  from django.utils import translation
17
37
  from django.utils.encoding import force_str
18
38
  from django.test import Client
19
39
  from django.db import connection, reset_queries as reset_sql_queries
20
- import json
21
- from bs4 import BeautifulSoup
22
- import textwrap
23
- from pprint import pprint, pformat
24
40
 
25
- from rstgen import table, ul
41
+ import pytest
42
+ # from rstgen import table, ul
26
43
  import rstgen
27
44
  from rstgen import attrtable
28
45
  from rstgen.utils import unindent, rmu, sixprint
@@ -49,10 +66,12 @@ from lino.core.actions import register_params
49
66
  from lino.core.layouts import BaseLayout
50
67
 
51
68
  test_client = Client()
52
- # naming it simply "client" caused conflict with a
53
- # `lino_welfare.pcsw.models.Client`
69
+ """An instance of :class:`django.test.Client`.
54
70
 
55
- import collections
71
+ N.B. Naming it simply "client" caused conflict with a
72
+ :class:`lino_welfare.pcsw.models.Client`
73
+
74
+ """
56
75
 
57
76
  HttpQuery = collections.namedtuple(
58
77
  "HttpQuery", ["username", "url_base", "json_fields", "expected_rows", "kwargs"]
@@ -293,7 +312,7 @@ def show_workflow(actions, all=False, language=None):
293
312
  # required_roles
294
313
  ]
295
314
  )
296
- print(table(cols, cells).strip())
315
+ print(rstgen.table(cols, cells).strip())
297
316
 
298
317
  if language:
299
318
  with translation.override(language):
@@ -363,7 +382,7 @@ def fields_help(model, fieldnames=None, columns=False, all=None):
363
382
 
364
383
  # return table(cols, cells).strip()
365
384
  items = ["{} ({}) : {}".format(row[1], row[0], row[2]) for row in cells]
366
- return ul(items).strip()
385
+ return rstgen.ul(items).strip()
367
386
 
368
387
 
369
388
  def show_fields(*args, **kwargs):
@@ -552,7 +571,7 @@ def show_choicelist(cls):
552
571
  for i in cls.get_list_items():
553
572
  row = [i.value, i.name] + str2languages(i.text)
554
573
  rows.append(row)
555
- print(table(headers, rows))
574
+ print(rstgen.table(headers, rows))
556
575
 
557
576
 
558
577
  def show_choicelists():
@@ -568,7 +587,7 @@ def show_choicelists():
568
587
  i.verbose_name_plural
569
588
  )
570
589
  rows.append(row)
571
- print(table(headers, rows))
590
+ print(rstgen.table(headers, rows))
572
591
 
573
592
 
574
593
  def show_permissions(*args):
@@ -590,7 +609,7 @@ def show_translations(things, fmt, languages=None):
590
609
  x, txt = fmt(thing)
591
610
  cells.append(txt)
592
611
  rows.append(cells)
593
- print(table(headers, rows))
612
+ print(rstgen.table(headers, rows))
594
613
 
595
614
 
596
615
  def show_model_translations(*models, **kwargs):
@@ -779,7 +798,7 @@ def show_change_watchers():
779
798
  rows.append(
780
799
  [full_model_name(m), ws.master_key, " ".join(sorted(ws.ignored_fields))]
781
800
  )
782
- print(table(headers, rows, max_width=40))
801
+ print(rstgen.table(headers, rows, max_width=40))
783
802
 
784
803
  def show_display_modes():
785
804
  """
@@ -796,4 +815,17 @@ def show_display_modes():
796
815
  ("x" if dm in a.extra_display_modes else "")
797
816
  for dm in dml]
798
817
  )
799
- print(table(headers, rows))
818
+ print(rstgen.table(headers, rows))
819
+
820
+
821
+ def checkdb(m, num):
822
+ """
823
+ Raise an exception if the database doesn't contain the specified number of
824
+ rows of the specified model.
825
+
826
+ This is for usage in :xfile:`startup.py` scripts.
827
+
828
+ """
829
+ if m.objects.count() != num:
830
+ raise Exception(
831
+ f"Model {m} should have {num} rows but has {m.objects.count()}")
lino/api/selenium.py CHANGED
@@ -115,7 +115,7 @@ class Tour(object):
115
115
  error_message = None
116
116
  language = None
117
117
  languages = []
118
- server_url = "http://127.0.0.1:8000/"
118
+ server_url = "http://127.0.0.1:8000"
119
119
  images_width = 90
120
120
 
121
121
  def __init__(
lino/core/actions.py CHANGED
@@ -6,6 +6,19 @@ decorator, and some of the standard actions. See :ref:`dev.actions`.
6
6
 
7
7
  """
8
8
 
9
+ from .utils import InstanceAction
10
+ from .utils import traverse_ddh_fklist
11
+ from .utils import Parametrizable
12
+ from .utils import navinfo
13
+ from .utils import resolve_model
14
+ from .utils import obj2unicode
15
+ from .permissions import Permittable
16
+ from lino.utils.choosers import check_for_chooser
17
+ from lino.modlib.users.utils import get_user_profile
18
+ from lino.core import keyboard
19
+ from lino.core import fields
20
+ from lino.core import layouts
21
+ from lino.core import constants
9
22
  from lino import logger
10
23
 
11
24
  from django.utils.translation import gettext_lazy as _
@@ -20,21 +33,6 @@ from django.apps import apps
20
33
 
21
34
  get_models = apps.get_models
22
35
 
23
- from lino.core import constants
24
- from lino.core import layouts
25
- from lino.core import fields
26
- from lino.core import keyboard
27
- from lino.modlib.users.utils import get_user_profile
28
- from lino.utils.choosers import check_for_chooser
29
-
30
- from .permissions import Permittable
31
- from .utils import obj2unicode
32
- from .utils import resolve_model
33
- from .utils import navinfo
34
- from .utils import Parametrizable
35
- from .utils import traverse_ddh_fklist
36
- from .utils import InstanceAction
37
-
38
36
 
39
37
  def discover_choosers():
40
38
  logger.debug("Discovering choosers for database fields...")
@@ -55,7 +53,8 @@ def resolve_layout(cls, k, spec, layout_class, **options):
55
53
  else:
56
54
  layout_class = settings.SITE.models.resolve(spec)
57
55
  if layout_class is None:
58
- raise Exception("Unresolved {} {!r} for {}".format(k, spec, cls))
56
+ raise Exception(
57
+ "Unresolved {} {!r} for {}".format(k, spec, cls))
59
58
  return layout_class(None, cls, **options)
60
59
  elif isinstance(spec, layouts.Panel):
61
60
  options.update(spec.options)
@@ -69,7 +68,8 @@ def resolve_layout(cls, k, spec, layout_class, **options):
69
68
  "{}.{}.{} must be a string, " "a Panel or an instance of {} (not {!r})"
70
69
  )
71
70
  raise Exception(
72
- msg.format(cls.__module__, cls.__name__, k, layout_class.__name__, spec)
71
+ msg.format(cls.__module__, cls.__name__,
72
+ k, layout_class.__name__, spec)
73
73
  )
74
74
  if spec._datasource is None:
75
75
  spec.set_datasource(cls)
@@ -163,7 +163,7 @@ class Action(Parametrizable, Permittable):
163
163
  _params_layout_class = layouts.ActionParamsLayout
164
164
 
165
165
  label = None
166
- button_text = None
166
+ button_text: str = None
167
167
 
168
168
  button_color = None
169
169
  """
@@ -487,7 +487,8 @@ class Action(Parametrizable, Permittable):
487
487
 
488
488
  if self.icon_name:
489
489
  if self.icon_name not in constants.ICON_NAMES:
490
- raise Exception("Unkonwn icon_name '{0}'".format(self.icon_name))
490
+ raise Exception(
491
+ "Unkonwn icon_name '{0}'".format(self.icon_name))
491
492
 
492
493
  register_params(self)
493
494
 
@@ -496,7 +497,6 @@ class Action(Parametrizable, Permittable):
496
497
  if not c in constants.WINDOW_TYPES:
497
498
  raise Exception(f"Invalid window_type spec {c} in {self}")
498
499
 
499
-
500
500
  def __get__(self, instance, owner):
501
501
  """
502
502
  When a model has an action "foo", then getting an attribute
@@ -524,7 +524,8 @@ class Action(Parametrizable, Permittable):
524
524
  name,
525
525
  getattr(
526
526
  forms,
527
- mapping.get(field.__class__.__name__, field.__class__.__name__),
527
+ mapping.get(field.__class__.__name__,
528
+ field.__class__.__name__),
528
529
  )(),
529
530
  )
530
531
  return LinoForm
@@ -815,7 +816,7 @@ class ShowDetail(Action):
815
816
  help_text = _("Open a detail window on this record.")
816
817
  action_name = "detail"
817
818
  label = _("Detail")
818
- icon_name = "application_form"
819
+ # icon_name = "application_form"
819
820
  ui5_icon_name = "sap-icon://detail-view"
820
821
  opens_a_window = True
821
822
  window_type = constants.WINDOW_TYPE_DETAIL
@@ -1415,7 +1416,8 @@ class DeleteSelected(MultipleRowAction):
1415
1416
  d.update(type=ar.actor.model._meta.verbose_name_plural)
1416
1417
  if len(objects) > 10:
1417
1418
  objects = objects[:9] + ["..."]
1418
- msg = gettext("You are about to delete %(num)d %(type)s\n(%(targets)s)") % d
1419
+ msg = gettext(
1420
+ "You are about to delete %(num)d %(type)s\n(%(targets)s)") % d
1419
1421
 
1420
1422
  if len(cascaded_objects):
1421
1423
  lst = [
lino/core/actors.py CHANGED
@@ -75,7 +75,8 @@ def discover():
75
75
  actors_list = []
76
76
  actors_dict = AttrDict()
77
77
 
78
- logger.debug("actors.discover() : registering %d actors", len(actor_classes))
78
+ logger.debug("actors.discover() : registering %d actors",
79
+ len(actor_classes))
79
80
  for cls in actor_classes:
80
81
  register_actor(cls)
81
82
  actor_classes = None
@@ -751,7 +752,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
751
752
  if hasattr(cls, "display_mode"):
752
753
  # cls.default_display_modes = {k:v for k, v in cls.display_mode}
753
754
  # logger.info(f"{cls} uses deprecated `display_mode`, please convert to `default_display_modes`.")
754
- raise ChangedAPI(f"{cls} must convert `display_mode` to `default_display_modes`")
755
+ raise ChangedAPI(
756
+ f"{cls} must convert `display_mode` to `default_display_modes`")
755
757
 
756
758
  master = getattr(cls, "master", None)
757
759
  if isinstance(master, str):
@@ -795,6 +797,9 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
795
797
  # cls.extra_display_modes = cls.extra_display_modes | {v}
796
798
  edm.add(v)
797
799
 
800
+ if cls.hide_navigator:
801
+ return
802
+
798
803
  if cls.card_layout is not None:
799
804
  edm.add(constants.DISPLAY_MODE_CARDS)
800
805
  if 'row_as_paragraph' in cls.__dict__:
@@ -805,7 +810,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
805
810
  if model.extra_display_modes is not None:
806
811
  for v in model.extra_display_modes:
807
812
  if v not in constants.DISPLAY_MODES:
808
- raise Exception(f"Invalid display mode {v} in {model}.extra_display_modes")
813
+ raise Exception(
814
+ f"Invalid extra_display_modes mode {v} in {model}")
809
815
  edm |= model.extra_display_modes
810
816
  if 'as_paragraph' in model.__dict__:
811
817
  edm.add(constants.DISPLAY_MODE_LIST)
@@ -1097,14 +1103,16 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1097
1103
  cls.delete_action = cls._bind_action(
1098
1104
  "delete_action", DELETE_ACTION, True
1099
1105
  )
1100
- cls.update_action = cls._bind_action("update_action", UPDATE_ACTION, True)
1106
+ cls.update_action = cls._bind_action(
1107
+ "update_action", UPDATE_ACTION, True)
1101
1108
  if cls.detail_layout:
1102
1109
  cls.validate_form = cls._bind_action(
1103
1110
  "validate_form", VALIDATE_FORM, True
1104
1111
  )
1105
1112
 
1106
1113
  if is_string(cls.workflow_owner_field):
1107
- cls.workflow_owner_field = cls.get_data_elem(cls.workflow_owner_field)
1114
+ cls.workflow_owner_field = cls.get_data_elem(
1115
+ cls.workflow_owner_field)
1108
1116
  if is_string(cls.workflow_state_field):
1109
1117
  # if isinstance(cls.workflow_state_field, string_types):
1110
1118
  fld = cls.get_data_elem(cls.workflow_state_field)
@@ -1136,7 +1144,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1136
1144
  da = cls.get_default_action()
1137
1145
  if da is not None:
1138
1146
  if isinstance(da, actions.Action):
1139
- cls.default_action = cls._bind_action("default_action", da, True)
1147
+ cls.default_action = cls._bind_action(
1148
+ "default_action", da, True)
1140
1149
  else:
1141
1150
  cls.default_action = da
1142
1151
 
@@ -1300,7 +1309,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1300
1309
  if cls.model is None:
1301
1310
  return False
1302
1311
  if not isinstance(cls.model, type):
1303
- raise Exception("{}.model is {!r} (must be a class)".format(cls, cls.model))
1312
+ raise Exception(
1313
+ "{}.model is {!r} (must be a class)".format(cls, cls.model))
1304
1314
  return issubclass(cls.model, fields.TableRow)
1305
1315
 
1306
1316
  @classmethod
@@ -1417,13 +1427,15 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1417
1427
  return format_html("<div>{}</div>", text)
1418
1428
 
1419
1429
  @classmethod
1420
- def override_column_headers(self, ar, **kwargs):
1430
+ def override_column_headers(self, ar, **headers):
1421
1431
  """A hook to dynamically override the column headers. This has no
1422
1432
  effect on a GridPanel, only in printed documents or plain
1423
1433
  html.
1424
1434
 
1425
1435
  """
1426
- return kwargs
1436
+ if self.model is None:
1437
+ return headers
1438
+ return self.model.override_column_headers(ar, **headers)
1427
1439
 
1428
1440
  @classmethod
1429
1441
  def get_sum_text(self, ar, sums):
@@ -1494,11 +1506,13 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1494
1506
  else:
1495
1507
  name = "main"
1496
1508
  if name in kw:
1497
- raise Exception("set_detail() got two definitions for %r." % name)
1509
+ raise Exception(
1510
+ "set_detail() got two definitions for %r." % name)
1498
1511
  kw[name] = dtl
1499
1512
  else:
1500
1513
  if not isinstance(dtl, lcl):
1501
- msg = "{} is neither a string nor a layout".format(type(dtl))
1514
+ msg = "{} is neither a string nor a layout".format(
1515
+ type(dtl))
1502
1516
  raise Exception(msg)
1503
1517
  assert dtl._datasource is None
1504
1518
  # added for 20120914c but it wasn't the problem
@@ -1557,7 +1571,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1557
1571
  # disabled because UsersWithClients defines virtual fields
1558
1572
  # on connection_created
1559
1573
  if name in cls.virtual_fields:
1560
- raise Exception("Duplicate add_virtual_field() %s.%s" % (cls, name))
1574
+ raise Exception(
1575
+ "Duplicate add_virtual_field() %s.%s" % (cls, name))
1561
1576
  # assert vf.model is None
1562
1577
  # if vf.model is not None:
1563
1578
  # # inherit from parent actor
@@ -1896,7 +1911,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1896
1911
  @classmethod
1897
1912
  def get_table_as_list(cls, obj, ar):
1898
1913
  # raise Exception("20240316")
1899
- sar = cls.request(parent=ar, master_instance=obj, is_on_main_actor=False)
1914
+ sar = cls.request(parent=ar, master_instance=obj,
1915
+ is_on_main_actor=False)
1900
1916
  grp = Grouper(sar)
1901
1917
  html_text = grp.begin()
1902
1918
  for i, obj in enumerate(sar.data_iterator):
@@ -1930,7 +1946,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1930
1946
 
1931
1947
  @classmethod
1932
1948
  def get_table_story(cls, obj, ar):
1933
- sar = cls.request(parent=ar, master_instance=obj, is_on_main_actor=False)
1949
+ sar = cls.request(parent=ar, master_instance=obj,
1950
+ is_on_main_actor=False)
1934
1951
  html = SAFE_EMPTY
1935
1952
  for i, obj in enumerate(sar.data_iterator):
1936
1953
  if i == cls.preview_limit:
@@ -1948,17 +1965,27 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1948
1965
  return html
1949
1966
 
1950
1967
  @classmethod
1951
- def get_table_summary(cls, obj, ar=None):
1968
+ def get_slave_summary(cls, obj, ar=None):
1969
+ """
1970
+ :param cls: Slave table
1971
+ :param obj: Master instance
1972
+ :param ar: Action request on master table
1973
+ """
1974
+ if ar is None:
1975
+ return ''
1976
+ sar = cls.request(parent=ar, master_instance=obj,
1977
+ is_on_main_actor=False)
1978
+ return cls.get_table_summary(sar)
1979
+
1980
+ @classmethod
1981
+ def get_table_summary(cls, ar):
1952
1982
  """
1953
1983
  Return the HTML `<div>` to be displayed by
1954
1984
  :class:`lino.core.elems.TableSummaryPanel`.
1955
1985
  It basically just calls :meth:`table_as_summary`.
1956
1986
 
1957
1987
  """
1958
- if ar is None:
1959
- return ''
1960
- sar = cls.request(parent=ar, master_instance=obj, is_on_main_actor=False)
1961
- p = cls.table_as_summary(sar)
1988
+ p = cls.table_as_summary(ar)
1962
1989
  # assert_safe(p) # temporary 20240506
1963
1990
  # print("20240712", p)
1964
1991
  # return format_html(DIVTPL, p)
@@ -1981,9 +2008,9 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1981
2008
  """
1982
2009
  p = qs2summary(
1983
2010
  ar,
1984
- ar.data_iterator,
2011
+ ar.sliced_data_iterator,
1985
2012
  separator=cls.summary_sep,
1986
- max_items=cls.preview_limit,
2013
+ max_items=ar.limit or cls.preview_limit,
1987
2014
  wraptpl=None,
1988
2015
  )
1989
2016
  # assert isinstance(p, str)
@@ -2101,7 +2128,8 @@ def resolve_action(spec, action=None):
2101
2128
  a = spec.get_action_by_name(action)
2102
2129
  # ~ print 20121210, a
2103
2130
  if a is None:
2104
- raise Exception("{} has no action named '{}'".format(spec, action))
2131
+ raise Exception(
2132
+ "{} has no action named '{}'".format(spec, action))
2105
2133
  else:
2106
2134
  a = spec.default_action
2107
2135
  # assert a is not None
@@ -2109,4 +2137,5 @@ def resolve_action(spec, action=None):
2109
2137
  raise Exception("%r default_action is None?!" % spec)
2110
2138
  return a
2111
2139
 
2112
- raise Exception("Action spec %r returned invalid object %r" % (givenspec, spec))
2140
+ raise Exception("Action spec %r returned invalid object %r" %
2141
+ (givenspec, spec))
lino/core/choicelists.py CHANGED
@@ -36,9 +36,10 @@ VALUE_FIELD.attname = "value"
36
36
 
37
37
  # @deconstructible
38
38
  class Choice(fields.TableRow):
39
- """A constant value whose unicode representation depends on the
40
- current language at runtime. Every item of a :class:`ChoiceList`
41
- must be an instance of :class:`Choice` or a subclass thereof.
39
+ """
40
+ A constant value whose string representation depends on the current language
41
+ at runtime. Every item of a :class:`ChoiceList` must be an instance of
42
+ :class:`Choice` or a subclass thereof.
42
43
 
43
44
  .. attribute:: choicelist
44
45
 
@@ -478,11 +479,11 @@ class ChoiceList(with_metaclass(ChoiceListMeta, tables.AbstractTable)):
478
479
 
479
480
  old2new = {}
480
481
  """
481
- A dict which maps old values to their new values.
482
+ A dict that maps old values to their new values.
482
483
 
483
- This dict is consulted when an unknown value is read from database
484
- (e.g. during a migration). If if contains a replacement for the
485
- old value, Lino will return the choice with the new value.
484
+ This is consulted when an unknown value is read from database (e.g. during a
485
+ migration). If it contains a replacement for the old value, Lino will
486
+ return the choice with the new value.
486
487
  """
487
488
 
488
489
  @classmethod
@@ -1050,7 +1051,8 @@ class MultiChoiceListField(ChoiceListField):
1050
1051
  return []
1051
1052
  if isinstance(value, list):
1052
1053
  return value
1053
- value = [self.choicelist.to_python(v) for v in value.split(self.delimiter_char)]
1054
+ value = [self.choicelist.to_python(
1055
+ v) for v in value.split(self.delimiter_char)]
1054
1056
  return value
1055
1057
 
1056
1058
  def get_prep_value(self, value):
lino/core/dbtables.py CHANGED
@@ -266,7 +266,7 @@ class Table(AbstractTable):
266
266
  yield self.detail_action.request(user=user)
267
267
 
268
268
  # @classmethod
269
- # def elem_filename_root(cls,elem):
269
+ # def elem_get_printable_target_stem(cls,elem):
270
270
  # return elem._meta.app_label + '.' + elem.__class__.__name__ + '-' + str(elem.pk)
271
271
  @classmethod
272
272
  def get_detail_sets(self):