lino 25.5.2__py3-none-any.whl → 25.6.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 (53) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +5 -3
  3. lino/api/doctest.py +2 -2
  4. lino/core/__init__.py +3 -3
  5. lino/core/actions.py +62 -582
  6. lino/core/actors.py +66 -32
  7. lino/core/atomizer.py +355 -0
  8. lino/core/boundaction.py +8 -4
  9. lino/core/constants.py +23 -1
  10. lino/core/dbtables.py +4 -3
  11. lino/core/elems.py +33 -21
  12. lino/core/fields.py +45 -210
  13. lino/core/kernel.py +18 -13
  14. lino/core/layouts.py +30 -57
  15. lino/core/model.py +6 -4
  16. lino/core/permissions.py +18 -0
  17. lino/core/renderer.py +15 -1
  18. lino/core/requests.py +19 -8
  19. lino/core/signals.py +1 -1
  20. lino/core/site.py +1 -1
  21. lino/core/store.py +13 -156
  22. lino/core/tables.py +5 -2
  23. lino/core/utils.py +124 -1
  24. lino/locale/bn/LC_MESSAGES/django.po +1034 -868
  25. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  26. lino/locale/de/LC_MESSAGES/django.po +996 -892
  27. lino/locale/django.pot +968 -869
  28. lino/locale/es/LC_MESSAGES/django.po +1032 -869
  29. lino/locale/et/LC_MESSAGES/django.po +1032 -866
  30. lino/locale/fr/LC_MESSAGES/django.po +1034 -866
  31. lino/locale/nl/LC_MESSAGES/django.po +1040 -868
  32. lino/locale/pt_BR/LC_MESSAGES/django.po +1029 -868
  33. lino/locale/zh_Hant/LC_MESSAGES/django.po +1029 -868
  34. lino/mixins/duplicable.py +8 -2
  35. lino/mixins/registrable.py +1 -1
  36. lino/modlib/changes/utils.py +4 -3
  37. lino/modlib/extjs/ext_renderer.py +1 -1
  38. lino/modlib/extjs/views.py +6 -1
  39. lino/modlib/help/config/makehelp/plugin.tpl.rst +3 -1
  40. lino/modlib/help/management/commands/makehelp.py +1 -0
  41. lino/modlib/memo/mixins.py +1 -3
  42. lino/modlib/uploads/ui.py +6 -8
  43. lino/modlib/users/fixtures/demo_users.py +16 -13
  44. lino/utils/choosers.py +11 -1
  45. lino/utils/diag.py +17 -8
  46. lino/utils/fieldutils.py +14 -11
  47. lino/utils/instantiator.py +4 -2
  48. lino/utils/report.py +5 -3
  49. {lino-25.5.2.dist-info → lino-25.6.0.dist-info}/METADATA +1 -1
  50. {lino-25.5.2.dist-info → lino-25.6.0.dist-info}/RECORD +53 -52
  51. {lino-25.5.2.dist-info → lino-25.6.0.dist-info}/WHEEL +0 -0
  52. {lino-25.5.2.dist-info → lino-25.6.0.dist-info}/licenses/AUTHORS.rst +0 -0
  53. {lino-25.5.2.dist-info → lino-25.6.0.dist-info}/licenses/COPYING +0 -0
lino/core/actors.py CHANGED
@@ -16,7 +16,7 @@ from inspect import getmro
16
16
  from django.db import models
17
17
  from django.conf import settings
18
18
  from django.utils.translation import gettext_lazy as _
19
- from django.utils.html import format_html, mark_safe, SafeString
19
+ from django.utils.html import format_html, mark_safe
20
20
 
21
21
  from lino import logger
22
22
  from lino.utils import MissingRow
@@ -27,6 +27,9 @@ from lino.core import constants
27
27
  from lino.core.boundaction import BoundAction
28
28
  from lino.core.exceptions import ChangedAPI
29
29
  from lino.core.constants import _handle_attr_name
30
+ from lino.core.utils import Parametrizable, install_layout, resolve_layout
31
+ # from lino.core.fields import setup_params_choosers
32
+ from lino.core.utils import make_params_layout_handle, register_params
30
33
  from lino.core.permissions import add_requirements, Permittable
