lino 25.2.2__py3-none-any.whl → 25.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lino/__init__.py +8 -3
- lino/api/dd.py +11 -35
- lino/api/doctest.py +49 -17
- lino/api/selenium.py +1 -1
- lino/core/actions.py +25 -23
- lino/core/actors.py +52 -23
- lino/core/choicelists.py +10 -8
- lino/core/dbtables.py +1 -1
- lino/core/elems.py +47 -31
- lino/core/fields.py +19 -9
- lino/core/kernel.py +26 -20
- lino/core/model.py +27 -16
- lino/core/renderer.py +2 -2
- lino/core/requests.py +103 -56
- lino/core/site.py +5 -5
- lino/core/store.py +5 -2
- lino/core/utils.py +12 -7
- lino/help_texts.py +7 -8
- lino/mixins/duplicable.py +6 -4
- lino/mixins/sequenced.py +17 -6
- lino/modlib/__init__.py +0 -2
- lino/modlib/changes/models.py +21 -10
- lino/modlib/checkdata/models.py +59 -24
- lino/modlib/comments/fixtures/demo2.py +12 -3
- lino/modlib/comments/models.py +7 -7
- lino/modlib/comments/ui.py +8 -5
- lino/modlib/export_excel/models.py +7 -5
- lino/modlib/extjs/__init__.py +2 -2
- lino/modlib/extjs/views.py +66 -22
- lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
- lino/modlib/jinja/mixins.py +73 -0
- lino/modlib/jinja/models.py +6 -0
- lino/modlib/linod/__init__.py +1 -0
- lino/modlib/linod/choicelists.py +21 -0
- lino/modlib/linod/consumers.py +13 -4
- lino/modlib/linod/fixtures/__init__.py +0 -0
- lino/modlib/linod/fixtures/linod.py +32 -0
- lino/modlib/linod/management/commands/linod.py +6 -2
- lino/modlib/linod/mixins.py +18 -14
- lino/modlib/linod/models.py +4 -2
- lino/modlib/memo/mixins.py +2 -1
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/models.py +19 -11
- lino/modlib/printing/actions.py +47 -42
- lino/modlib/printing/choicelists.py +17 -15
- lino/modlib/printing/mixins.py +22 -20
- lino/modlib/publisher/models.py +5 -5
- lino/modlib/summaries/models.py +3 -2
- lino/modlib/system/models.py +28 -29
- lino/modlib/uploads/__init__.py +14 -11
- lino/modlib/uploads/actions.py +2 -8
- lino/modlib/uploads/choicelists.py +10 -10
- lino/modlib/uploads/fixtures/std.py +17 -0
- lino/modlib/uploads/mixins.py +20 -8
- lino/modlib/uploads/models.py +62 -38
- lino/modlib/uploads/ui.py +15 -9
- lino/utils/__init__.py +0 -1
- lino/utils/jscompressor.py +4 -4
- lino/utils/media.py +45 -23
- lino/utils/report.py +5 -4
- lino/utils/restify.py +2 -2
- lino/utils/soup.py +26 -8
- lino/utils/xml.py +19 -5
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/METADATA +1 -1
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/RECORD +68 -65
- lino/mixins/uploadable.py +0 -3
- lino/utils/requests.py +0 -55
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/WHEEL +0 -0
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/COPYING +0 -0
lino/core/elems.py
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
8
|
+
from lino.utils.jsgen import VisibleComponent
|
8
9
|
import types
|
9
10
|
import decimal
|
10
11
|
from lxml.html import fromstring
|
@@ -181,7 +182,8 @@ class GridColumn(jsgen.Component):
|
|
181
182
|
elif isinstance(editor.field, fields.VirtualField):
|
182
183
|
# kw.update(sortable=False)
|
183
184
|
if has_fk_renderer(editor.field.return_type):
|
184
|
-
rend = fk_renderer(
|
185
|
+
rend = fk_renderer(
|
186
|
+
editor.field.return_type, editor.field.name)
|
185
187
|
if rend:
|
186
188
|
kw.update(renderer=js_code(rend))
|
187
189
|
kw.update(editable=editor.editable)
|
@@ -221,9 +223,6 @@ class Calendar(jsgen.Component):
|
|
221
223
|
value_template = "new Lino.CalendarPanel(%s)"
|
222
224
|
|
223
225
|
|
224
|
-
from lino.utils.jsgen import VisibleComponent
|
225
|
-
|
226
|
-
|
227
226
|
class LayoutElement(VisibleComponent):
|
228
227
|
stored = False
|
229
228
|
ext_name = None
|
@@ -358,10 +357,12 @@ class LayoutElement(VisibleComponent):
|
|
358
357
|
label = self.get_label()
|
359
358
|
if not label:
|
360
359
|
raise Exception(
|
361
|
-
"Item %s of tabbed %s has no label!" % (
|
360
|
+
"Item %s of tabbed %s has no label!" % (
|
361
|
+
self, self.layout_handle)
|
362
362
|
)
|
363
363
|
ukw = dict(title=label)
|
364
|
-
ukw.update(listeners=dict(
|
364
|
+
ukw.update(listeners=dict(
|
365
|
+
activate=js_code("Lino.on_tab_activate")))
|
365
366
|
# add_help_text(
|
366
367
|
# ukw, self.help_text, 'title',
|
367
368
|
# self.layout_handle.layout._datasource, self.name)
|
@@ -485,14 +486,13 @@ class FieldElement(LayoutElement):
|
|
485
486
|
self.hide_sum = hide_sum
|
486
487
|
|
487
488
|
if "listeners" not in kw:
|
488
|
-
if not isinstance(layout_handle.layout, ColumnsLayout):
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
)
|
489
|
+
# if not isinstance(layout_handle.layout, ColumnsLayout):
|
490
|
+
layout_handle.ui.renderer.add_help_text(
|
491
|
+
kw,
|
492
|
+
self.field.help_text,
|
493
|
+
self.field.verbose_name,
|
494
|
+
layout_handle.layout._datasource,
|
495
|
+
self.field.name)
|
496
496
|
|
497
497
|
# http://www.rowlands-bcs.com/extjs/tips/tooltips-form-fields
|
498
498
|
# if self.field.__doc__:
|
@@ -747,7 +747,8 @@ class TextFieldElement(FieldElement):
|
|
747
747
|
oui5_field_template = "/openui5/elems/field/TextFieldElement.xml"
|
748
748
|
|
749
749
|
def __init__(self, layout_handle, field, **kw):
|
750
|
-
self.format = getattr(
|
750
|
+
self.format = getattr(
|
751
|
+
field, "format", None) or settings.SITE.textfield_format
|
751
752
|
|
752
753
|
if layout_handle.ui.renderer.extjs_version == 3:
|
753
754
|
self.value_template = "new Ext.form.TextArea(%s)"
|
@@ -885,7 +886,8 @@ class CharFieldElement(FieldElement):
|
|
885
886
|
kw.update(maxLength=self.field.max_length)
|
886
887
|
if self.field.max_length <= 10:
|
887
888
|
kw.update(
|
888
|
-
boxMinWidth=js_code("Lino.chars2width(%d)" %
|
889
|
+
boxMinWidth=js_code("Lino.chars2width(%d)" %
|
890
|
+
self.field.max_length)
|
889
891
|
)
|
890
892
|
for lino_name, extjs_name in (
|
891
893
|
("regex", "regex"),
|
@@ -945,7 +947,8 @@ class ComboFieldElement(FieldElement):
|
|
945
947
|
if not isinstance(self.layout_handle.layout, ColumnsLayout) and not isinstance(
|
946
948
|
self, SimpleRemoteComboFieldElement
|
947
949
|
):
|
948
|
-
kw.update(hiddenName=self.field.name
|
950
|
+
kw.update(hiddenName=self.field.name
|
951
|
+
+ constants.CHOICES_HIDDEN_SUFFIX)
|
949
952
|
return kw
|
950
953
|
|
951
954
|
|
@@ -1030,12 +1033,14 @@ class RemoteComboFieldElement(ComboFieldElement):
|
|
1030
1033
|
# print repr(sto)
|
1031
1034
|
if self.layout_handle.ui.renderer.extjs_version == 3:
|
1032
1035
|
kw.update(
|
1033
|
-
store=js_code(
|
1036
|
+
store=js_code(
|
1037
|
+
"new Lino.ComplexRemoteComboStore(%s)" % py2js(sto))
|
1034
1038
|
)
|
1035
1039
|
else:
|
1036
1040
|
kw.update(
|
1037
1041
|
store=js_code(
|
1038
|
-
"Ext.create('Lino.ComplexRemoteComboStore',%s)" % py2js(
|
1042
|
+
"Ext.create('Lino.ComplexRemoteComboStore',%s)" % py2js(
|
1043
|
+
sto)
|
1039
1044
|
)
|
1040
1045
|
)
|
1041
1046
|
return kw
|
@@ -1088,11 +1093,13 @@ class ForeignKeyElement(ComplexRemoteComboFieldElement):
|
|
1088
1093
|
|
1089
1094
|
if settings.SITE.kernel.web_front_ends[0].app_label == "react":
|
1090
1095
|
options = dict(
|
1091
|
-
related_actor_id=actor.actor_id, allowBlank=kw.get(
|
1096
|
+
related_actor_id=actor.actor_id, allowBlank=kw.get(
|
1097
|
+
"allowBlank", False)
|
1092
1098
|
)
|
1093
1099
|
da = actor.detail_action
|
1094
1100
|
options.update(
|
1095
|
-
view_permission=bool(da) and da.get_view_permission(
|
1101
|
+
view_permission=bool(da) and da.get_view_permission(
|
1102
|
+
get_user_profile())
|
1096
1103
|
)
|
1097
1104
|
if hasattr(self.field, "model") and hasattr(
|
1098
1105
|
self.field.model, f"create_{self.field.name}_choice"
|
@@ -1122,7 +1129,8 @@ class ForeignKeyElement(ComplexRemoteComboFieldElement):
|
|
1122
1129
|
if self.layout_handle.ui.renderer.extjs_version is not None:
|
1123
1130
|
if actor is None:
|
1124
1131
|
raise Exception(
|
1125
|
-
"20181229 {!r} {}".format(
|
1132
|
+
"20181229 {!r} {}".format(
|
1133
|
+
self, self.field.remote_field.model)
|
1126
1134
|
)
|
1127
1135
|
a1 = actor.detail_action
|
1128
1136
|
if a1 and not a1.get_view_permission(get_user_profile()):
|
@@ -1146,7 +1154,8 @@ class ForeignKeyElement(ComplexRemoteComboFieldElement):
|
|
1146
1154
|
|
1147
1155
|
kw.update(pageSize=actor.page_length)
|
1148
1156
|
if actor.model is not None:
|
1149
|
-
kw.update(emptyText=_("Select a %s...") %
|
1157
|
+
kw.update(emptyText=_("Select a %s...") %
|
1158
|
+
actor.model._meta.verbose_name)
|
1150
1159
|
return kw
|
1151
1160
|
|
1152
1161
|
def cell_html(self, ar, row):
|
@@ -1174,7 +1183,8 @@ class TimeFieldElement(FieldElement):
|
|
1174
1183
|
def get_field_options(self, **kwargs):
|
1175
1184
|
kwargs = FieldElement.get_field_options(self, **kwargs)
|
1176
1185
|
if settings.SITE.calendar_start_hour:
|
1177
|
-
kwargs["minValue"] = "{}:00".format(
|
1186
|
+
kwargs["minValue"] = "{}:00".format(
|
1187
|
+
settings.SITE.calendar_start_hour)
|
1178
1188
|
# kwargs['minValue'] = settings.SITE.calendar_start_hour
|
1179
1189
|
eh = settings.SITE.calendar_end_hour
|
1180
1190
|
if eh:
|
@@ -1243,7 +1253,8 @@ class DateFieldElement(FieldElement):
|
|
1243
1253
|
# ~ preferred_width = 8 # 20131022
|
1244
1254
|
preferred_width = 13
|
1245
1255
|
filter_type = "date"
|
1246
|
-
gridfilters_settings = dict(
|
1256
|
+
gridfilters_settings = dict(
|
1257
|
+
type="date", dateFormat=settings.SITE.date_format_extjs)
|
1247
1258
|
# todo: DateFieldElement.preferred_width should be computed from Report.date_format
|
1248
1259
|
# ~ grid_column_template = "new Ext.grid.DateColumn(%s)"
|
1249
1260
|
|
@@ -1376,7 +1387,8 @@ class DecimalFieldElement(NumberFieldElement):
|
|
1376
1387
|
|
1377
1388
|
def __init__(self, *args, **kw):
|
1378
1389
|
FieldElement.__init__(self, *args, **kw)
|
1379
|
-
self.preferred_width = max(
|
1390
|
+
self.preferred_width = max(
|
1391
|
+
5, self.field.max_digits) + self.field.decimal_places
|
1380
1392
|
# fmt = '0,000'
|
1381
1393
|
# fmt = '0.0'
|
1382
1394
|
# if self.field.decimal_places > 0:
|
@@ -1830,7 +1842,7 @@ class SlaveSummaryPanel(LightWeightContainer):
|
|
1830
1842
|
oui5_field_template = "openui5/elems/field/SlaveSummaryElement.xml"
|
1831
1843
|
|
1832
1844
|
def __init__(self, lh, slave, name, **kw):
|
1833
|
-
super().__init__(lh, slave, name, slave.
|
1845
|
+
super().__init__(lh, slave, name, slave.get_slave_summary, **kw)
|
1834
1846
|
|
1835
1847
|
|
1836
1848
|
class StoryElement(LightWeightContainer):
|
@@ -2442,9 +2454,11 @@ class SlaveContainer(GridElement):
|
|
2442
2454
|
if constants.DISPLAY_MODE_LIST in rpt.extra_display_modes:
|
2443
2455
|
slaves["list"] = get_list_element(layout_handle, rpt, name, **kw)
|
2444
2456
|
if constants.DISPLAY_MODE_SUMMARY in rpt.extra_display_modes:
|
2445
|
-
slaves["summary"] = get_summary_element(
|
2457
|
+
slaves["summary"] = get_summary_element(
|
2458
|
+
layout_handle, rpt, name, **kw)
|
2446
2459
|
if constants.DISPLAY_MODE_HTML in rpt.extra_display_modes:
|
2447
|
-
slaves["html"] = get_htmlbox_element(
|
2460
|
+
slaves["html"] = get_htmlbox_element(
|
2461
|
+
layout_handle, rpt, name, **kw)
|
2448
2462
|
if slaves:
|
2449
2463
|
self.slaves = slaves
|
2450
2464
|
super().__init__(layout_handle, name, rpt, *columns, **kw)
|
@@ -2765,7 +2779,8 @@ def create_layout_element(lh, name, **kw):
|
|
2765
2779
|
# ctx = (lh.layout.__class__, name, ', '.join(dir(lh.layout)))
|
2766
2780
|
# raise Exception(
|
2767
2781
|
# "Instance of %s has no data element '%s' (names are %s)" % ctx)
|
2768
|
-
raise Exception(
|
2782
|
+
raise Exception(
|
2783
|
+
"Invalid data element '{1}' in {0}".format(lh.layout, name))
|
2769
2784
|
|
2770
2785
|
if isinstance(de, type) and issubclass(de, fields.Dummy):
|
2771
2786
|
return None
|
@@ -2864,7 +2879,8 @@ def create_layout_element(lh, name, **kw):
|
|
2864
2879
|
title=_("Show this table in own window"),
|
2865
2880
|
style="text-decoration:none;",
|
2866
2881
|
)
|
2867
|
-
kw.update(label="{} {}".format(
|
2882
|
+
kw.update(label="{} {}".format(
|
2883
|
+
de.get_label(), tostring(btn)))
|
2868
2884
|
|
2869
2885
|
if (
|
2870
2886
|
len(de.default_display_modes) > 1
|
lino/core/fields.py
CHANGED
@@ -70,7 +70,7 @@ def set_default_verbose_name(f):
|
|
70
70
|
|
71
71
|
"""
|
72
72
|
if f.verbose_name is None or (
|
73
|
-
|
73
|
+
f.name is not None and f.verbose_name == f.name.replace("_", " ")):
|
74
74
|
f.verbose_name = f.remote_field.model._meta.verbose_name
|
75
75
|
|
76
76
|
|
@@ -322,7 +322,8 @@ class FakeField(object):
|
|
322
322
|
|
323
323
|
def __repr__(self):
|
324
324
|
# copied from django Field
|
325
|
-
path = "%s.%s" % (self.__class__.__module__,
|
325
|
+
path = "%s.%s" % (self.__class__.__module__,
|
326
|
+
self.__class__.__qualname__)
|
326
327
|
name = getattr(self, "name", None)
|
327
328
|
if name is not None:
|
328
329
|
return "<%s: %s>" % (path, name)
|
@@ -604,7 +605,8 @@ class VirtualField(FakeField):
|
|
604
605
|
f = self.return_type = resolve_field(f)
|
605
606
|
except Exception as e:
|
606
607
|
raise Exception(
|
607
|
-
"Invalid return type spec {} for {} : {}".format(
|
608
|
+
"Invalid return type spec {} for {} : {}".format(
|
609
|
+
f, self, e)
|
608
610
|
)
|
609
611
|
|
610
612
|
if isinstance(f, FakeField):
|
@@ -947,7 +949,8 @@ class QuantityField(models.CharField):
|
|
947
949
|
kw.setdefault("max_length", settings.SITE.quantity_max_length)
|
948
950
|
super().__init__(*args, **kw)
|
949
951
|
if self.blank and not self.null:
|
950
|
-
raise ChangedAPI(
|
952
|
+
raise ChangedAPI(
|
953
|
+
"When `blank` is True, `null` must be True as well.")
|
951
954
|
|
952
955
|
# ~ def get_internal_type(self):
|
953
956
|
# ~ return "CharField"
|
@@ -1207,7 +1210,8 @@ class ImportedFields(object):
|
|
1207
1210
|
|
1208
1211
|
@classmethod
|
1209
1212
|
def declare_imported_fields(cls, names):
|
1210
|
-
cls._imported_fields = cls._imported_fields | set(
|
1213
|
+
cls._imported_fields = cls._imported_fields | set(
|
1214
|
+
fields_list(cls, names))
|
1211
1215
|
# ~ logger.info('20120801 %s.declare_imported_fields() --> %s' % (
|
1212
1216
|
# ~ cls,cls._imported_fields))
|
1213
1217
|
|
@@ -1299,14 +1303,19 @@ class TableRow(object):
|
|
1299
1303
|
# return v
|
1300
1304
|
# raise Exception("Oops, {} on {} is {}".format(name, cls, v))
|
1301
1305
|
|
1306
|
+
@classmethod
|
1307
|
+
def override_column_headers(cls, ar, **headers):
|
1308
|
+
return headers
|
1309
|
+
|
1302
1310
|
@classmethod
|
1303
1311
|
def disable_create(self, ar):
|
1304
1312
|
return None
|
1305
1313
|
|
1306
1314
|
def get_detail_action(self, ar):
|
1307
|
-
"""
|
1308
|
-
|
1309
|
-
|
1315
|
+
"""
|
1316
|
+
Return the (bound) detail action to use for showing this database row in
|
1317
|
+
a detail window. Return `None` when no detail window exists or the
|
1318
|
+
requesting user has no permission to see it.
|
1310
1319
|
|
1311
1320
|
`ar` is the action request that asks to see the detail.
|
1312
1321
|
If the action request's actor can be used for this model,
|
@@ -1511,7 +1520,8 @@ def fields_list(model, field_names):
|
|
1511
1520
|
else:
|
1512
1521
|
e = model.get_data_elem(name)
|
1513
1522
|
if e is None:
|
1514
|
-
raise FieldDoesNotExist(
|
1523
|
+
raise FieldDoesNotExist(
|
1524
|
+
"No data element %r in %s" % (name, model))
|
1515
1525
|
if not hasattr(e, "name"):
|
1516
1526
|
raise FieldDoesNotExist(
|
1517
1527
|
"%s %r in %s has no name" % (e.__class__, name, model)
|
lino/core/kernel.py
CHANGED
@@ -4,8 +4,7 @@
|
|
4
4
|
"""This defines the :class:`Kernel` class.
|
5
5
|
|
6
6
|
The "kernel" of a Lino site is (like `SITE` itself) a "de facto
|
7
|
-
singleton", available to application code as ``SITE.kernel``
|
8
|
-
alias for backwards compatibility: ``SITE.ui``).
|
7
|
+
singleton", available to application code as ``SITE.kernel``).
|
9
8
|
|
10
9
|
The kernel is instantiated at the end of the startup process, when the
|
11
10
|
:setting:`SITE` has been instantiated and models have been loaded. It
|
@@ -22,7 +21,6 @@ application.
|
|
22
21
|
|
23
22
|
import os
|
24
23
|
import sys
|
25
|
-
import time
|
26
24
|
import codecs
|
27
25
|
import atexit
|
28
26
|
import signal
|
@@ -30,11 +28,10 @@ import threading
|
|
30
28
|
from importlib import import_module
|
31
29
|
import json
|
32
30
|
|
33
|
-
from django.apps import AppConfig
|
31
|
+
# from django.apps import AppConfig
|
34
32
|
from django.apps import apps
|
35
33
|
from django.conf import settings
|
36
34
|
from django.http import HttpResponse
|
37
|
-
from django.utils.encoding import force_str
|
38
35
|
from django.utils.text import format_lazy
|
39
36
|
from django.utils.translation import gettext_lazy as _
|
40
37
|
from django.core.exceptions import PermissionDenied, ValidationError
|
@@ -454,7 +451,8 @@ class Kernel(object):
|
|
454
451
|
try:
|
455
452
|
a.setup_columns()
|
456
453
|
except DatabaseError:
|
457
|
-
logger.debug(
|
454
|
+
logger.debug(
|
455
|
+
"Ignoring DatabaseError in %s.setup_columns", a)
|
458
456
|
if (
|
459
457
|
issubclass(a, dbtables.Table)
|
460
458
|
and a.model is not None
|
@@ -533,7 +531,8 @@ class Kernel(object):
|
|
533
531
|
# for p in site.installed_plugins:
|
534
532
|
# p.after_discover()
|
535
533
|
|
536
|
-
self.reserved_names = [getattr(constants, n)
|
534
|
+
self.reserved_names = [getattr(constants, n)
|
535
|
+
for n in constants.URL_PARAMS]
|
537
536
|
|
538
537
|
names = set()
|
539
538
|
for n in self.reserved_names:
|
@@ -565,7 +564,8 @@ class Kernel(object):
|
|
565
564
|
found = True
|
566
565
|
break
|
567
566
|
if not found:
|
568
|
-
raise Exception(
|
567
|
+
raise Exception(
|
568
|
+
f"Invalid modname {modname} in Site.web_front_ends")
|
569
569
|
self.web_front_ends = lst
|
570
570
|
# print("20230407 web front ends:")
|
571
571
|
# print("\n".join(["- {0.url_prefix} --> {0.app_name} {1}".format(p, hash(p))
|
@@ -623,7 +623,8 @@ class Kernel(object):
|
|
623
623
|
|
624
624
|
return get_welcome_messages
|
625
625
|
|
626
|
-
site.add_welcome_handler(
|
626
|
+
site.add_welcome_handler(
|
627
|
+
handler(a), a, "welcome_message_when_count")
|
627
628
|
|
628
629
|
# Remove "meaningless" user roles, i.e. those which are either used by all
|
629
630
|
# types or by no type. This seems to be
|
@@ -712,7 +713,8 @@ class Kernel(object):
|
|
712
713
|
msg = (
|
713
714
|
"{0}.{1} has on_delete SET_NULL but " "is not nullable "
|
714
715
|
)
|
715
|
-
msg = msg.format(
|
716
|
+
msg = msg.format(
|
717
|
+
fmn(m), fk.name, fk.remote_field.model)
|
716
718
|
raise Exception(msg)
|
717
719
|
|
718
720
|
else:
|
@@ -785,7 +787,8 @@ class Kernel(object):
|
|
785
787
|
pointed_model.objects.get(pk=fk)
|
786
788
|
except pointed_model.DoesNotExist:
|
787
789
|
msg = "Invalid primary key {1} for {2} in `{0}`"
|
788
|
-
obj._message = msg.format(
|
790
|
+
obj._message = msg.format(
|
791
|
+
gfk.fk_field, fk, fmn(pointed_model))
|
789
792
|
if gfk.name in model.allow_cascaded_delete:
|
790
793
|
obj._todo = "delete"
|
791
794
|
elif fk_field.null:
|
@@ -807,7 +810,8 @@ class Kernel(object):
|
|
807
810
|
"""
|
808
811
|
if not ar.get_permission():
|
809
812
|
if False:
|
810
|
-
msg = "{} has no permission to run {}".format(
|
813
|
+
msg = "{} has no permission to run {}".format(
|
814
|
+
ar.get_user(), ar)
|
811
815
|
else:
|
812
816
|
msg = "No permission to run {}".format(ar)
|
813
817
|
# raise Exception(msg)
|
@@ -842,7 +846,8 @@ class Kernel(object):
|
|
842
846
|
and ar.master_instance is not None
|
843
847
|
and len(ar.selected_rows)
|
844
848
|
):
|
845
|
-
ar.set_response(
|
849
|
+
ar.set_response(
|
850
|
+
master_data=ar.selected_rows[0].get_master_data(ar))
|
846
851
|
if (
|
847
852
|
a.parameters
|
848
853
|
and not a.no_params_window
|
@@ -852,15 +857,16 @@ class Kernel(object):
|
|
852
857
|
except Warning as e:
|
853
858
|
ar.error(e, alert=True)
|
854
859
|
except Exception as e:
|
855
|
-
# except (ValidationError, IntegrityError) as e:
|
856
|
-
msg = ar.ah.actor.error2str(e)
|
857
|
-
logger.info("Error during kernel.run_action() in %s: %s", ar, msg)
|
858
|
-
if is_devserver():
|
859
|
-
# can be interesting during development but disturbs on a
|
860
|
-
# production server
|
861
|
-
logger.exception(e)
|
862
860
|
# print(f"20240911 oops {repr(e)}")
|
861
|
+
msg = ar.ah.actor.error2str(e)
|
863
862
|
ar.error(msg, alert=True)
|
863
|
+
if not isinstance(e, (ValidationError, IntegrityError)):
|
864
|
+
logger.info(
|
865
|
+
"Error during kernel.run_action() in %s: %s", ar, msg)
|
866
|
+
if is_devserver():
|
867
|
+
# can be interesting during development but disturbs on a
|
868
|
+
# production server
|
869
|
+
logger.exception(e)
|
864
870
|
|
865
871
|
return ar.renderer.render_action_response(ar)
|
866
872
|
|
lino/core/model.py
CHANGED
@@ -180,7 +180,8 @@ class Model(models.Model, fields.TableRow):
|
|
180
180
|
and b is not models.Model
|
181
181
|
and not b._meta.abstract
|
182
182
|
):
|
183
|
-
msg = b._lino_ddh.disable_delete_on_object(
|
183
|
+
msg = b._lino_ddh.disable_delete_on_object(
|
184
|
+
self, [self.__class__])
|
184
185
|
if msg is not None:
|
185
186
|
return msg
|
186
187
|
return self.__class__._lino_ddh.disable_delete_on_object(self)
|
@@ -243,7 +244,8 @@ class Model(models.Model, fields.TableRow):
|
|
243
244
|
@classmethod
|
244
245
|
def add_active_field(cls, names):
|
245
246
|
if isinstance(cls.active_fields, str):
|
246
|
-
cls.active_fields = frozenset(
|
247
|
+
cls.active_fields = frozenset(
|
248
|
+
fields.fields_list(cls, cls.active_fields))
|
247
249
|
cls.active_fields = cls.active_fields | fields.fields_list(cls, names)
|
248
250
|
|
249
251
|
@classmethod
|
@@ -274,9 +276,11 @@ class Model(models.Model, fields.TableRow):
|
|
274
276
|
@classmethod
|
275
277
|
def on_analyze(cls, site):
|
276
278
|
if isinstance(cls.workflow_owner_field, str):
|
277
|
-
cls.workflow_owner_field = cls.get_data_elem(
|
279
|
+
cls.workflow_owner_field = cls.get_data_elem(
|
280
|
+
cls.workflow_owner_field)
|
278
281
|
if isinstance(cls.workflow_state_field, str):
|
279
|
-
cls.workflow_state_field = cls.get_data_elem(
|
282
|
+
cls.workflow_state_field = cls.get_data_elem(
|
283
|
+
cls.workflow_state_field)
|
280
284
|
# for vf in cls._meta.private_fields:
|
281
285
|
# if vf.name == 'detail_link':
|
282
286
|
# if vf.verbose_name is None:
|
@@ -300,7 +304,8 @@ class Model(models.Model, fields.TableRow):
|
|
300
304
|
bleached_fields.append(f)
|
301
305
|
cls._bleached_fields = tuple(bleached_fields)
|
302
306
|
if hasattr(cls, "bleached_fields"):
|
303
|
-
raise ChangedAPI(
|
307
|
+
raise ChangedAPI(
|
308
|
+
"Replace bleached_fields by bleached=True on each field")
|
304
309
|
|
305
310
|
@classmethod
|
306
311
|
def create_from_choice(cls, text, ar=None, save=True):
|
@@ -344,7 +349,8 @@ class Model(models.Model, fields.TableRow):
|
|
344
349
|
if isinstance(lookup_field, str):
|
345
350
|
lookup_field = model._meta.get_field(lookup_field)
|
346
351
|
if isinstance(lookup_field, BabelCharField):
|
347
|
-
flt = settings.SITE.lookup_filter(
|
352
|
+
flt = settings.SITE.lookup_filter(
|
353
|
+
lookup_field.name, value, **known_values)
|
348
354
|
else:
|
349
355
|
if isinstance(lookup_field, models.CharField):
|
350
356
|
fkw[lookup_field.name + "__iexact"] = value
|
@@ -369,7 +375,8 @@ class Model(models.Model, fields.TableRow):
|
|
369
375
|
try:
|
370
376
|
obj.full_clean()
|
371
377
|
except ValidationError as e:
|
372
|
-
raise ValidationError(
|
378
|
+
raise ValidationError(
|
379
|
+
"Failed to auto_create %s : %s" % (obj2str(obj), e))
|
373
380
|
obj.save()
|
374
381
|
signals.auto_create.send(obj, known_values=known_values)
|
375
382
|
return obj
|
@@ -410,15 +417,15 @@ class Model(models.Model, fields.TableRow):
|
|
410
417
|
pass
|
411
418
|
|
412
419
|
def before_ui_save(self, ar, cw):
|
413
|
-
for f, old, new in self.fields_to_bleach():
|
420
|
+
for f, old, new in self.fields_to_bleach(save=True, ar=ar):
|
414
421
|
setattr(self, f.name, new)
|
415
422
|
|
416
|
-
def fields_to_bleach(self):
|
423
|
+
def fields_to_bleach(self, **kwargs):
|
417
424
|
for f in self._bleached_fields:
|
418
425
|
old = getattr(self, f.name)
|
419
426
|
if old is None:
|
420
427
|
continue
|
421
|
-
new = sanitize(old)
|
428
|
+
new = sanitize(old, **kwargs)
|
422
429
|
# try:
|
423
430
|
# new = bleach.clean(
|
424
431
|
# new,
|
@@ -628,7 +635,8 @@ class Model(models.Model, fields.TableRow):
|
|
628
635
|
# print("20210325", navinfo)
|
629
636
|
# raise Exception("20")
|
630
637
|
|
631
|
-
qs = sliced_data_iterator(
|
638
|
+
qs = sliced_data_iterator(
|
639
|
+
ar.data_iterator, navinfo["offset"], ar.limit)
|
632
640
|
for obj in qs:
|
633
641
|
if obj.pk == self.pk:
|
634
642
|
items.append(E.b(str(obj)))
|
@@ -710,7 +718,6 @@ class Model(models.Model, fields.TableRow):
|
|
710
718
|
# return self.preview(ar)
|
711
719
|
# #~ username = kw.pop('username',None)
|
712
720
|
|
713
|
-
|
714
721
|
@classmethod
|
715
722
|
def get_chooser_for_field(cls, fieldname):
|
716
723
|
d = getattr(cls, "_choosers_dict", {})
|
@@ -718,7 +725,7 @@ class Model(models.Model, fields.TableRow):
|
|
718
725
|
# print("20200425 Model.get_chooser_for_field", cls, fieldname, d)
|
719
726
|
return d.get(fieldname, None)
|
720
727
|
|
721
|
-
def
|
728
|
+
def get_printable_target_stem(self):
|
722
729
|
return self._meta.app_label + "." + self.__class__.__name__ + "-" + str(self.pk)
|
723
730
|
|
724
731
|
@classmethod
|
@@ -743,7 +750,8 @@ class Model(models.Model, fields.TableRow):
|
|
743
750
|
return set([fld.choicelist.get_by_name(x) for x in states.split()])
|
744
751
|
elif isinstance(states, set):
|
745
752
|
return states
|
746
|
-
raise Exception(
|
753
|
+
raise Exception(
|
754
|
+
"Cannot resolve stateset specifier {!r}".format(states))
|
747
755
|
|
748
756
|
@classmethod
|
749
757
|
def add_picker(model, fldname):
|
@@ -879,7 +887,8 @@ class Model(models.Model, fields.TableRow):
|
|
879
887
|
collect(self)
|
880
888
|
s = "\n".join(
|
881
889
|
[
|
882
|
-
' "%s" -> "%s"' % (p._meta.verbose_name,
|
890
|
+
' "%s" -> "%s"' % (p._meta.verbose_name,
|
891
|
+
c._meta.verbose_name)
|
883
892
|
for p, c in pairs
|
884
893
|
]
|
885
894
|
)
|
@@ -970,6 +979,7 @@ LINO_MODEL_ATTRIBS = (
|
|
970
979
|
"error2str",
|
971
980
|
"print_subclasses_graph",
|
972
981
|
"disable_create",
|
982
|
+
"override_column_headers",
|
973
983
|
"grid_post",
|
974
984
|
"submit_insert",
|
975
985
|
"delete_veto_message",
|
@@ -1021,7 +1031,8 @@ def pre_delete_handler(sender, instance=None, **kw):
|
|
1021
1031
|
for obj in qs:
|
1022
1032
|
setattr(obj, gfk.name, None)
|
1023
1033
|
elif qs.count():
|
1024
|
-
raise Warning(instance.delete_veto_message(
|
1034
|
+
raise Warning(instance.delete_veto_message(
|
1035
|
+
qs.model, qs.count()))
|
1025
1036
|
for qs in must_cascade:
|
1026
1037
|
if qs.count():
|
1027
1038
|
logger.info(
|
lino/core/renderer.py
CHANGED
@@ -284,7 +284,7 @@ class HtmlRenderer(Renderer):
|
|
284
284
|
raise Exception("Both nosummary and display_mode were specified")
|
285
285
|
|
286
286
|
if display_mode == constants.DISPLAY_MODE_SUMMARY:
|
287
|
-
yield ar.actor.get_table_summary(ar
|
287
|
+
yield ar.actor.get_table_summary(ar)
|
288
288
|
return
|
289
289
|
|
290
290
|
if header_level is not None:
|
@@ -834,7 +834,7 @@ class TextRenderer(HtmlRenderer):
|
|
834
834
|
# yield "20240506 {}".format(ar)
|
835
835
|
if display_mode == constants.DISPLAY_MODE_SUMMARY:
|
836
836
|
s = to_rst(
|
837
|
-
ar.actor.get_table_summary(ar
|
837
|
+
ar.actor.get_table_summary(ar),
|
838
838
|
stripped=stripped,
|
839
839
|
)
|
840
840
|
if stripped:
|