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.
- lino/__init__.py +1 -1
- lino/api/doctest.py +21 -0
- lino/core/actions.py +59 -25
- lino/core/actors.py +38 -16
- lino/core/boundaction.py +16 -0
- lino/core/choicelists.py +7 -7
- lino/core/constants.py +3 -0
- lino/core/dashboard.py +1 -0
- lino/core/dbtables.py +1 -1
- lino/core/elems.py +38 -13
- lino/core/fields.py +20 -11
- lino/core/kernel.py +8 -0
- lino/core/layouts.py +6 -2
- lino/core/menus.py +3 -6
- lino/core/model.py +5 -4
- lino/core/renderer.py +14 -5
- lino/core/requests.py +8 -7
- lino/core/signals.py +1 -0
- lino/core/site.py +48 -28
- lino/core/store.py +4 -2
- lino/core/tables.py +23 -10
- lino/core/utils.py +4 -1
- lino/core/workflows.py +2 -1
- lino/help_texts.py +1 -2
- lino/management/commands/prep.py +2 -2
- lino/management/commands/show.py +8 -10
- lino/mixins/__init__.py +14 -13
- lino/mixins/periods.py +2 -0
- lino/mixins/sequenced.py +1 -1
- lino/modlib/about/models.py +4 -3
- lino/modlib/checkdata/__init__.py +42 -36
- lino/modlib/checkdata/choicelists.py +9 -1
- lino/modlib/checkdata/fixtures/checkdata.py +4 -2
- lino/modlib/checkdata/models.py +9 -2
- lino/modlib/comments/models.py +4 -3
- lino/modlib/extjs/ext_renderer.py +4 -4
- lino/modlib/extjs/views.py +8 -2
- lino/modlib/gfks/fields.py +1 -1
- lino/modlib/help/__init__.py +3 -3
- lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
- lino/modlib/help/fixtures/demo2.py +6 -1
- lino/modlib/help/management/commands/makehelp.py +4 -1
- lino/modlib/help/models.py +2 -1
- lino/modlib/help/utils.py +12 -6
- lino/modlib/linod/choicelists.py +57 -4
- lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
- lino/modlib/linod/management/commands/linod.py +0 -13
- lino/modlib/linod/mixins.py +8 -0
- lino/modlib/linod/models.py +29 -30
- lino/modlib/memo/__init__.py +7 -7
- lino/modlib/memo/management/__init__,py +0 -0
- lino/modlib/memo/management/commands/__init__.py +0 -0
- lino/modlib/memo/management/commands/removeurls.py +67 -0
- lino/modlib/memo/mixins.py +1 -9
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/config/notify/summary.eml +5 -2
- lino/modlib/notify/fixtures/demo2.py +5 -6
- lino/modlib/notify/models.py +9 -10
- lino/modlib/periods/__init__.py +11 -8
- lino/modlib/periods/choicelists.py +16 -10
- lino/modlib/periods/models.py +45 -45
- lino/modlib/summaries/fixtures/checksummaries.py +4 -2
- lino/modlib/system/models.py +17 -18
- lino/modlib/uploads/fixtures/demo.py +9 -3
- lino/modlib/uploads/mixins.py +5 -2
- lino/modlib/uploads/models.py +15 -9
- lino/modlib/uploads/utils.py +4 -1
- lino/modlib/users/__init__.py +59 -18
- lino/modlib/users/actions.py +24 -20
- lino/modlib/users/fixtures/demo_users.py +2 -35
- lino/modlib/users/mixins.py +3 -4
- lino/modlib/users/models.py +53 -13
- lino/modlib/users/ui.py +30 -16
- lino/modlib/users/utils.py +5 -6
- lino/projects/std/settings.py +1 -1
- lino/sphinxcontrib/logo/templates/footer.html +1 -0
- lino/utils/ajax.py +1 -1
- lino/utils/cycler.py +5 -0
- lino/utils/dbhash.py +4 -9
- lino/utils/dpy.py +2 -2
- lino/utils/format_date.py +4 -3
- lino/utils/html.py +13 -5
- lino/utils/jsgen.py +1 -1
- lino/utils/quantities.py +8 -0
- lino/utils/soup.py +75 -94
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/METADATA +1 -1
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/RECORD +90 -87
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/WHEEL +0 -0
- {lino-25.6.0.dist-info → lino-25.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {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.
|
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
|
-
# "
|
265
|
-
#
|
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
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
-
|
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.
|
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}:
|
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
|
-
|
741
|
-
|
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
|
-
@
|
745
|
-
def help_text(self
|
746
|
-
|
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.
|
1103
|
+
if (ia := cls.get_insert_action()) is not None:
|
1103
1104
|
cls.insert_action = cls._bind_action(
|
1104
|
-
"insert_action",
|
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
|
-
|
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
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
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
|
-
|
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
|
-
|
1985
|
-
|
1986
|
-
|
1987
|
-
return
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
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.
|
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-
|
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
|
-
|
2462
|
-
|
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
|
-
|
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)
|