lino 25.6.0__py3-none-any.whl → 25.7.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 (90) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +21 -0
  3. lino/core/actions.py +59 -25
  4. lino/core/actors.py +38 -16
  5. lino/core/boundaction.py +16 -0
  6. lino/core/choicelists.py +7 -7
  7. lino/core/constants.py +3 -0
  8. lino/core/dashboard.py +1 -0
  9. lino/core/dbtables.py +1 -1
  10. lino/core/elems.py +38 -13
  11. lino/core/fields.py +20 -11
  12. lino/core/kernel.py +8 -0
  13. lino/core/layouts.py +6 -2
  14. lino/core/menus.py +3 -6
  15. lino/core/model.py +5 -4
  16. lino/core/renderer.py +14 -5
  17. lino/core/requests.py +8 -7
  18. lino/core/signals.py +1 -0
  19. lino/core/site.py +48 -28
  20. lino/core/store.py +4 -2
  21. lino/core/tables.py +23 -10
  22. lino/core/utils.py +4 -1
  23. lino/core/workflows.py +2 -1
  24. lino/help_texts.py +1 -2
  25. lino/management/commands/prep.py +2 -2
  26. lino/management/commands/show.py +8 -10
  27. lino/mixins/__init__.py +14 -13
  28. lino/mixins/periods.py +2 -0
  29. lino/mixins/sequenced.py +1 -1
  30. lino/modlib/about/models.py +4 -3
  31. lino/modlib/checkdata/__init__.py +42 -36
  32. lino/modlib/checkdata/choicelists.py +9 -1
  33. lino/modlib/checkdata/fixtures/checkdata.py +4 -2
  34. lino/modlib/checkdata/models.py +9 -2
  35. lino/modlib/comments/models.py +4 -3
  36. lino/modlib/extjs/ext_renderer.py +4 -4
  37. lino/modlib/extjs/views.py +8 -2
  38. lino/modlib/gfks/fields.py +1 -1
  39. lino/modlib/help/__init__.py +3 -3
  40. lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
  41. lino/modlib/help/fixtures/demo2.py +6 -1
  42. lino/modlib/help/management/commands/makehelp.py +4 -1
  43. lino/modlib/help/models.py +2 -1
  44. lino/modlib/help/utils.py +12 -6
  45. lino/modlib/linod/choicelists.py +57 -4
  46. lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
  47. lino/modlib/linod/management/commands/linod.py +0 -13
  48. lino/modlib/linod/mixins.py +8 -0
  49. lino/modlib/linod/models.py +29 -30
  50. lino/modlib/memo/__init__.py +7 -7
  51. lino/modlib/memo/management/__init__,py +0 -0
  52. lino/modlib/memo/management/commands/__init__.py +0 -0
  53. lino/modlib/memo/management/commands/removeurls.py +67 -0
  54. lino/modlib/memo/mixins.py +1 -9
  55. lino/modlib/memo/parser.py +1 -1
  56. lino/modlib/notify/config/notify/summary.eml +5 -2
  57. lino/modlib/notify/fixtures/demo2.py +5 -6
  58. lino/modlib/notify/models.py +9 -10
  59. lino/modlib/periods/__init__.py +11 -8
  60. lino/modlib/periods/choicelists.py +16 -10
  61. lino/modlib/periods/models.py +45 -45
  62. lino/modlib/summaries/fixtures/checksummaries.py +4 -2
  63. lino/modlib/system/models.py +17 -18
  64. lino/modlib/uploads/fixtures/demo.py +9 -3
  65. lino/modlib/uploads/mixins.py +5 -2
  66. lino/modlib/uploads/models.py +15 -9
  67. lino/modlib/uploads/utils.py +4 -1
  68. lino/modlib/users/__init__.py +59 -18
  69. lino/modlib/users/actions.py +24 -20
  70. lino/modlib/users/fixtures/demo_users.py +2 -35
  71. lino/modlib/users/mixins.py +3 -4
  72. lino/modlib/users/models.py +53 -13
  73. lino/modlib/users/ui.py +30 -16
  74. lino/modlib/users/utils.py +5 -6
  75. lino/projects/std/settings.py +1 -1
  76. lino/sphinxcontrib/logo/templates/footer.html +1 -0
  77. lino/utils/ajax.py +1 -1
  78. lino/utils/cycler.py +5 -0
  79. lino/utils/dbhash.py +4 -9
  80. lino/utils/dpy.py +2 -2
  81. lino/utils/format_date.py +4 -3
  82. lino/utils/html.py +13 -5
  83. lino/utils/jsgen.py +1 -1
  84. lino/utils/quantities.py +8 -0
  85. lino/utils/soup.py +75 -94
  86. {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/METADATA +1 -1
  87. {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/RECORD +90 -87
  88. {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/WHEEL +0 -0
  89. {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
  90. {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/licenses/COPYING +0 -0
lino/__init__.py CHANGED
@@ -31,7 +31,7 @@ from django import VERSION
31
31
  from django.apps import AppConfig
32
32
  from django.conf import settings
33
33
  import warnings
34
- __version__ = '25.6.0'
34
+ __version__ = '25.7.0'
35
35
 
36
36
  # import setuptools # avoid UserWarning "Distutils was imported before Setuptools"?
37
37
 
lino/api/doctest.py CHANGED
@@ -766,6 +766,27 @@ def show_display_modes():
766
766
  print(rstgen.table(headers, rows))
767
767
 
768
768
 
769
+ def show_choosers():
770
+ """
771
+ Show the availble choosers per actor.
772
+ """
773
+ headers = ["field"]
774
+ headers = ["field", "context_fields", "can_create_choice"]
775
+ rows = []
776
+ # for a in sorted(actors.actors_list, key=str):
777
+ for m in sorted(get_models(), key=full_model_name):
778
+ if (cd := getattr(m, "_choosers_dict", None)) is None:
779
+ continue
780
+ for fld in get_fields(m):
781
+ if (c := cd.get(fld.name, None)):
782
+ cf = ", ".join([cf.name for cf in c.context_fields])
783
+ rows.append([
784
+ f"{full_model_name(m)}.{fld.name}",
785
+ str(cf),
786
+ str(c.can_create_choice)])
787
+ print(rstgen.table(headers, rows))
788
+
789
+
769
790
  def checkdb(m, num):
770
791
  """
771
792
  Raise an exception if the database doesn't contain the specified number of
lino/core/actions.py CHANGED
@@ -3,6 +3,7 @@
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  # See src/core/actions.rst
5
5
 
6
+ from typing import Any
6
7
  from django.conf import settings
7
8
  from django.utils.translation import gettext_lazy as _
8
9
  from django.utils.text import format_lazy
@@ -10,6 +11,7 @@ from django.utils.encoding import force_str
10
11
  from django.utils.translation import gettext
11
12
  from lino.core import constants
12
13
  from lino.core import keyboard
14
+ from lino.core.exceptions import ChangedAPI
13
15
  from .utils import traverse_ddh_fklist
14
16
  from .utils import navinfo
15
17
  from .utils import obj2unicode
@@ -40,7 +42,7 @@ class Action(Parametrizable, Permittable):
40
42
  react_icon_name = None
41
43
  hidden_elements = frozenset()
42
44
  combo_group = None
43
- parameters = None
45
+ parameters: dict[str, Any] | None = None
44
46
 
45
47
  use_param_panel = False
46
48
  no_params_window = False
@@ -76,6 +78,8 @@ class Action(Parametrizable, Permittable):
76
78
  default_record_id = None
77
79
 
78
80
  def __init__(self, label=None, **kwargs):
81
+ # if hasattr(self, 'help_text'):
82
+ # raise ChangedAPI("Replace help_text on Action by help_text")
79
83
  if label is not None:
80
84
  self.label = label
81
85
 
@@ -130,12 +134,14 @@ class Action(Parametrizable, Permittable):
130
134
  # return LinoForm
131
135
 
132
136
  @classmethod
133
- def decorate(cls, *args, **kw):
137
+ def decorate(cls, *args, help_text=None, **kw):
134
138
 
135
139
  def decorator(fn):
136
140
  assert "required" not in kw
137
141
  # print 20140422, fn.__name__
138
142
  kw.setdefault("custom_handler", True)
143
+ if help_text is not None:
144
+ kw.update(help_text=help_text)
139
145
  a = cls(*args, **kw)
140
146
 
141
147
  def wrapped(ar):
@@ -147,6 +153,13 @@ class Action(Parametrizable, Permittable):
147
153
 
148
154
  return decorator
149
155
 
156
+ def get_help_text(self, ba):
157
+ if ba is ba.actor.default_action:
158
+ if self.default_record_id is not None:
159
+ return ba.actor.help_text or self.help_text
160
+ return self.help_text or ba.actor.help_text
161
+ return self.help_text
162
+
150
163
  def get_required_roles(self, actor):
151
164
  return actor.required_roles
152
165
 
@@ -210,11 +223,11 @@ class Action(Parametrizable, Permittable):
210
223
  else:
211
224
  return self.button_text or self.label
212
225
 
213
- def full_name(self, actor):
226
+ def full_name(self, actor=None):
214
227
  if self.action_name is None:
215
228
  raise Exception("Tried to full_name() on %r" % self)
216
229
  # ~ return repr(self)
217
- if self.parameters and not self.no_params_window:
230
+ if actor is None or (self.parameters and not self.no_params_window):
218
231
  return self.defining_actor.actor_id + "." + self.action_name
219
232
  return str(actor) + "." + self.action_name
220
233
 
@@ -256,16 +269,17 @@ class Action(Parametrizable, Permittable):
256
269
  self.defining_actor = owner
257
270
  # if self.label is None:
258
271
  # self.label = name
272
+ # if self.__class__.__name__ == "CreateExamByCourse":
273
+ # print(f"20250608 {self} attach_to_actor({owner})")
274
+ fields.setup_params_choosers(self)
259
275
  if self.action_name is not None:
260
276
  return True
261
277
  # if name == self.action_name:
262
278
  # return True
263
279
  # raise Exception(
264
- # "tried to attach named action %s.%s as %s" %
265
- # (actor, self.action_name, name))
280
+ # f"Can't attach named action {self.action_name} "
281
+ # f"as {name} to {owner}")
266
282
  self.action_name = name
267
- fields.setup_params_choosers(self)
268
- # fields.setup_params_choosers(self.__class__)
269
283
  return True
270
284
 
271
285
  def get_action_permission(self, ar, obj, state):
@@ -349,6 +363,10 @@ class ShowDetail(Action):
349
363
  self.owner = dl
350
364
  super().__init__(label, **kwargs)
351
365
 
366
+ # def get_help_text(self, ba):
367
+ # return _("Open a detail window on records of {}.").format(
368
+ # ba.actor.app_label)
369
+
352
370
  def attach_to_actor(self, actor, name):
353
371
  self.help_text = _(
354
372
  "Open a detail window on records of " + actor.app_label + "."
@@ -365,6 +383,11 @@ class ShowDetail(Action):
365
383
  # return actor.extra_layouts[0]
366
384
  return actor.detail_layout
367
385
 
386
+ # def get_help_text(self, ba):
387
+ # if self.default_record_id is not None:
388
+ # return ba.actor.help_text
389
+ # return super().get_help_text(ba)
390
+
368
391
  def get_window_size(self, actor):
369
392
  wl = self.get_window_layout(actor)
370
393
  if wl is not None:
@@ -432,12 +455,15 @@ class ShowInsert(TableAction):
432
455
  select_rows = False
433
456
  http_method = "POST"
434
457
 
435
- def attach_to_actor(self, owner, name):
436
- if owner.model is not None:
437
- self.help_text = format_lazy(
438
- _("Insert a new {}."), owner.model._meta.verbose_name
439
- )
440
- return super().attach_to_actor(owner, name)
458
+ def get_help_text(self, ba):
459
+ return format_lazy(
460
+ _("Insert a new {}."), ba.actor.model._meta.verbose_name)
461
+ # def attach_to_actor(self, owner, name):
462
+ # if owner.model is not None:
463
+ # self.help_text = format_lazy(
464
+ # _("Insert a new {}."), owner.model._meta.verbose_name
465
+ # )
466
+ # return super().attach_to_actor(owner, name)
441
467
 
442
468
  def get_action_title(self, ar):
443
469
  # return _("Insert into %s") % force_str(ar.get_title())
@@ -446,7 +472,7 @@ class ShowInsert(TableAction):
446
472
  return format_lazy(_("Insert a new {}"), ar.actor.model._meta.verbose_name)
447
473
 
448
474
  def get_window_layout(self, actor):
449
- return actor.insert_layout or actor.detail_layout
475
+ return self.params_layout or actor.insert_layout or actor.detail_layout
450
476
 
451
477
  def get_window_size(self, actor):
452
478
  wl = self.get_window_layout(actor)
@@ -708,13 +734,13 @@ class ShowSlaveTable(Action):
708
734
  "button_text",
709
735
  ) # 'help_text',
710
736
  show_in_toolbar = True
711
- _help_text = None
737
+ _defined_help_text = None
712
738
 
713
739
  def __init__(self, slave_table, help_text=None, **kw):
714
740
  self.slave_table = slave_table
715
741
  self.explicit_attribs = set(kw.keys())
716
742
  if help_text is not None:
717
- self._help_text = help_text
743
+ self._defined_help_text = help_text
718
744
  super().__init__(**kw)
719
745
 
720
746
  # Removed 20250521 because I don't see why it is needed
@@ -726,24 +752,30 @@ class ShowSlaveTable(Action):
726
752
  if isinstance(self.slave_table, str):
727
753
  T = settings.SITE.models.resolve(self.slave_table)
728
754
  if T is None:
729
- msg = "Invalid action {} on actor {!r}: " "no table named {}".format(
755
+ msg = "Invalid action {} on actor {!r}: no table named {}".format(
730
756
  name, actor, self.slave_table
731
757
  )
732
758
  raise Exception(msg)
733
759
  self.slave_table = T
760
+
734
761
  for k in self.TABLE2ACTION_ATTRS:
735
762
  if k not in self.explicit_attribs:
736
763
  attr = getattr(self.slave_table, k, None)
737
764
  setattr(self, k, attr)
765
+ # if self.help_text is None:
766
+ # self.help_text = self.slave_table.help_text
738
767
  return super().attach_to_actor(actor, name)
739
768
 
740
- @property
741
- def help_text(self):
742
- return self._help_text or self.slave_table.help_text
769
+ def get_help_text(self, ba):
770
+ return self._defined_help_text or self.slave_table.help_text
743
771
 
744
- @help_text.setter
745
- def help_text(self, help_text):
746
- self._help_text = help_text
772
+ # @property
773
+ # def help_text(self):
774
+ # return self._defined_help_text or self.slave_table.help_text
775
+
776
+ # @help_text.setter
777
+ # def help_text(self, help_text):
778
+ # self._help_text = help_text
747
779
 
748
780
  def run_from_ui(self, ar, **kw):
749
781
  obj = ar.selected_rows[0]
@@ -792,7 +824,7 @@ class WrappedAction(Action):
792
824
  # print(self.bound_action, actor.required_roles | self.bound_action.required)
793
825
  return actor.required_roles | self.bound_action.required
794
826
 
795
- @classmethod
827
+ @ classmethod
796
828
  def get_actor_label(self):
797
829
  return self.get_label() or self.bound_action.label
798
830
 
@@ -930,6 +962,8 @@ class DeleteSelected(MultipleRowAction):
930
962
 
931
963
  # Some actions are described by a single action instance used by most actors:
932
964
 
965
+ SHOW_INSERT = ShowInsert()
966
+ SHOW_TABLE = ShowTable()
933
967
  SUBMIT_DETAIL = SubmitDetail()
934
968
  DELETE_ACTION = DeleteSelected()
935
969
  UPDATE_ACTION = SaveGridCell()
lino/core/actors.py CHANGED
@@ -590,6 +590,7 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
590
590
  # obj = cls.cast_master_instance(obj)
591
591
  # return obj
592
592
  except Exception as e:
593
+ # logger.error(e)
593
594
  # raise Exception("20240804 {}\n{}\n{}".format(e, model, ar))
594
595
  return MissingRow("{} (pk={})".format(e, pk))
595
596
 
@@ -1099,9 +1100,9 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1099
1100
  # if cls.detail_action and not cls.hide_top_toolbar:
1100
1101
  # if cls.insert_layout and not cls.hide_top_toolbar:
1101
1102
  # NB polls.AnswerRemarksByAnswer has hide_top_toolbar but we need its insert_action.
1102
- if cls.insert_layout:
1103
+ if (ia := cls.get_insert_action()) is not None:
1103
1104
  cls.insert_action = cls._bind_action(
1104
- "insert_action", cls.get_insert_action(), True
1105
+ "insert_action", ia, True
1105
1106
  )
1106
1107
  if cls.allow_delete:
1107
1108
  cls.delete_action = cls._bind_action(
@@ -1219,11 +1220,15 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1219
1220
  # if str(cls) == "system.SiteConfigs": # and a.__class__.__name__ == "ShowDetail":
1220
1221
  # print("20210106 ignore {} {} because {} exists".format(k, a.__class__, old))
1221
1222
  if override:
1223
+ # if name == "update_guests":
1224
+ # print(f"20250622 override {old} on {cls}")
1222
1225
  cls._actions_list.remove(old)
1223
1226
  else:
1224
1227
  return old
1225
1228
 
1226
1229
  ba = BoundAction(cls, a)
1230
+ # if name == "update_guests":
1231
+ # print(f"20250622 create {hash(ba)} on {cls}")
1227
1232
  # try:
1228
1233
  # ba = BoundAction(cls, a)
1229
1234
  # except Exception as e:
@@ -1244,7 +1249,9 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1244
1249
  def get_insert_action(cls):
1245
1250
  # create a new instance for each actor because attach_to_actor will
1246
1251
  # modify the help_text
1247
- return actions.ShowInsert()
1252
+ if cls.insert_layout:
1253
+ # return actions.ShowInsert() # help_text gest modified.
1254
+ return actions.SHOW_INSERT
1248
1255
 
1249
1256
  @classmethod
1250
1257
  def get_label(self):
@@ -1756,14 +1763,14 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1756
1763
  @classmethod
1757
1764
  def get_data_elem(self, name):
1758
1765
  """Find data element in this actor by name."""
1766
+ # Note that there are models with fields named 'master', 'app_label',
1767
+ # 'model' (i.e. a name that is also used as attribute of an actor.
1768
+
1759
1769
  c = self._constants.get(name, None)
1760
1770
  if c is not None:
1761
1771
  return c
1762
1772
  # ~ return self.virtual_fields.get(name,None)
1763
1773
 
1764
- # Note that there are models with fields named 'master', 'app_label',
1765
- # 'model' (i.e. a name that is also used as attribute of an actor.
1766
-
1767
1774
  for cls in getmro(self):
1768
1775
  if hasattr(cls, "virtual_fields"):
1769
1776
  vf = cls.virtual_fields.get(name, None)
@@ -1771,6 +1778,15 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1771
1778
  # ~ logger.info("20120202 Actor.get_data_elem found vf %r",vf)
1772
1779
  return vf
1773
1780
 
1781
+ # Replacing above code block with the code below is theoretically the
1782
+ # same but in reality causes #5739 (Oops, get_atomizer(...) returned
1783
+ # None) to reappear in projects/noi1r/tests/test_notify.py:
1784
+
1785
+ # vf = self.virtual_fields.get(name, None)
1786
+ # if vf is not None:
1787
+ # # ~ logger.info("20120202 Actor.get_data_elem found vf %r",vf)
1788
+ # return vf
1789
+
1774
1790
  if self.model is not None:
1775
1791
  de = self.model.get_data_elem(name)
1776
1792
  if de is not None:
@@ -1964,11 +1980,16 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1964
1980
  html_text += grp.after_row(obj)
1965
1981
  html_text += grp.stop()
1966
1982
 
1967
- if cls.editable and cls.insert_action is not None:
1968
- ir = cls.insert_action.request_from(sar)
1969
- if ir.get_permission():
1970
- # html_text = mark_safe(tostring(ir.ar2button()) + html_text)
1971
- html_text = tostring(ir.ar2button()) + html_text
1983
+ # 20250713
1984
+ if len(toolbar := sar.plain_toolbar_buttons()):
1985
+ p = mark_safe(btn_sep.join([tostring(b) for b in toolbar]))
1986
+ html_text = p + html_text
1987
+
1988
+ # if cls.editable and cls.insert_action is not None:
1989
+ # ir = cls.insert_action.request_from(sar)
1990
+ # if ir.get_permission():
1991
+ # # html_text = mark_safe(tostring(ir.ar2button()) + html_text)
1992
+ # html_text = tostring(ir.ar2button()) + html_text
1972
1993
 
1973
1994
  # assert_safe(html_text) # temporary 20240506
1974
1995
  return format_html(DIVTPL, html_text)
@@ -1977,14 +1998,15 @@ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
1977
1998
  def get_table_as_tiles(cls, obj, ar):
1978
1999
  sar = cls.create_request(parent=ar, master_instance=obj,
1979
2000
  is_on_main_actor=False)
1980
- html = SAFE_EMPTY
2001
+ tiles = SAFE_EMPTY
2002
+ prev = None
1981
2003
  for i, obj in enumerate(sar.data_iterator):
1982
2004
  if i == cls.preview_limit:
1983
2005
  break
1984
- s = obj.as_tile(sar)
1985
- # assert_safe(s) # temporary 20240506
1986
- html += format_html(constants.TILE_TEMPLATE, chunk=s)
1987
- return html
2006
+ tiles += obj.as_tile(sar, prev)
2007
+ prev = obj
2008
+ # return format_html(constants.TILES_CONTAINER_TEMPLATE, tiles=tiles)
2009
+ return mark_safe(tiles)
1988
2010
 
1989
2011
  @classmethod
1990
2012
  def get_table_story(cls, obj, ar):
lino/core/boundaction.py CHANGED
@@ -26,6 +26,8 @@ class BoundAction(object):
26
26
  each subclass "inherits" its actions.
27
27
 
28
28
  """
29
+ help_text = None # install_help_text() tests if hasattr(fld, "help_text")
30
+ # _started = False
29
31
 
30
32
  def __init__(self, actor, action):
31
33
  # the following test would require us to import Action, which
@@ -82,6 +84,17 @@ class BoundAction(object):
82
84
  # ~ logger.info("20130424 _allow is %s",self._allow)
83
85
  # ~ actor.actions.define(a.action_name,ba)
84
86
 
87
+ # def __setattr__(self, name, value):
88
+ # if name == "help_text" and self.action.action_name == 'update_guests':
89
+ # old = getattr(self, name, None)
90
+ # if value is None and old is not None:
91
+ # raise Exception(f"20250622 set to None {hash(self)} {name} was {old}")
92
+ # super().__setattr__(name, value)
93
+
94
+ # @property
95
+ # def help_text(self):
96
+ # return self.action.get_help_text(self)
97
+
85
98
  def get_window_layout(self):
86
99
  return self.action.get_window_layout(self.actor)
87
100
 
@@ -96,6 +109,9 @@ class BoundAction(object):
96
109
  def get_window_size(self):
97
110
  return self.action.get_window_size(self.actor)
98
111
 
112
+ def get_help_text(self):
113
+ return self.help_text
114
+
99
115
  def full_name(self):
100
116
  return self.action.full_name(self.actor)
101
117
 
lino/core/choicelists.py CHANGED
@@ -464,7 +464,7 @@ class ChoiceList(tables.AbstractTable, metaclass=ChoiceListMeta):
464
464
 
465
465
  @classmethod
466
466
  def get_default_action(cls):
467
- return actions.ShowTable()
467
+ return actions.SHOW_TABLE
468
468
 
469
469
  hidden_columns = frozenset(["workflow_buttons"])
470
470
 
@@ -720,7 +720,7 @@ class ChoiceList(tables.AbstractTable, metaclass=ChoiceListMeta):
720
720
  # ~ We must make it dynamic since e.g. UserTypes can change after
721
721
  # ~ the fields have been created.
722
722
 
723
- # ~ https://docs.djangoproject.com/en/5.0/ref/models/fields/
723
+ # ~ https://docs.djangoproject.com/en/5.2/ref/models/fields/
724
724
  # ~ note that choices can be any iterable object -- not necessarily
725
725
  # ~ a list or tuple. This lets you construct choices dynamically.
726
726
  # ~ But if you find yourself hacking choices to be dynamic, you're
@@ -930,7 +930,7 @@ class ChoiceListField(models.CharField):
930
930
  def deconstruct(self):
931
931
  """
932
932
  Needed for Django 1.7+, see
933
- https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/#custom-field-deconstruct-method
933
+ https://docs.djangoproject.com/en/5.2/howto/custom-model-fields/#custom-field-deconstruct-method
934
934
  """
935
935
 
936
936
  name, path, args, kwargs = super().deconstruct()
@@ -958,7 +958,7 @@ class ChoiceListField(models.CharField):
958
958
 
959
959
  def to_python(self, value):
960
960
  """See Django's docs about `to_python()
961
- <https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.Field.to_python>`__.
961
+ <https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.Field.to_python>`__.
962
962
 
963
963
  """
964
964
  # ~ if self.attname == 'query_register':
@@ -985,11 +985,11 @@ class ChoiceListField(models.CharField):
985
985
  def get_prep_value(self, value):
986
986
  """
987
987
  Excerpt from `Django docs
988
- <https://docs.djangoproject.com/en/5.0/howto/custom-model-fields/#converting-python-objects-to-query-values>`__:
988
+ <https://docs.djangoproject.com/en/5.2/howto/custom-model-fields/#converting-python-objects-to-query-values>`__:
989
989
  "If you override `to_python()
990
- <https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.Field.to_python>`__
990
+ <https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.Field.to_python>`__
991
991
  you also have to override `get_prep_value()
992
- <https://docs.djangoproject.com/en/5.0/ref/models/fields/#django.db.models.Field.get_prep_value>`__
992
+ <https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.Field.get_prep_value>`__
993
993
  to convert Python objects back to query values."
994
994
  """
995
995
  # ~ if self.attname == 'query_register':
lino/core/constants.py CHANGED
@@ -224,3 +224,6 @@ TILE_TEMPLATE = """
224
224
  {chunk}
225
225
  </div>
226
226
  """
227
+
228
+ # TILES_CONTAINER_TEMPLATE = '<div style="display:flex;"{tiles}</div>'
229
+ # TILES_CONTAINER_TEMPLATE = '{tiles}'
lino/core/dashboard.py CHANGED
@@ -66,6 +66,7 @@ class DashboardItem(Permittable):
66
66
  yield mark_safe('<div class="dashboard-item">')
67
67
  if self.header_level is not None:
68
68
  buttons = sar.plain_toolbar_buttons()
69
+ # 20250713 Maybe add the ⏏ button already in plain_toolbar_buttons()
69
70
  buttons.append(sar.open_in_own_window_button())
70
71
  elems = []
71
72
  for b in buttons:
lino/core/dbtables.py CHANGED
@@ -54,7 +54,7 @@ def base_attrs(cl):
54
54
  def add_gridfilters(qs, gridfilters):
55
55
  """Converts a `filter` request in the format used by
56
56
  :extux:`Ext.ux.grid.GridFilters` into a `Django field lookup
57
- <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#field-lookups>`_
57
+ <https://docs.djangoproject.com/en/5.2/ref/models/querysets/#field-lookups>`_
58
58
  on a :class:`django.db.models.query.QuerySet`.
59
59
 
60
60
  :param qs: the queryset to be modified.
lino/core/elems.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2009-2024 Rumma & Ko Ltd
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """Defines "layout elements" (widgets).
5
5
 
@@ -29,12 +29,12 @@ from django.db.models.fields.related import ManyToManyRel, ManyToOneRel
29
29
  from django.db.models.fields import NOT_PROVIDED
30
30
 
31
31
  from lino import logger
32
-
33
32
  from lino.core import layouts
34
33
  from lino.core import fields
35
34
  from lino.core import constants
36
35
  from lino.core import choicelists
37
36
  from lino.core import actions
37
+ from lino.core.utils import resolve_model
38
38
  from lino.core.gfks import GenericRelation, GenericRel
39
39
  from lino.core.permissions import Permittable
40
40
  from lino.modlib.bootstrap3.views import table2html
@@ -78,6 +78,11 @@ FULLHEIGHT = "-10"
78
78
 
79
79
  DEFAULT_PADDING = 2
80
80
 
81
+ # See discussion in #6167 (Should we cache delayed values?). We tried this and
82
+ # it worked as such, but it caused --or revealed-- some strange test failures
83
+
84
+ DELAYED_HTML = False
85
+
81
86
 
82
87
  def form_field_name(f):
83
88
  if isinstance(f, models.ForeignKey) or (isinstance(f, models.Field) and f.choices):
@@ -1083,6 +1088,17 @@ class ForeignKeyElement(ComplexRemoteComboFieldElement):
1083
1088
 
1084
1089
  def get_field_options(self, **kw):
1085
1090
  kw = super().get_field_options(**kw)
1091
+ if not self.field.remote_field:
1092
+ raise Exception("20171210 %r" % self.field.__class__)
1093
+
1094
+ if isinstance(self.field.remote_field.model, str):
1095
+ # fld = self.field.remote_field
1096
+ # print(f"20250607 {self.field} {fld.model}")
1097
+ # fld.model = resolve_model(fld.model)
1098
+ raise Exception(
1099
+ "20130827 %s.remote_field.model is %r"
1100
+ % (self.field, self.field.remote_field.model)
1101
+ )
1086
1102
  actor = self.field.remote_field.model.get_default_table()
1087
1103
  if actor is None:
1088
1104
  return kw
@@ -1105,13 +1121,6 @@ class ForeignKeyElement(ComplexRemoteComboFieldElement):
1105
1121
  options.update(allowCreate=False)
1106
1122
  return options
1107
1123
 
1108
- if not self.field.remote_field:
1109
- raise Exception("20171210 %r" % self.field.__class__)
1110
- if isinstance(self.field.remote_field.model, str):
1111
- raise Exception(
1112
- "20130827 %s.remote_field.model is %r"
1113
- % (self.field, self.field.remote_field.model)
1114
- )
1115
1124
  pw = self.field.remote_field.model.preferred_foreignkey_width
1116
1125
  if pw is not None:
1117
1126
  kw.setdefault("preferred_width", pw)
@@ -1862,6 +1871,11 @@ class TilesElement(LightWeightContainer):
1862
1871
  super().__init__(lh, slave, name, slave.get_table_as_tiles, **kw)
1863
1872
 
1864
1873
 
1874
+ class HtmlElement(LightWeightContainer):
1875
+ def __init__(self, lh, slave, name, **kw):
1876
+ super().__init__(lh, slave, name, slave.slave_as_html, **kw)
1877
+
1878
+
1865
1879
  class ManyRelatedObjectElement(HtmlBoxElement):
1866
1880
  def __init__(self, lh, relobj, **kw):
1867
1881
  name = relobj.field.remote_field.related_name
@@ -2458,8 +2472,12 @@ class SlaveContainer(GridElement):
2458
2472
  slaves["summary"] = get_summary_element(
2459
2473
  layout_handle, rpt, name, **kw)
2460
2474
  if constants.DISPLAY_MODE_HTML in rpt.extra_display_modes:
2461
- slaves["html"] = get_htmlbox_element(
2462
- layout_handle, rpt, name, **kw)
2475
+ if DELAYED_HTML:
2476
+ slaves["html"] = get_html_element(
2477
+ layout_handle, rpt, name, **kw)
2478
+ else:
2479
+ slaves["html"] = get_htmlbox_element(
2480
+ layout_handle, rpt, name, **kw)
2463
2481
  if slaves:
2464
2482
  self.slaves = slaves
2465
2483
  super().__init__(layout_handle, name, rpt, *columns, **kw)
@@ -2744,6 +2762,10 @@ def get_list_element(lh, de, name, **kw):
2744
2762
  return get_display_element(lh, de, name, ListElement, **kw)
2745
2763
 
2746
2764
 
2765
+ def get_html_element(lh, de, name, **kw):
2766
+ return get_display_element(lh, de, name, HtmlElement, **kw)
2767
+
2768
+
2747
2769
  def get_htmlbox_element(lh, de, name, **kw):
2748
2770
  field = fields.HtmlBox(verbose_name=de.get_label())
2749
2771
  field.name = name # de.actor_id # de.__name__
@@ -2870,7 +2892,7 @@ def create_layout_element(lh, name, **kw):
2870
2892
  if lh.ui.renderer.extjs_version is not None:
2871
2893
  kw.update(master_panel=js_code("this"))
2872
2894
 
2873
- # print("20240317", de, lh)
2895
+ # print("20240317 before FormLayout test", de, lh, lh.layout)
2874
2896
  if isinstance(lh.layout, FormLayout):
2875
2897
  # When a table is specified in the layout of a detail window, then it
2876
2898
  # is rendered as a :term:`slave panel`. The panel will have a
@@ -2915,7 +2937,10 @@ def create_layout_element(lh, name, **kw):
2915
2937
  return GridElement(lh, name, de, **kw)
2916
2938
 
2917
2939
  elif dm == constants.DISPLAY_MODE_HTML:
2918
- return get_htmlbox_element(lh, de, name, **kw)
2940
+ if DELAYED_HTML:
2941
+ return get_html_element(lh, de, name, **kw)
2942
+ else:
2943
+ return get_htmlbox_element(lh, de, name, **kw)
2919
2944
 
2920
2945
  elif dm == constants.DISPLAY_MODE_SUMMARY:
2921
2946
  return get_summary_element(lh, de, name, **kw)