lino 25.6.1__py3-none-any.whl → 25.7.1__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 +1 -0
- lino/api/doctest.py +21 -0
- lino/core/actions.py +80 -25
- lino/core/actors.py +54 -27
- lino/core/boundaction.py +16 -0
- lino/core/choicelists.py +7 -7
- lino/core/constants.py +3 -0
- lino/core/dashboard.py +4 -2
- lino/core/dbtables.py +2 -2
- 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 +20 -9
- 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/management/commands/checkdata.py +3 -3
- 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 +4 -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/publisher/renderer.py +2 -5
- 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 +3 -2
- lino/utils/quantities.py +8 -0
- lino/utils/soup.py +75 -106
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/METADATA +1 -1
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/RECORD +93 -90
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/WHEEL +0 -0
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/COPYING +0 -0
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)
|
lino/core/fields.py
CHANGED
@@ -22,6 +22,8 @@ from django.db.models.fields import NOT_PROVIDED
|
|
22
22
|
from django.utils.functional import cached_property
|
23
23
|
|
24
24
|
from lino import logger
|
25
|
+
if settings.SITE.is_installed("contenttypes"):
|
26
|
+
from lino.modlib.gfks.fields import GenericForeignKey
|
25
27
|
from lino.utils.html import E, forcetext, tostring, SafeString, escape, mark_safe
|
26
28
|
|
27
29
|
from lino.core.utils import (
|
@@ -147,8 +149,9 @@ class MonthField(models.DateField):
|
|
147
149
|
A DateField that uses a MonthPicker instead of a normal DateWidget
|
148
150
|
"""
|
149
151
|
|
150
|
-
|
151
|
-
|
152
|
+
pass
|
153
|
+
# def __init__(self, *args, **kw):
|
154
|
+
# models.DateField.__init__(self, *args, **kw)
|
152
155
|
|
153
156
|
|
154
157
|
# def PriceField(*args, **kwargs):
|
@@ -164,7 +167,7 @@ class MonthField(models.DateField):
|
|
164
167
|
class PriceField(models.DecimalField):
|
165
168
|
"""
|
166
169
|
A thin wrapper around Django's `DecimalField
|
167
|
-
<https://docs.djangoproject.com/en/5.
|
170
|
+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#decimalfield>`_
|
168
171
|
with price-like default values for `decimal_places`, `max_length` and
|
169
172
|
`max_digits`.
|
170
173
|
"""
|
@@ -300,6 +303,7 @@ class FakeField(object):
|
|
300
303
|
max_length = None
|
301
304
|
generated = False
|
302
305
|
choicelist = None # avoid 'DummyField' object has no attribute 'choicelist'
|
306
|
+
hide_unless_explicit = True
|
303
307
|
|
304
308
|
wildcard_data_elem = False
|
305
309
|
"""Whether to consider this field as wildcard data element.
|
@@ -471,7 +475,7 @@ class DisplayField(FakeField):
|
|
471
475
|
|
472
476
|
def __init__(self, verbose_name=None, **kwargs):
|
473
477
|
self.verbose_name = verbose_name
|
474
|
-
super(
|
478
|
+
super().__init__(**kwargs)
|
475
479
|
|
476
480
|
# the following dummy methods are never called but needed when
|
477
481
|
# using a DisplayField as return_type of a VirtualField
|
@@ -982,7 +986,7 @@ class QuantityField(models.CharField):
|
|
982
986
|
def to_python(self, value):
|
983
987
|
"""
|
984
988
|
Excerpt from `Django docs
|
985
|
-
<https://docs.djangoproject.com/en/5.
|
989
|
+
<https://docs.djangoproject.com/en/5.2/howto/custom-model-fields/#converting-values-to-python-objects>`__:
|
986
990
|
|
987
991
|
As a general rule, :meth:`to_python` should deal gracefully with
|
988
992
|
any of the following arguments:
|
@@ -1433,7 +1437,7 @@ class TableRow(object):
|
|
1433
1437
|
return escape(str(self))
|
1434
1438
|
return self.as_paragraph(ar)
|
1435
1439
|
|
1436
|
-
@displayfield(_("Select multiple rows"))
|
1440
|
+
@displayfield(_("Select multiple rows"), wildcard_data_elem=True)
|
1437
1441
|
def rowselect(self, ar):
|
1438
1442
|
"""A place holder for primereact Datatable column "Selection Column\""""
|
1439
1443
|
return None
|
@@ -1468,8 +1472,9 @@ class TableRow(object):
|
|
1468
1472
|
return escape(str(self))
|
1469
1473
|
return tostring(self.as_summary_item(ar, **kwargs))
|
1470
1474
|
|
1471
|
-
def as_tile(self, ar, **kwargs):
|
1472
|
-
|
1475
|
+
def as_tile(self, ar, prev, **kwargs):
|
1476
|
+
s = self.as_paragraph(ar, **kwargs)
|
1477
|
+
return constants.TILE_TEMPLATE.format(chunk=s)
|
1473
1478
|
|
1474
1479
|
def as_story_item(self, ar, **kwargs):
|
1475
1480
|
kwargs.update(display_mode=constants.DISPLAY_MODE_STORY)
|
@@ -1535,9 +1540,13 @@ def wildcard_data_elems(model):
|
|
1535
1540
|
yield f
|
1536
1541
|
for f in meta.many_to_many:
|
1537
1542
|
yield f
|
1543
|
+
|
1544
|
+
# private_fields are available at meta.fields on which we iterate
|
1545
|
+
# over just above, but, some field.is_relation are filtered out.
|
1538
1546
|
for f in meta.private_fields:
|
1539
|
-
if
|
1540
|
-
|
1547
|
+
if settings.SITE.is_installed("contenttypes"):
|
1548
|
+
if isinstance(f, GenericForeignKey):
|
1549
|
+
yield f
|
1541
1550
|
# todo: for slave in self.report.slaves
|
1542
1551
|
|
1543
1552
|
|
@@ -1557,7 +1566,7 @@ def pointer_factory(cls, othermodel, *args, **kw):
|
|
1557
1566
|
case. This is useful when designing reusable models.
|
1558
1567
|
|
1559
1568
|
- Explicitly sets the default value for `on_delete
|
1560
|
-
<https://docs.djangoproject.com/en/5.
|
1569
|
+
<https://docs.djangoproject.com/en/5.2/ref/models/fields/#django.db.models.ForeignKey.on_delete>`__
|
1561
1570
|
to ``CASCADE`` (as required by Django 2).
|
1562
1571
|
|
1563
1572
|
"""
|
lino/core/kernel.py
CHANGED
@@ -88,6 +88,10 @@ from .utils import resolve_fields_list
|
|
88
88
|
from lino.core.fields import set_default_verbose_name
|
89
89
|
# from lino.core.requests import ActorRequest
|
90
90
|
|
91
|
+
from lino.utils import dbhash
|
92
|
+
from lino.core.signals import database_prepared
|
93
|
+
database_prepared.connect(dbhash.mark_virgin)
|
94
|
+
|
91
95
|
startup_rlock = threading.RLock() # Lock() or RLock()?
|
92
96
|
|
93
97
|
GFK_TARGETS = (models.AutoField, models.IntegerField)
|
@@ -683,6 +687,9 @@ class Kernel(object):
|
|
683
687
|
# virtual fields in LightWeightContainer
|
684
688
|
for res in actors.actors_list:
|
685
689
|
for ba in res.get_actions():
|
690
|
+
# ba._started = True
|
691
|
+
if ba.help_text is None:
|
692
|
+
ba.help_text = ba.action.get_help_text(ba)
|
686
693
|
if ba.action.params_layout is not None:
|
687
694
|
ba.action.params_layout.get_layout_handle()
|
688
695
|
if ba.action.is_window_action():
|
@@ -895,6 +902,7 @@ class Kernel(object):
|
|
895
902
|
except Warning as e:
|
896
903
|
ar.error(e, alert=True)
|
897
904
|
except Exception as e:
|
905
|
+
# raise # A plain traceback can be easier to debug.
|
898
906
|
# print(f"20240911 oops {repr(e)}")
|
899
907
|
msg = ar.ah.actor.error2str(e)
|
900
908
|
ar.error(msg, alert=True)
|
lino/core/layouts.py
CHANGED
@@ -9,6 +9,7 @@ import re
|
|
9
9
|
|
10
10
|
from django.conf import settings
|
11
11
|
from django.utils.translation import gettext_lazy as _
|
12
|
+
from django.utils.functional import Promise
|
12
13
|
from django.db.models.fields import NOT_PROVIDED
|
13
14
|
from django.db.models.fields.related import ForeignObject
|
14
15
|
# from django.contrib.contenttypes.fields import GenericRelation
|
@@ -118,7 +119,9 @@ class LayoutHandle:
|
|
118
119
|
# if 'label_align' in kwargs:
|
119
120
|
# print("20170921 desc2elem", elemname, desc, kwargs)
|
120
121
|
|
121
|
-
if
|
122
|
+
if isinstance(desc, Promise):
|
123
|
+
desc = str(desc)
|
124
|
+
elif not isinstance(desc, str):
|
122
125
|
raise Exception("{} is {} (must be a string)".format(elemname, desc))
|
123
126
|
|
124
127
|
# flatten continued lines:
|
@@ -171,7 +174,8 @@ class LayoutHandle:
|
|
171
174
|
if de.name not in explicit_specs:
|
172
175
|
if self.use_as_wildcard(de):
|
173
176
|
wildcard_names.append(de.name)
|
174
|
-
if len(explicit_specs) or
|
177
|
+
if len(explicit_specs) or (
|
178
|
+
isinstance(de, fields.FakeField) and de.hide_unless_explicit):
|
175
179
|
self.hidden_elements.add(de.name)
|
176
180
|
wildcard_str = self.layout.join_str.join(wildcard_names)
|
177
181
|
desc = desc.replace("*", wildcard_str)
|
lino/core/menus.py
CHANGED
@@ -179,15 +179,12 @@ def create_item(unused, spec, action=None, help_text=None, **kw):
|
|
179
179
|
if action_spec is not None:
|
180
180
|
kw.update(action=resolve_action(action_spec))
|
181
181
|
else:
|
182
|
-
|
182
|
+
ba = resolve_action(spec, action)
|
183
183
|
# if str(spec).startswith("webshop"):
|
184
184
|
# print("20210319", spec, action, a)
|
185
|
-
kw.update(action=
|
185
|
+
kw.update(action=ba)
|
186
186
|
if help_text is None:
|
187
|
-
|
188
|
-
help_text = a.actor.help_text or a.action.help_text
|
189
|
-
else:
|
190
|
-
help_text = a.action.help_text
|
187
|
+
ba.get_help_text()
|
191
188
|
if help_text is not None:
|
192
189
|
kw.update(help_text=help_text)
|
193
190
|
return MenuItem(**kw)
|
lino/core/model.py
CHANGED
@@ -7,9 +7,8 @@ See :doc:`/dev/models`, :doc:`/dev/delete`, :doc:`/dev/disable`,
|
|
7
7
|
:doc:`/dev/hide`, :doc:`/dev/format`
|
8
8
|
|
9
9
|
"""
|
10
|
-
# from bs4 import BeautifulSoup
|
11
|
-
from lino import logger
|
12
10
|
import copy
|
11
|
+
# from bs4 import BeautifulSoup
|
13
12
|
|
14
13
|
from django.db import models
|
15
14
|
from django.conf import settings
|
@@ -19,7 +18,8 @@ from django.utils.translation import gettext_lazy as _
|
|
19
18
|
from django.db.models.signals import pre_delete
|
20
19
|
from django.utils.text import format_lazy
|
21
20
|
|
22
|
-
from lino
|
21
|
+
from lino import logger
|
22
|
+
from lino.utils.html import E, tostring, join_elems
|
23
23
|
from lino.utils.soup import sanitize
|
24
24
|
|
25
25
|
from lino.core import fields
|
@@ -29,6 +29,7 @@ from lino.core import actions
|
|
29
29
|
from lino.core import inject
|
30
30
|
|
31
31
|
from lino.core.atomizer import make_remote_field
|
32
|
+
from lino.core.exceptions import ChangedAPI
|
32
33
|
from .fields import RichTextField, displayfield
|
33
34
|
from .utils import error2str
|
34
35
|
from .utils import obj2str
|
@@ -1008,7 +1009,7 @@ def pre_delete_handler(sender, instance=None, **kw):
|
|
1008
1009
|
|
1009
1010
|
It seems that Django deletes *generic related objects* only if
|
1010
1011
|
the object being deleted has a `GenericRelation
|
1011
|
-
<https://docs.djangoproject.com/en/5.
|
1012
|
+
<https://docs.djangoproject.com/en/5.2/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericRelation>`_
|
1012
1013
|
field (according to `Why won't my GenericForeignKey cascade
|
1013
1014
|
when deleting?
|
1014
1015
|
<https://stackoverflow.com/questions/6803018/why-wont-my-genericforeignkey-cascade-when-deleting>`_).
|
lino/core/renderer.py
CHANGED
@@ -19,6 +19,7 @@ from django.utils.translation import gettext as _
|
|
19
19
|
from django.utils.translation import get_language
|
20
20
|
|
21
21
|
from etgen.html2rst import RstTable
|
22
|
+
from etgen.utils import join_elems
|
22
23
|
# from lino import logger
|
23
24
|
from lino.utils import isiterable
|
24
25
|
from lino.utils.jsgen import py2js, js_code
|
@@ -441,7 +442,7 @@ class HtmlRenderer(Renderer):
|
|
441
442
|
url = self.js2url(self.action_call(ar, ba, status))
|
442
443
|
# ~ logger.info('20121002 window_action_button %s %r',a,unicode(label))
|
443
444
|
return self.href_button_action(
|
444
|
-
ba, url, str(label), title or ba.
|
445
|
+
ba, url, str(label), title or ba.get_help_text(), **kw
|
445
446
|
)
|
446
447
|
|
447
448
|
def quick_add_buttons(self, ar):
|
@@ -543,7 +544,7 @@ class HtmlRenderer(Renderer):
|
|
543
544
|
js = self.ar2js(ar, obj, **ar._status)
|
544
545
|
uri = self.js2url(js)
|
545
546
|
return self.href_button_action(
|
546
|
-
ba, uri, label, title or ba.
|
547
|
+
ba, uri, label, title or ba.get_help_text(), **kwargs
|
547
548
|
)
|
548
549
|
|
549
550
|
def menu_item_button(self, ar, mi, label=None, icon_name=None, **kwargs):
|
@@ -607,7 +608,7 @@ class HtmlRenderer(Renderer):
|
|
607
608
|
js = self.action_call_on_instance(obj, ar, ba, request_kwargs)
|
608
609
|
uri = self.js2url(js)
|
609
610
|
return self.href_button_action(
|
610
|
-
ba, uri, label, title or ba.
|
611
|
+
ba, uri, label, title or ba.get_help_text(), **button_attrs
|
611
612
|
)
|
612
613
|
|
613
614
|
def row_action_button_ar(
|
@@ -622,7 +623,7 @@ class HtmlRenderer(Renderer):
|
|
622
623
|
js = self.action_call_on_instance(obj, ar, ba)
|
623
624
|
uri = self.js2url(js)
|
624
625
|
return self.href_button_action(
|
625
|
-
ba, uri, label, title or ba.
|
626
|
+
ba, uri, label, title or ba.get_help_text(), **kw
|
626
627
|
)
|
627
628
|
|
628
629
|
def show_story(self, ar, story, stripped=True, **kwargs):
|
@@ -836,10 +837,11 @@ class TextRenderer(HtmlRenderer):
|
|
836
837
|
# print(f"20240929 {nosummary} {display_mode} {ar}")
|
837
838
|
# yield "20240506 {}".format(ar)
|
838
839
|
if display_mode == constants.DISPLAY_MODE_SUMMARY:
|
839
|
-
s = to_rst(
|
840
|
-
|
841
|
-
|
842
|
-
|
840
|
+
s = to_rst(tostring(E.span(*join_elems(ar.plain_toolbar_buttons()))),
|
841
|
+
stripped=stripped)
|
842
|
+
if s:
|
843
|
+
s += " | "
|
844
|
+
s += to_rst(ar.actor.get_table_summary(ar), stripped=stripped)
|
843
845
|
if stripped:
|
844
846
|
s = s.strip()
|
845
847
|
yield s
|
@@ -859,7 +861,7 @@ class TextRenderer(HtmlRenderer):
|
|
859
861
|
if cls.insert_action is not None and cls.editable:
|
860
862
|
ir = cls.insert_action.request_from(ar)
|
861
863
|
if ir.get_permission():
|
862
|
-
items.append("(+) {}".format(cls.insert_action.
|
864
|
+
items.append("(+) {}".format(cls.insert_action.get_help_text()))
|
863
865
|
# for i, obj in enumerate(ar.data_iterator):
|
864
866
|
# if i == cls.preview_limit:
|
865
867
|
# break
|
@@ -874,6 +876,15 @@ class TextRenderer(HtmlRenderer):
|
|
874
876
|
yield rstgen.ul(items).strip()
|
875
877
|
return
|
876
878
|
|
879
|
+
if display_mode == constants.DISPLAY_MODE_TILES:
|
880
|
+
prev = None
|
881
|
+
for obj in ar.sliced_data_iterator:
|
882
|
+
txt = obj.as_tile(ar, prev)
|
883
|
+
txt = html2text(txt)
|
884
|
+
yield txt.strip()
|
885
|
+
prev = obj
|
886
|
+
return
|
887
|
+
|
877
888
|
# At this point, display_mode is one of story, grid or html.
|
878
889
|
|
879
890
|
if header_level is not None:
|
lino/core/requests.py
CHANGED
@@ -1562,6 +1562,7 @@ class BaseRequest:
|
|
1562
1562
|
# assert iselement(btn)
|
1563
1563
|
buttons.append(btn)
|
1564
1564
|
# print("20181106", cls, self.bound_action, buttons)
|
1565
|
+
buttons.append(self.open_in_own_window_button()) # 20250713
|
1565
1566
|
return buttons
|
1566
1567
|
# if len(buttons) == 0:
|
1567
1568
|
# return None
|
@@ -1576,6 +1577,13 @@ class BaseRequest:
|
|
1576
1577
|
# raise Exception("20230331 {}".format(self.subst_user))
|
1577
1578
|
return self.renderer.ar2button(self, *args, **kwargs)
|
1578
1579
|
|
1580
|
+
def as_button(self, *args, **kw):
|
1581
|
+
"""Return a button which when activated executes (a copy of)
|
1582
|
+
this request.
|
1583
|
+
|
1584
|
+
"""
|
1585
|
+
return self.renderer.action_button(None, self, self.bound_action, *args, **kw)
|
1586
|
+
|
1579
1587
|
def instance_action_button(self, ia, *args, **kwargs):
|
1580
1588
|
"""Return an HTML element with a button that would run the given
|
1581
1589
|
:class:`InstanceAction <lino.core.requests.InstanceAction>`
|
@@ -1607,13 +1615,6 @@ class BaseRequest:
|
|
1607
1615
|
def get_main_card(self):
|
1608
1616
|
return self.actor.get_main_card(self)
|
1609
1617
|
|
1610
|
-
def as_button(self, *args, **kw):
|
1611
|
-
"""Return a button which when activated executes (a copy of)
|
1612
|
-
this request.
|
1613
|
-
|
1614
|
-
"""
|
1615
|
-
return self.renderer.action_button(None, self, self.bound_action, *args, **kw)
|
1616
|
-
|
1617
1618
|
def elem2rec1(ar, rh, elem, fields=None, **rec):
|
1618
1619
|
rec.update(data=rh.store.row2dict(ar, elem, fields))
|
1619
1620
|
return rec
|
lino/core/signals.py
CHANGED
lino/core/site.py
CHANGED
@@ -3,9 +3,6 @@
|
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
# doctest lino/core/site.py
|
5
5
|
|
6
|
-
from importlib.util import find_spec
|
7
|
-
from importlib import import_module
|
8
|
-
from pathlib import Path
|
9
6
|
import os
|
10
7
|
import re
|
11
8
|
import sys
|
@@ -20,6 +17,10 @@ import logging
|
|
20
17
|
from logging.handlers import SocketHandler
|
21
18
|
import time
|
22
19
|
import rstgen
|
20
|
+
from importlib.util import find_spec
|
21
|
+
from importlib import import_module
|
22
|
+
from pathlib import Path
|
23
|
+
from asgiref.sync import sync_to_async
|
23
24
|
from rstgen.confparser import ConfigParser
|
24
25
|
from django.apps import apps
|
25
26
|
from django.utils import timezone
|
@@ -264,6 +265,7 @@ class Site(object):
|
|
264
265
|
never_build_site_cache = False
|
265
266
|
keep_erroneous_cache_files = False
|
266
267
|
use_java = True
|
268
|
+
use_systemd = False
|
267
269
|
use_silk_icons = False
|
268
270
|
use_new_unicode_symbols = False
|
269
271
|
use_experimental_features = False
|
@@ -367,8 +369,8 @@ class Site(object):
|
|
367
369
|
|
368
370
|
csv_params = dict()
|
369
371
|
|
370
|
-
# attributes documented in book/docs/
|
371
|
-
_history_aware_logging =
|
372
|
+
# attributes documented in book/docs/topics/logging.rst:
|
373
|
+
_history_aware_logging = False
|
372
374
|
log_each_action_request = False
|
373
375
|
default_loglevel = "INFO"
|
374
376
|
logger_filename = "lino.log"
|
@@ -667,15 +669,19 @@ class Site(object):
|
|
667
669
|
"encoding": "UTF-8",
|
668
670
|
"formatter": "verbose",
|
669
671
|
}
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
672
|
+
elif self.use_systemd:
|
673
|
+
handlers["file"] = {
|
674
|
+
"class": "systemd.journal.JournalHandler",
|
675
|
+
"SYSLOG_IDENTIFIER": str(self.project_name),
|
676
|
+
}
|
677
|
+
# try:
|
678
|
+
# from systemd.journal import JournalHandler
|
679
|
+
# handlers["file"] = {
|
680
|
+
# "class": "systemd.journal.JournalHandler",
|
681
|
+
# "SYSLOG_IDENTIFIER": str(self.project_name),
|
682
|
+
# }
|
683
|
+
# except ImportError:
|
684
|
+
# pass
|
679
685
|
|
680
686
|
# when a file handler exists, we have the loggers use it even if this
|
681
687
|
# instance didn't create it:
|
@@ -952,6 +958,8 @@ class Site(object):
|
|
952
958
|
reqs.add(r)
|
953
959
|
if self.textfield_bleached:
|
954
960
|
reqs.add("beautifulsoup4")
|
961
|
+
if self.use_systemd:
|
962
|
+
reqs.add("systemd-python")
|
955
963
|
return sorted(reqs)
|
956
964
|
|
957
965
|
def setup_plugins(self):
|
@@ -1377,6 +1385,9 @@ class Site(object):
|
|
1377
1385
|
base = timezone.now().date()
|
1378
1386
|
return date_offset(base, *args, **kwargs)
|
1379
1387
|
|
1388
|
+
async def atoday(self, *args, **kwargs):
|
1389
|
+
return await sync_to_async(self.today)(*args, **kwargs)
|
1390
|
+
|
1380
1391
|
def now(self, *args, **kwargs):
|
1381
1392
|
t = self.today(*args, **kwargs)
|
1382
1393
|
now = timezone.now()
|
@@ -1587,7 +1598,7 @@ class Site(object):
|
|
1587
1598
|
for info in tuple(new_languages):
|
1588
1599
|
if "-" in info.django_code:
|
1589
1600
|
base, loc = info.django_code.split("-")
|
1590
|
-
if not
|
1601
|
+
if base not in self.language_dict:
|
1591
1602
|
self.language_dict[base] = info
|
1592
1603
|
|
1593
1604
|
# replace the complicated info by a simplified one
|
@@ -1825,11 +1836,9 @@ class Site(object):
|
|
1825
1836
|
# ~ return obj.id is not None and (obj.id > 10 and obj.id < 21)
|
1826
1837
|
|
1827
1838
|
def site_header(self):
|
1828
|
-
if self.is_installed("contacts")
|
1829
|
-
if self.
|
1830
|
-
return
|
1831
|
-
# ~ s = unicode(self.site_config.site_company) + " / " + s
|
1832
|
-
# ~ return ''
|
1839
|
+
if self.is_installed("contacts"):
|
1840
|
+
if (owner := self.plugins.contacts.site_owner) is not None:
|
1841
|
+
return owner.get_address("<br/>")
|
1833
1842
|
|
1834
1843
|
# def setup_main_menu(self):
|
1835
1844
|
# """
|
@@ -1843,11 +1852,22 @@ class Site(object):
|
|
1843
1852
|
for i in p.get_dashboard_items(user):
|
1844
1853
|
yield i
|
1845
1854
|
|
1855
|
+
@property
|
1856
|
+
def copyright_name(self):
|
1857
|
+
"""Name of copyright holder of the site's content."""
|
1858
|
+
if (owner := self.get_plugin_setting('contacts', 'site_owner')) is not None:
|
1859
|
+
# print("20230423", self.site_company)
|
1860
|
+
return str(owner)
|
1861
|
+
|
1862
|
+
@property
|
1863
|
+
def copyright_url(self):
|
1864
|
+
if (owner := self.get_plugin_setting('contacts', 'site_owner')) is not None:
|
1865
|
+
return owner.url
|
1866
|
+
|
1846
1867
|
@property
|
1847
1868
|
def site_config(self):
|
1848
1869
|
if "system" not in self.models:
|
1849
1870
|
return None
|
1850
|
-
|
1851
1871
|
return self.models.system.SiteConfig.get_site_config()
|
1852
1872
|
|
1853
1873
|
def get_config_value(self, name, default=None):
|
@@ -2081,6 +2101,9 @@ class Site(object):
|
|
2081
2101
|
def add_top_link_generator(self, func):
|
2082
2102
|
self._top_link_generator.append(func)
|
2083
2103
|
|
2104
|
+
def get_footer_html(self, ar):
|
2105
|
+
return mark_safe("<p>This is a new feature.</p>")
|
2106
|
+
|
2084
2107
|
def get_welcome_messages(self, ar):
|
2085
2108
|
for h in self._welcome_handlers:
|
2086
2109
|
for msg in h(ar):
|
@@ -2138,11 +2161,6 @@ class Site(object):
|
|
2138
2161
|
|
2139
2162
|
# yield "lino.modlib.lino_startup"
|
2140
2163
|
|
2141
|
-
copyright_name = None
|
2142
|
-
"""Name of copyright holder of the site's content."""
|
2143
|
-
|
2144
|
-
copyright_url = None
|
2145
|
-
|
2146
2164
|
server_url = "http://127.0.0.1:8000"
|
2147
2165
|
"""The "official" URL used by "normal" users when accessing this Lino
|
2148
2166
|
site.
|
@@ -2151,7 +2169,7 @@ class Site(object):
|
|
2151
2169
|
:mod:`lino.modlib.notify` to send notification emails)
|
2152
2170
|
|
2153
2171
|
Django has a `HttpRequest.build_absolute_uri()
|
2154
|
-
<https://docs.djangoproject.com/en/5.
|
2172
|
+
<https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest.build_absolute_uri>`__
|
2155
2173
|
method, but e.g. notification emails are sent via :manage:`linod` where no
|
2156
2174
|
HttpRequest exists. That's why we need to manually set :attr:`server_url`.
|
2157
2175
|
|
@@ -2236,7 +2254,9 @@ class Site(object):
|
|
2236
2254
|
yield E.span(*p)
|
2237
2255
|
|
2238
2256
|
def get_letter_date_text(self, today=None):
|
2239
|
-
|
2257
|
+
if self.is_installed("contacts"):
|
2258
|
+
if (sc := self.plugins.contacts.site_owner) is None:
|
2259
|
+
return
|
2240
2260
|
if today is None:
|
2241
2261
|
today = self.today()
|
2242
2262
|
from lino.utils.format_date import fdl
|
lino/core/store.py
CHANGED
@@ -689,7 +689,7 @@ class DisableEditingStoreField(SpecialStoreField):
|
|
689
689
|
|
690
690
|
class BooleanStoreField(StoreField):
|
691
691
|
"""A :class:`StoreField` for
|
692
|
-
`BooleanField <https://docs.djangoproject.com/en/5.
|
692
|
+
`BooleanField <https://docs.djangoproject.com/en/5.2/ref/models/fields/#booleanfield>`__.
|
693
693
|
|
694
694
|
"""
|
695
695
|
|
@@ -797,7 +797,7 @@ class IntegerStoreField(StoreField):
|
|
797
797
|
|
798
798
|
class AutoStoreField(StoreField):
|
799
799
|
"""A :class:`StoreField` for
|
800
|
-
`AutoField <https://docs.djangoproject.com/en/5.
|
800
|
+
`AutoField <https://docs.djangoproject.com/en/5.2/ref/models/fields/#autofield>`__
|
801
801
|
|
802
802
|
"""
|
803
803
|
|
@@ -1252,6 +1252,8 @@ class Store(BaseStore):
|
|
1252
1252
|
if isinstance(row, PhantomRow):
|
1253
1253
|
for fld in self.grid_fields:
|
1254
1254
|
fld.value2list(ar, None, l, row)
|
1255
|
+
# instead of calling ar.scrap_row_meta, add independently the meta item
|
1256
|
+
l.append({'meta': True, 'phantom': True})
|
1255
1257
|
else:
|
1256
1258
|
for fld in self.grid_fields:
|
1257
1259
|
if fld.delayed_value:
|