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.
Files changed (93) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +1 -0
  3. lino/api/doctest.py +21 -0
  4. lino/core/actions.py +80 -25
  5. lino/core/actors.py +54 -27
  6. lino/core/boundaction.py +16 -0
  7. lino/core/choicelists.py +7 -7
  8. lino/core/constants.py +3 -0
  9. lino/core/dashboard.py +4 -2
  10. lino/core/dbtables.py +2 -2
  11. lino/core/elems.py +38 -13
  12. lino/core/fields.py +20 -11
  13. lino/core/kernel.py +8 -0
  14. lino/core/layouts.py +6 -2
  15. lino/core/menus.py +3 -6
  16. lino/core/model.py +5 -4
  17. lino/core/renderer.py +20 -9
  18. lino/core/requests.py +8 -7
  19. lino/core/signals.py +1 -0
  20. lino/core/site.py +48 -28
  21. lino/core/store.py +4 -2
  22. lino/core/tables.py +23 -10
  23. lino/core/utils.py +4 -1
  24. lino/core/workflows.py +2 -1
  25. lino/help_texts.py +1 -2
  26. lino/management/commands/prep.py +2 -2
  27. lino/management/commands/show.py +8 -10
  28. lino/mixins/__init__.py +14 -13
  29. lino/mixins/periods.py +2 -0
  30. lino/mixins/sequenced.py +1 -1
  31. lino/modlib/about/models.py +4 -3
  32. lino/modlib/checkdata/__init__.py +42 -36
  33. lino/modlib/checkdata/choicelists.py +9 -1
  34. lino/modlib/checkdata/fixtures/checkdata.py +4 -2
  35. lino/modlib/checkdata/management/commands/checkdata.py +3 -3
  36. lino/modlib/checkdata/models.py +9 -2
  37. lino/modlib/comments/models.py +4 -3
  38. lino/modlib/extjs/ext_renderer.py +4 -4
  39. lino/modlib/extjs/views.py +8 -2
  40. lino/modlib/gfks/fields.py +1 -1
  41. lino/modlib/help/__init__.py +3 -3
  42. lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
  43. lino/modlib/help/fixtures/demo2.py +6 -1
  44. lino/modlib/help/management/commands/makehelp.py +4 -1
  45. lino/modlib/help/models.py +4 -1
  46. lino/modlib/help/utils.py +12 -6
  47. lino/modlib/linod/choicelists.py +57 -4
  48. lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
  49. lino/modlib/linod/management/commands/linod.py +0 -13
  50. lino/modlib/linod/mixins.py +8 -0
  51. lino/modlib/linod/models.py +29 -30
  52. lino/modlib/memo/__init__.py +7 -7
  53. lino/modlib/memo/management/__init__,py +0 -0
  54. lino/modlib/memo/management/commands/__init__.py +0 -0
  55. lino/modlib/memo/management/commands/removeurls.py +67 -0
  56. lino/modlib/memo/mixins.py +1 -9
  57. lino/modlib/memo/parser.py +1 -1
  58. lino/modlib/notify/config/notify/summary.eml +5 -2
  59. lino/modlib/notify/fixtures/demo2.py +5 -6
  60. lino/modlib/notify/models.py +9 -10
  61. lino/modlib/periods/__init__.py +11 -8
  62. lino/modlib/periods/choicelists.py +16 -10
  63. lino/modlib/periods/models.py +45 -45
  64. lino/modlib/publisher/renderer.py +2 -5
  65. lino/modlib/summaries/fixtures/checksummaries.py +4 -2
  66. lino/modlib/system/models.py +17 -18
  67. lino/modlib/uploads/fixtures/demo.py +9 -3
  68. lino/modlib/uploads/mixins.py +5 -2
  69. lino/modlib/uploads/models.py +15 -9
  70. lino/modlib/uploads/utils.py +4 -1
  71. lino/modlib/users/__init__.py +59 -18
  72. lino/modlib/users/actions.py +24 -20
  73. lino/modlib/users/fixtures/demo_users.py +2 -35
  74. lino/modlib/users/mixins.py +3 -4
  75. lino/modlib/users/models.py +53 -13
  76. lino/modlib/users/ui.py +30 -16
  77. lino/modlib/users/utils.py +5 -6
  78. lino/projects/std/settings.py +1 -1
  79. lino/sphinxcontrib/logo/templates/footer.html +1 -0
  80. lino/utils/ajax.py +1 -1
  81. lino/utils/cycler.py +5 -0
  82. lino/utils/dbhash.py +4 -9
  83. lino/utils/dpy.py +2 -2
  84. lino/utils/format_date.py +4 -3
  85. lino/utils/html.py +13 -5
  86. lino/utils/jsgen.py +3 -2
  87. lino/utils/quantities.py +8 -0
  88. lino/utils/soup.py +75 -106
  89. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/METADATA +1 -1
  90. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/RECORD +93 -90
  91. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/WHEEL +0 -0
  92. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/AUTHORS.rst +0 -0
  93. {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-2024 Rumma & Ko Ltd
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
- slaves["html"] = get_htmlbox_element(
2462
- layout_handle, rpt, name, **kw)
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
- return get_htmlbox_element(lh, de, name, **kw)
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
- def __init__(self, *args, **kw):
151
- models.DateField.__init__(self, *args, **kw)
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.0/ref/models/fields/#decimalfield>`_
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(DisplayField, self).__init__(**kwargs)
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.0/howto/custom-model-fields/#converting-values-to-python-objects>`__:
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
- return self.as_paragraph(ar, **kwargs)
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 not isinstance(f, VirtualField) or f.wildcard_data_elem:
1540
- yield f
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.0/ref/models/fields/#django.db.models.ForeignKey.on_delete>`__
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 not isinstance(desc, str):
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 isinstance(de, fields.VirtualField):
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
- a = resolve_action(spec, action)
182
+ ba = resolve_action(spec, action)
183
183
  # if str(spec).startswith("webshop"):
184
184
  # print("20210319", spec, action, a)
185
- kw.update(action=a)
185
+ kw.update(action=ba)
186
186
  if help_text is None:
187
- if a is a.actor.default_action:
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.utils.html import E, forcetext, tostring, join_elems
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.0/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericRelation>`_
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.action.help_text, **kw
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.action.help_text, **kwargs
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.action.help_text, **button_attrs
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.action.help_text, **kw
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
- ar.actor.get_table_summary(ar),
841
- stripped=stripped,
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.action.help_text))
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
@@ -26,3 +26,4 @@ pre_ui_delete = Signal() # ['request'])
26
26
  pre_ui_build = Signal()
27
27
  # post_ui_build = Signal()
28
28
  # database_connected = Signal()
29
+ database_prepared = Signal()
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/opics/loggin.rst:
371
- _history_aware_logging = True
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
- else:
671
- try:
672
- from systemd.journal import JournalHandler
673
- handlers["file"] = {
674
- "class": "systemd.journal.JournalHandler",
675
- "SYSLOG_IDENTIFIER": str(self.project_name),
676
- }
677
- except ImportError:
678
- pass
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 base in self.language_dict:
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") and self.site_config:
1829
- if self.site_config.site_company:
1830
- return self.site_config.site_company.get_address("<br/>")
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.0/ref/request-response/#django.http.HttpRequest.build_absolute_uri>`__
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
- sc = self.site_config.site_company
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.0/ref/models/fields/#booleanfield>`__.
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.0/ref/models/fields/#autofield>`__
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: