lino 25.5.2__py3-none-any.whl → 25.5.3__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.
- lino/__init__.py +1 -1
- lino/api/dd.py +5 -3
- lino/api/doctest.py +2 -2
- lino/core/__init__.py +3 -3
- lino/core/actions.py +60 -582
- lino/core/actors.py +66 -32
- lino/core/atomizer.py +355 -0
- lino/core/boundaction.py +8 -4
- lino/core/constants.py +3 -1
- lino/core/dbtables.py +4 -3
- lino/core/elems.py +33 -21
- lino/core/fields.py +40 -210
- lino/core/kernel.py +18 -13
- lino/core/layouts.py +30 -57
- lino/core/model.py +6 -4
- lino/core/permissions.py +18 -0
- lino/core/renderer.py +5 -1
- lino/core/requests.py +13 -7
- lino/core/signals.py +1 -1
- lino/core/site.py +1 -1
- lino/core/store.py +13 -156
- lino/core/tables.py +5 -2
- lino/core/utils.py +124 -1
- lino/locale/bn/LC_MESSAGES/django.po +1034 -868
- lino/locale/de/LC_MESSAGES/django.mo +0 -0
- lino/locale/de/LC_MESSAGES/django.po +996 -892
- lino/locale/django.pot +968 -869
- lino/locale/es/LC_MESSAGES/django.po +1032 -869
- lino/locale/et/LC_MESSAGES/django.po +1032 -866
- lino/locale/fr/LC_MESSAGES/django.po +1034 -866
- lino/locale/nl/LC_MESSAGES/django.po +1040 -868
- lino/locale/pt_BR/LC_MESSAGES/django.po +1029 -868
- lino/locale/zh_Hant/LC_MESSAGES/django.po +1029 -868
- lino/mixins/duplicable.py +8 -2
- lino/mixins/registrable.py +1 -1
- lino/modlib/changes/utils.py +4 -3
- lino/modlib/extjs/ext_renderer.py +1 -1
- lino/modlib/extjs/views.py +5 -0
- lino/modlib/memo/mixins.py +1 -3
- lino/modlib/uploads/ui.py +6 -8
- lino/modlib/users/fixtures/demo_users.py +16 -13
- lino/utils/choosers.py +11 -1
- lino/utils/diag.py +6 -4
- lino/utils/fieldutils.py +14 -11
- lino/utils/instantiator.py +4 -2
- lino/utils/report.py +5 -3
- {lino-25.5.2.dist-info → lino-25.5.3.dist-info}/METADATA +1 -1
- {lino-25.5.2.dist-info → lino-25.5.3.dist-info}/RECORD +51 -50
- {lino-25.5.2.dist-info → lino-25.5.3.dist-info}/WHEEL +0 -0
- {lino-25.5.2.dist-info → lino-25.5.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.5.2.dist-info → lino-25.5.3.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
|
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
|
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(
|
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 =
|
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
|
-
|
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 =
|
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"
|
716
|
-
return
|
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
|
-
|
832
|
-
|
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
|
-
|
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 =
|
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
|
-
"
|
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"
|
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
|
-
|
1540
|
-
|
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
|
-
|
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
|
-
|
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_story_item(sar)
|
1985
|
+
# assert_safe(s) # temporary 20240506
|
1986
|
+
html += 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
|
90
|
-
|
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.
|
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.
|
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,
|
lino/core/dbtables.py
CHANGED
@@ -14,7 +14,8 @@ from lino.core import actors
|
|
14
14
|
from lino.core.model import Model
|
15
15
|
from lino.core import actions
|
16
16
|
from lino.core import fields
|
17
|
-
from lino.
|
17
|
+
from lino.core.atomizer import fields_list
|
18
|
+
|
18
19
|
from lino import logger
|
19
20
|
|
20
21
|
import datetime
|
@@ -382,13 +383,13 @@ class Table(AbstractTable):
|
|
382
383
|
if self.model is not None:
|
383
384
|
if isinstance(self.hidden_columns, str):
|
384
385
|
self.hidden_columns = frozenset(
|
385
|
-
|
386
|
+
fields_list(self.model, self.hidden_columns)
|
386
387
|
)
|
387
388
|
self.hidden_columns |= self.model.hidden_columns
|
388
389
|
|
389
390
|
if isinstance(self.active_fields, str):
|
390
391
|
self.active_fields = frozenset(
|
391
|
-
|
392
|
+
fields_list(self.model, self.active_fields)
|
392
393
|
)
|
393
394
|
self.active_fields |= self.model.active_fields
|
394
395
|
self.hidden_elements |= self.model.hidden_elements
|