31
34
  from lino.core.utils import resolve_model
32
35
  from lino.core.utils import error2str
@@ -43,14 +46,6 @@ ACTOR_SEP = "."
43
46
  DIVTPL = '<div class="htmlText">{}</div>'
44
47
  btn_sep = mark_safe(" ")
45
48
 
46
- # The well-known standard actions are described by (usually) always a same
47
- # action instance.
48
-
49
- SUBMIT_DETAIL = actions.SubmitDetail()
50
- DELETE_ACTION = actions.DeleteSelected()
51
- # INSERT_ACTION = actions.ShowInsert()
52
- UPDATE_ACTION = actions.SaveGridCell()
53
- VALIDATE_FORM = actions.ValidateForm()
54
49
 
55
50
  # actors are automatically discovered at startup.
56
51
 
@@ -102,6 +97,14 @@ def register_actor(a):
102
97
  return
103
98
  old = actors_dict.define(a.app_label, a.__name__, a)
104
99
  if old is not None:
100
+ # For example lino_voga.lib.courses.EnrolmentsByCourse replaces class of
101
+ # same name from lino_xl
102
+ # print(f"20250523 {repr(a)} replaces {repr(old)} ({old.abstract} {a.abstract})")
103
+ # if not issubclass(a, old):
104
+ # raise Exception(f"20250523 {a} is not subclass of {old}")
105
+ # if not a.abstract:
106
+ # old.abstract = True
107
+ # a.abstract = False
105
108
  i = actors_list.index(old)
106
109
  actors_list[i] = a
107
110
  # actors_list.remove(old)
@@ -150,7 +153,7 @@ class ActorMetaClass(type):
150
153
  if declared_editable is not None:
151
154
  classDict.update(_editable=declared_editable)
152
155
 
153
- if (pk := classDict.get("default_record_id", None)) is not None:
156
+ if classDict.get("default_record_id", None) is not None:
154
157
  classDict.update(hide_navigator=True)
155
158
  classDict.update(allow_create=False)
156
159
  classDict.update(allow_delete=False)
@@ -184,6 +187,8 @@ class ActorMetaClass(type):
184
187
 
185
188
  if actor_classes is not None:
186
189
  actor_classes.append(cls)
190
+ # else:
191
+ # print(f"20250523 found {cls} but actor_classes is None")
187
192
  return cls
188
193
 
189
194
  def __str__(cls):
@@ -208,7 +213,7 @@ class ActorMetaClass(type):
208
213
 
209
214
 
210
215
  # class Actor(metaclass=ActorMetaClass, type('NewBase', (actions.Parametrizable, Permittable), {}))):
211
- class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
216
+ class Actor(Parametrizable, Permittable, metaclass=ActorMetaClass):
212
217
  """
213
218
  The base class for all actors (:term:`data table`). Subclassed by
214
219
  :class:`AbstractTable <lino.core.tables.AbstractTable>`, :class:`Table
@@ -255,7 +260,7 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
255
260
  the user has permission to view the actor or not.
256
261
  """
257
262
 
258
- _detail_action_class = actions.ShowDetail
263
+ _detail_action_class = None
259
264
 
260
265
  obvious_fields = set()
261
266
  """A set of names of fields that are considered :term:`obvious field`. """
@@ -337,8 +342,7 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
337
342
  Special cases: :class:`lino_xl.lib.cal.EntriesByGuest` shows the entries
338
343
  having a presence pointing to this guest.
339
344
 
340
- Note that the :attr:`master_key` is automatically added to
341
- :attr:`hidden_columns`.
345
+ The :attr:`master_key` is automatically added to :attr:`hidden_columns`.
342
346
 
343
347
 
344
348
  """
@@ -348,7 +352,7 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
348
352
  parameters = None
349
353
  # See :attr:`lino.core.utils.Parametrizable.parameters`.
350
354
 
351
- _params_layout_class = layouts.ParamsLayout
355
+ _params_layout_class = None
352
356
  _state_to_disabled_actions = None
353
357
 
354
358
  ignore_required_states = False
@@ -712,8 +716,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
712
716
  @classmethod
713
717
  def make_params_layout_handle(cls):
714
718
  if cls.is_abstract():
715
- raise Exception("{} is abstract".format(cls))
716
- return actions.make_params_layout_handle(cls)
719
+ raise Exception(f"{repr(cls)} is abstract")
720
+ return make_params_layout_handle(cls)
717
721
 
718
722
  @classmethod
719
723
  def is_abstract(cls):
@@ -815,6 +819,8 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
815
819
  edm.add(constants.DISPLAY_MODE_LIST)
816
820
  if 'as_page' in model.__dict__:
817
821
  edm.add(constants.DISPLAY_MODE_STORY)
822
+ if 'as_tile' in model.__dict__:
823
+ edm.add(constants.DISPLAY_MODE_TILES)
818
824
  # no need to automatically add summary because it's in default_display_modes of every table
819
825
  # if 'as_summary_item' in model.__dict__:
820
826
  # edm.add(constants.DISPLAY_MODE_SUMMARY)
@@ -828,19 +834,19 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
828
834
  # 20200430 this was previously part of class_init, but is now called in
829
835
  # a second loop. Because calview.EventsParams copies parameters from Events.
830
836
 
831
- actions.install_layout(cls, "detail_layout", layouts.DetailLayout)
832
- actions.install_layout(
837
+ install_layout(cls, "detail_layout", layouts.DetailLayout)
838
+ install_layout(
833
839
  cls,
834
840
  "insert_layout",
835
841
  layouts.InsertLayout,
836
842
  window_size=(cls.insert_layout_width, "auto"),
837
843
  )
838
- actions.install_layout(cls, "card_layout", layouts.DetailLayout)
844
+ install_layout(cls, "card_layout", layouts.DetailLayout)
839
845
  # actions.install_layout(cls, "list_layout", layouts.DetailLayout)
840
846
 
841
847
  cls.extra_layouts = dict()
842
848
  for name, main in cls.get_extra_layouts():
843
- layout_instance = actions.resolve_layout(
849
+ layout_instance = resolve_layout(
844
850
  cls, "extra_layout", main, layouts.DetailLayout
845
851
  )
846
852
  cls.extra_layouts.update({name: layout_instance})
@@ -1079,7 +1085,7 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1079
1085
  # logger.info("20181230 %r detail_action is %r", cls, cls.detail_action)
1080
1086
  if cls.editable:
1081
1087
  cls.submit_detail = cls._bind_action(
1082
- "submit_detail", SUBMIT_DETAIL, True
1088
+ "submit_detail", actions.SUBMIT_DETAIL, True
1083
1089
  )
1084
1090
 
1085
1091
  # avoid inheriting the following actions from parent:
@@ -1099,13 +1105,13 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1099
1105
  )
1100
1106
  if cls.allow_delete:
1101
1107
  cls.delete_action = cls._bind_action(
1102
- "delete_action", DELETE_ACTION, True
1108
+ "delete_action", actions.DELETE_ACTION, True
1103
1109
  )
1104
1110
  cls.update_action = cls._bind_action(
1105
- "update_action", UPDATE_ACTION, True)
1111
+ "update_action", actions.UPDATE_ACTION, True)
1106
1112
  if cls.detail_layout:
1107
1113
  cls.validate_form = cls._bind_action(
1108
- "validate_form", VALIDATE_FORM, True
1114
+ "validate_form", actions.VALIDATE_FORM, True
1109
1115
  )
1110
1116
 
1111
1117
  if is_string(cls.workflow_owner_field):
@@ -1505,14 +1511,14 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1505
1511
  name = "main"
1506
1512
  if name in kw:
1507
1513
  raise Exception(
1508
- "set_detail() got two definitions for %r." % name)
1514
+ "set_form_layout() got two definitions for %r." % name)
1509
1515
  kw[name] = dtl
1510
1516
  else:
1511
1517
  if not isinstance(dtl, lcl):
1512
- msg = "{} is neither a string nor a layout".format(
1513
- type(dtl))
1518
+ msg = f"{repr(dtl)} is neither a string nor a layout"
1514
1519
  raise Exception(msg)
1515
1520
  assert dtl._datasource is None
1521
+
1516
1522
  # added for 20120914c but it wasn't the problem
1517
1523
  # if existing and not isinstance(existing, string_types):
1518
1524
  if existing and not is_string(existing):
@@ -1536,8 +1542,19 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1536
1542
  dtl._element_options.update(existing._element_options)
1537
1543
  dtl._datasource = self
1538
1544
  setattr(self, attname, dtl)
1539
- if kw:
1540
- getattr(self, attname).update(**kw)
1545
+
1546
+ # The following tests added for :ref:`book.changes.20250523`:
1547
+ if (kernel := settings.SITE.kernel) is not None:
1548
+ if (front_end := kernel.editing_front_end) is not None:
1549
+ hname = front_end.ui_handle_attr_name
1550
+ # we do not want any inherited handle
1551
+ h = existing.__dict__.get(hname, None)
1552
+ if h is not None:
1553
+ raise Exception(
1554
+ f"{existing} has already a layout handle {h.ui}")
1555
+
1556
+ if kw:
1557
+ getattr(self, attname).update(**kw)
1541
1558
 
1542
1559
  @classmethod
1543
1560
  def add_detail_panel(self, *args, **kw):
@@ -1606,6 +1623,7 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1606
1623
 
1607
1624
  @classmethod
1608
1625
  def after_site_setup(cls, site):
1626
+ # print(f"20250523 after_site_setup {cls}")
1609
1627
  self = cls
1610
1628
  # ~ raise "20100616"
1611
1629
  # ~ assert not self._setup_done, "%s.setup() called again" % self
@@ -1627,11 +1645,14 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1627
1645
  # settings.SITE.register_virtual_field(vf)
1628
1646
 
1629
1647
  if not self.is_abstract():
1630
- actions.register_params(self)
1648
+ register_params(self)
1649
+ if self.parameters is not None:
1650
+ assert isinstance(self.params_layout, self._params_layout_class)
1651
+ # print(f"20250523 params_layout ok for {repr(self)}")
1631
1652
  self._collect_actions()
1632
1653
 
1633
1654
  if not self.is_abstract():
1634
- actions.setup_params_choosers(self)
1655
+ fields.setup_params_choosers(self)
1635
1656
 
1636
1657
  # ~ logger.info("20130906 Gonna Actor.do_setup() on %s", self)
1637
1658
  self.do_setup()
@@ -1952,6 +1973,19 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1952
1973
  # assert_safe(html_text) # temporary 20240506
1953
1974
  return format_html(DIVTPL, html_text)
1954
1975
 
1976
+ @classmethod
1977
+ def get_table_as_tiles(cls, obj, ar):
1978
+ sar = cls.create_request(parent=ar, master_instance=obj,
1979
+ is_on_main_actor=False)
1980
+ html = SAFE_EMPTY
1981
+ for i, obj in enumerate(sar.data_iterator):
1982
+ if i == cls.preview_limit:
1983
+ 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
1988
+
1955
1989
  @classmethod
1956
1990
  def get_table_story(cls, obj, ar):
1957
1991
  sar = cls.create_request(parent=ar, master_instance=obj,
lino/core/atomizer.py ADDED
@@ -0,0 +1,355 @@
1
+ # Copyright 2009-2026 Rumma & Ko Ltd
2
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
3
+
4
+ from django.db import models
5
+ from django.conf import settings
6
+
7
+ from lino import logger
8
+ from lino.utils import curry
9
+ from lino.utils import choosers
10
+ from lino.core import store
11
+ from lino.core import actors
12
+ from lino.core import fields
13
+ from lino.core.utils import resolve_model
14
+
15
+
16
+ def make_remote_field(model, name):
17
+ parts = name.split("__")
18
+ if len(parts) == 1:
19
+ return
20
+ # It's going to be a RemoteField
21
+ # logger.warning("20151203 RemoteField %s in %s", name, cls)
22
+
23
+ cls = model
24
+ field_chain = []
25
+ editable = False
26
+ gettable = True
27
+ leaf_chooser = None
28
+ for n in parts:
29
+ if model is None:
30
+ return
31
+ # raise Exception(
32
+ # "Invalid remote field {0} for {1}".format(name, cls))
33
+
34
+ if isinstance(model, str):
35
+ # Django 1.9 no longer resolves the
36
+ # rel.model of ForeignKeys on abstract
37
+ # models, so we do it here.
38
+ model = resolve_model(model)
39
+ # logger.warning("20151203 %s", model)
40
+
41
+ fld = model.get_data_elem(n)
42
+ if fld is None:
43
+ return
44
+ # raise Exception(
45
+ # "Invalid RemoteField %s.%s (no field %s in %s)" %
46
+ # (full_model_name(model), name, n, full_model_name(model)))
47
+
48
+ if isinstance(fld, fields.DummyField):
49
+ # a remote field containing at least one dummy field is itself a
50
+ # dummy field
51
+ return fld
52
+
53
+ # Why was this? it caused docs/specs/avanti/courses.rst to fail
54
+ # if isinstance(fld, models.ManyToOneRel):
55
+ # gettable = False
56
+
57
+ # make sure that the atomizer gets created.
58
+ get_atomizer(model, fld, fld.name)
59
+
60
+ if isinstance(fld, fields.VirtualField):
61
+ fld.lino_resolve_type()
62
+ leaf_chooser = choosers.check_for_chooser(model, fld)
63
+
64
+ field_chain.append(fld)
65
+ if isinstance(
66
+ fld, (models.OneToOneRel, models.OneToOneField, models.ForeignKey)
67
+ ):
68
+ editable = True
69
+ if getattr(fld, "remote_field", None):
70
+ model = fld.remote_field.model
71
+ else:
72
+ model = None
73
+
74
+ # if not gettable:
75
+ # # raise Exception("20231112")
76
+ # return RemoteField(none_getter, name, fld)
77
+
78
+ if leaf_chooser is not None:
79
+ d = choosers.get_choosers_dict(cls)
80
+ d[name] = leaf_chooser
81
+
82
+ def getter(obj, ar=None):
83
+ try:
84
+ for fld in field_chain:
85
+ if obj is None:
86
+ return None
87
+ obj = fld._lino_atomizer.full_value_from_object(obj, ar)
88
+ return obj
89
+ except Exception as e:
90
+ # raise
91
+ msg = "Error while computing {}: {} ({} in {})"
92
+ raise Exception(msg.format(name, e, fld, field_chain))
93
+ # ~ if False: # only for debugging
94
+ if True: # see 20130802
95
+ logger.exception(e)
96
+ return str(e)
97
+ return None
98
+
99
+ if not editable:
100
+ rf = fields.RemoteField(getter, name, fld)
101
+ # choosers.check_for_chooser(model, rf)
102
+ return rf
103
+
104
+ def setter(obj, value):
105
+ # logger.info("20180712 %s setter() %s", name, value)
106
+ # all intermediate fields are OneToOneRel
107
+ target = obj
108
+ try:
109
+ for i, fld in enumerate(field_chain):
110
+ # print("20180712a %s" % fld)
111
+ if isinstance(fld, (models.OneToOneRel, models.ForeignKey)):
112
+ reltarget = getattr(target, fld.name, None)
113
+ if reltarget is None:
114
+ rkw = {fld.field.name: target}
115
+ # print(
116
+ # "20180712 create {}({})".format(
117
+ # fld.related_model, rkw))
118
+ reltarget = fld.related_model(**rkw)
119
+ reltarget.save_new_instance(
120
+ fld.related_model.get_default_table().create_request()
121
+ )
122
+
123
+ setattr(target, fld.name, reltarget)
124
+
125
+ if target == obj and target.id is None:
126
+ # Model.save_new_instance will be called do not insert this record.
127
+ target = reltarget
128
+ continue
129
+ target.full_clean()
130
+ target.save()
131
+ # print("20180712b {}.{} = {}".format(
132
+ # target, fld.name, reltarget))
133
+ target = reltarget
134
+ else:
135
+ setattr(target, fld.name, value)
136
+ target.full_clean()
137
+ target.save()
138
+ # print(
139
+ # "20180712c setattr({},{},{}".format(
140
+ # target, fld.name, value))
141
+ return True
142
+ except Exception as e:
143
+ if False: # only for debugging
144
+ logger.exception(e)
145
+ return str(e)
146
+ raise e.__class__("Error while setting %s: %s" % (name, e))
147
+ return False
148
+
149
+ rf = fields.RemoteField(getter, name, fld, setter)
150
+ # choosers.check_for_chooser(model, rf)
151
+ return rf
152
+
153
+
154
+ def create_atomizer(holder, fld, name):
155
+ """
156
+ The holder is where the (potential) choices come from. It can be
157
+ a model, an actor or an action. `fld` is a data element.
158
+ """
159
+ if name is None:
160
+ # print("20181023 create_atomizer() no name {}".format(fld))
161
+ return
162
+ # raise Exception("20181023 create_atomizer() {}".format(fld))
163
+ if isinstance(fld, fields.RemoteField):
164
+ """
165
+ Hack: we create a StoreField based on the remote field,
166
+ then modify some of its behaviour.
167
+ """
168
+ sf = create_atomizer(holder, fld.field, fld.name)
169
+
170
+ # print("20180712 create_atomizer {} from {}".format(sf, fld.field))
171
+
172
+ def value_from_object(unused, obj, ar=None):
173
+ return fld.func(obj, ar)
174
+
175
+ def full_value_from_object(unused, obj, ar=None):
176
+ return fld.func(obj, ar)
177
+
178
+ def set_value_in_object(sf, ar, instance, v):
179
+ # print("20180712 {}.set_value_in_object({}, {})".format(
180
+ # sf, instance, v))
181
+ old_value = sf.value_from_object(instance, ar.request)
182
+ if old_value != v:
183
+ return fld.setter(instance, v)
184
+
185
+ sf.value_from_object = curry(value_from_object, sf)
186
+ sf.full_value_from_object = curry(full_value_from_object, sf)
187
+ sf.set_value_in_object = curry(set_value_in_object, sf)
188
+ return sf
189
+
190
+ meth = getattr(fld, "_return_type_for_method", None)
191
+ if meth is not None:
192
+ # uh, this is tricky...
193
+ # raise Exception(f"20250523 {fld} has a _return_type_for_method")
194
+ # print(f"20250523 {fld} has a _return_type_for_method")
195
+ return store.MethodStoreField(fld, name)
196
+
197
+ # sf_class = getattr(fld, 'lino_atomizer_class', None)
198
+ # if sf_class is not None:
199
+ # return sf_class(fld, name)
200
+
201
+ if isinstance(fld, fields.DummyField):
202
+ return None
203
+ if isinstance(fld, fields.RequestField):
204
+ delegate = create_atomizer(holder, fld.return_type, fld.name)
205
+ return store.RequestStoreField(fld, delegate, name)
206
+ if isinstance(fld, type) and issubclass(fld, actors.Actor):
207
+ # raise Exception(f"20250523 {fld} is an actor!")
208
+ return
209
+ # if isinstance(fld, type) and issubclass(fld, actors.Actor):
210
+ # raise Exception("20230219")
211
+ # # 20210618 & 20230218
212
+ # if settings.SITE.kernel.default_ui.support_async:
213
+ # return SlaveTableStoreField(fld, name)
214
+ # return DisplayStoreField(fld, name)
215
+
216
+ if isinstance(fld, fields.VirtualField):
217
+ delegate = create_atomizer(holder, fld.return_type, fld.name)
218
+ if delegate is None:
219
+ # e.g. VirtualField with DummyField as return_type
220
+ return None
221
+ # raise Exception("No atomizer for %s %s %s" % (
222
+ # holder, fld.return_type, fld.name))
223
+ return store.VirtStoreField(fld, delegate, name)
224
+ if isinstance(fld, models.FileField):
225
+ return store.FileFieldStoreField(fld, name)
226
+ if isinstance(fld, models.ManyToManyField):
227
+ return store.StoreField(fld, name)
228
+ if isinstance(fld, fields.PasswordField):
229
+ return store.PasswordStoreField(fld, name)
230
+ if isinstance(fld, models.OneToOneField):
231
+ return store.OneToOneStoreField(fld, name)
232
+ if isinstance(fld, models.OneToOneRel):
233
+ return store.OneToOneRelStoreField(fld, name)
234
+
235
+ if settings.SITE.is_installed("contenttypes"):
236
+ from lino.core.gfks import GenericForeignKey, GenericRel
237
+ from lino.modlib.gfks.fields import GenericForeignKeyIdField
238
+
239
+ if isinstance(fld, GenericForeignKey):
240
+ return store.GenericForeignKeyField(fld, name)
241
+ if isinstance(fld, GenericRel):
242
+ return store.GenericRelField(fld, name)
243
+ if isinstance(fld, GenericForeignKeyIdField):
244
+ return store.ComboStoreField(fld, name)
245
+
246
+ if isinstance(fld, models.ForeignKey):
247
+ return store.ForeignKeyStoreField(fld, name)
248
+ if isinstance(fld, models.TimeField):
249
+ return store.TimeStoreField(fld, name)
250
+ if isinstance(fld, models.DateTimeField):
251
+ return store.DateTimeStoreField(fld, name)
252
+ if isinstance(fld, fields.IncompleteDateField):
253
+ return store.IncompleteDateStoreField(fld, name)
254
+ if isinstance(fld, models.DateField):
255
+ return store.DateStoreField(fld, name)
256
+ if isinstance(fld, models.BooleanField):
257
+ return store.BooleanStoreField(fld, name)
258
+ if isinstance(fld, models.DecimalField):
259
+ return store.DecimalStoreField(fld, name)
260
+ if isinstance(fld, models.AutoField):
261
+ return store.AutoStoreField(fld, name)
262
+ # kw.update(type='int')
263
+ if isinstance(fld, models.SmallIntegerField):
264
+ return store.IntegerStoreField(fld, name)
265
+ if isinstance(fld, fields.DisplayField):
266
+ return store.DisplayStoreField(fld, name)
267
+ if isinstance(fld, models.IntegerField):
268
+ return store.IntegerStoreField(fld, name)
269
+ if isinstance(fld, fields.PreviewTextField):
270
+ return store.PreviewTextStoreField(fld, name)
271
+ if isinstance(fld, models.ManyToOneRel):
272
+ # raise Exception("20190625 {} {} {}".format(holder, fld, name))
273
+ return
274
+ if (sft := store.FIELD_TYPES.get(fld.__class__, None)) is not None:
275
+ return sft(fld, name)
276
+ kw = {}
277
+ if choosers.uses_simple_values(holder, fld):
278
+ return store.StoreField(fld, name, **kw)
279
+ else:
280
+ return store.ComboStoreField(fld, name, **kw)
281
+
282
+
283
+ def get_atomizer(holder, fld, name):
284
+ """
285
+ Return the :term:`atomizer` for this database field.
286
+
287
+ An atomizer is an instance of a subclass of :class:`StoreField`.
288
+
289
+ """
290
+ # if name is None:
291
+ # raise Exception("20250523 name is None")
292
+ sf = getattr(fld, "_lino_atomizer", None)
293
+ if sf is None:
294
+ sf = create_atomizer(holder, fld, name)
295
+ if sf is None:
296
+ # print(f"20250523 {fld} on {holder} has no StoreField")
297
+ return
298
+ assert isinstance(sf, store.StoreField)
299
+ # if not isinstance(sf, StoreField):
300
+ # raise Exception("{} is not a StoreField".format(sf))
301
+ if isinstance(fld, type):
302
+ raise Exception("20240913 trying to set class attribute")
303
+ setattr(fld, "_lino_atomizer", sf)
304
+ return sf
305
+
306
+
307
+ def fields_list(model, field_names):
308
+ """
309
+ Return a set with the names of the specified fields, checking
310
+ whether each of them exists.
311
+
312
+ Arguments: `model` is any subclass of `django.db.models.Model`. It
313
+ may be a string with the full name of a model
314
+ (e.g. ``"myapp.MyModel"``). `field_names` is a single string with
315
+ a space-separated list of field names.
316
+
317
+ If one of the names refers to a dummy field, this name will be ignored
318
+ silently.
319
+
320
+ For example if you have a model `MyModel` with two fields `foo` and
321
+ `bar`, then ``dd.fields_list(MyModel,"foo bar")`` will return
322
+ ``['foo','bar']`` and ``dd.fields_list(MyModel,"foo baz")`` will raise
323
+ an exception.
324
+
325
+ TODO: either rename this to `fields_set` or change it to return an
326
+ iterable on the fields.
327
+ """
328
+ lst = set()
329
+ names_list = field_names.split()
330
+
331
+ for name in names_list:
332
+ if name == "*":
333
+ explicit_names = set()
334
+ for name in names_list:
335
+ if name != "*":
336
+ explicit_names.add(name)
337
+ for de in fields.wildcard_data_elems(model):
338
+ if not isinstance(de, DummyField):
339
+ if de.name not in explicit_names:
340
+ if fields.use_as_wildcard(de):
341
+ lst.add(de.name)
342
+ else:
343
+ e = model.get_data_elem(name)
344
+ if e is None:
345
+ raise fields.FieldDoesNotExist(
346
+ "No data element %r in %s" % (name, model))
347
+ if not hasattr(e, "name"):
348
+ raise fields.FieldDoesNotExist(
349
+ "%s %r in %s has no name" % (e.__class__, name, model)
350
+ )
351
+ if isinstance(e, fields.DummyField):
352
+ pass
353
+ else:
354
+ lst.add(e.name)
355
+ return lst
lino/core/boundaction.py CHANGED
@@ -86,8 +86,12 @@ class BoundAction(object):
86
86
  return self.action.get_window_layout(self.actor)
87
87
 
88
88
  def get_layout_handel(self):
89
- layout = self.get_window_layout()
90
- return layout.get_layout_handle() if layout is not None else None
89
+ if (layout := self.get_window_layout()) is not None:
90
+ try:
91
+ return layout.get_layout_handle()
92
+ except Exception as e:
93
+ raise Exception(
94
+ f"20250523 get_layout_handle for {self} failed ({e})")
91
95
 
92
96
  def get_window_size(self):
93
97
  return self.action.get_window_size(self.actor)
@@ -163,7 +167,7 @@ class BoundAction(object):
163
167
  """
164
168
  u = ar.get_user()
165
169
 
166
- if not self.action.get_view_permission(u.user_type):
170
+ if not self.action.get_action_view_permission(self.actor, u.user_type):
167
171
  return False
168
172
  if not self._allow(u, obj, state):
169
173
  return False
@@ -186,7 +190,7 @@ class BoundAction(object):
186
190
  # return False
187
191
  # elif not self.action.defining_actor.get_view_permission(user_type):
188
192
  # return False
189
- if not self.action.get_view_permission(user_type):
193
+ if not self.action.get_action_view_permission(self.actor, user_type):
190
194
  return False
191
195
  return self.allow_view(user_type)
192
196
 
lino/core/constants.py CHANGED
@@ -65,6 +65,7 @@ DISPLAY_MODE_LIST = "list"
65
65
  DISPLAY_MODE_CARDS = "cards"
66
66
  DISPLAY_MODE_GALLERY = "gallery"
67
67
  DISPLAY_MODE_STORY = "story"
68
+ DISPLAY_MODE_TILES = "tiles"
68
69
 
69
70
  DISPLAY_MODES = {
70
71
  DISPLAY_MODE_GRID,
@@ -74,7 +75,8 @@ DISPLAY_MODES = {
74
75
  DISPLAY_MODE_LIST,
75
76
  DISPLAY_MODE_CARDS,
76
77
  DISPLAY_MODE_GALLERY,
77
- DISPLAY_MODE_STORY}
78
+ DISPLAY_MODE_STORY,
79
+ DISPLAY_MODE_TILES}
78
80
 
79
81
  BASIC_DISPLAY_MODES = {
80
82
  DISPLAY_MODE_GRID,
@@ -202,3 +204,23 @@ def parse_boolean(v):
202
204
  if v in ("false", "off", False):
203
205
  return False
204
206
  raise Exception("Invalid boolean value %r" % v)
207
+
208
+
209
+ # TILE_TEMPLATE = """
210
+ # <div style="margin: 20px;" class="p-card md:w-25rem"><div class="p-card-content">
211
+ # {chunk}
212
+ # </div></div>
213
+ # """
214
+ #
215
+ # TILE_TEMPLATE = """
216
+ # <div style="position: relative; width: fit-content; display: inline-block;
217
+ # background-color:cornsilk; margin:1rem; padding: 1rem; width: 20rem; max-height: 35rem;">
218
+ # {chunk}
219
+ # </div>
220
+ # """
221
+
222
+ TILE_TEMPLATE = """
223
+ <div class="l-tile">
224
+ {chunk}
225
+ </div>
226
+ """