lino 25.6.1__py3-none-any.whl → 25.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +21 -0
  3. lino/core/actions.py +59 -25
  4. lino/core/actors.py +38 -16
  5. lino/core/boundaction.py +16 -0
  6. lino/core/choicelists.py +7 -7
  7. lino/core/constants.py +3 -0
  8. lino/core/dashboard.py +1 -0
  9. lino/core/dbtables.py +1 -1
  10. lino/core/elems.py +38 -13
  11. lino/core/fields.py +20 -11
  12. lino/core/kernel.py +8 -0
  13. lino/core/layouts.py +6 -2
  14. lino/core/menus.py +3 -6
  15. lino/core/model.py +5 -4
  16. lino/core/renderer.py +14 -5
  17. lino/core/requests.py +8 -7
  18. lino/core/signals.py +1 -0
  19. lino/core/site.py +48 -28
  20. lino/core/store.py +4 -2
  21. lino/core/tables.py +23 -10
  22. lino/core/utils.py +4 -1
  23. lino/core/workflows.py +2 -1
  24. lino/help_texts.py +1 -2
  25. lino/management/commands/prep.py +2 -2
  26. lino/management/commands/show.py +8 -10
  27. lino/mixins/__init__.py +14 -13
  28. lino/mixins/periods.py +2 -0
  29. lino/mixins/sequenced.py +1 -1
  30. lino/modlib/about/models.py +4 -3
  31. lino/modlib/checkdata/__init__.py +42 -36
  32. lino/modlib/checkdata/choicelists.py +9 -1
  33. lino/modlib/checkdata/fixtures/checkdata.py +4 -2
  34. lino/modlib/checkdata/models.py +9 -2
  35. lino/modlib/comments/models.py +4 -3
  36. lino/modlib/extjs/ext_renderer.py +4 -4
  37. lino/modlib/extjs/views.py +8 -2
  38. lino/modlib/gfks/fields.py +1 -1
  39. lino/modlib/help/__init__.py +3 -3
  40. lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
  41. lino/modlib/help/fixtures/demo2.py +6 -1
  42. lino/modlib/help/management/commands/makehelp.py +4 -1
  43. lino/modlib/help/models.py +2 -1
  44. lino/modlib/help/utils.py +12 -6
  45. lino/modlib/linod/choicelists.py +57 -4
  46. lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
  47. lino/modlib/linod/management/commands/linod.py +0 -13
  48. lino/modlib/linod/mixins.py +8 -0
  49. lino/modlib/linod/models.py +29 -30
  50. lino/modlib/memo/__init__.py +7 -7
  51. lino/modlib/memo/management/__init__,py +0 -0
  52. lino/modlib/memo/management/commands/__init__.py +0 -0
  53. lino/modlib/memo/management/commands/removeurls.py +67 -0
  54. lino/modlib/memo/mixins.py +1 -9
  55. lino/modlib/memo/parser.py +1 -1
  56. lino/modlib/notify/config/notify/summary.eml +5 -2
  57. lino/modlib/notify/fixtures/demo2.py +5 -6
  58. lino/modlib/notify/models.py +9 -10
  59. lino/modlib/periods/__init__.py +11 -8
  60. lino/modlib/periods/choicelists.py +16 -10
  61. lino/modlib/periods/models.py +45 -45
  62. lino/modlib/summaries/fixtures/checksummaries.py +4 -2
  63. lino/modlib/system/models.py +17 -18
  64. lino/modlib/uploads/fixtures/demo.py +9 -3
  65. lino/modlib/uploads/mixins.py +5 -2
  66. lino/modlib/uploads/models.py +15 -9
  67. lino/modlib/uploads/utils.py +4 -1
  68. lino/modlib/users/__init__.py +59 -18
  69. lino/modlib/users/actions.py +24 -20
  70. lino/modlib/users/fixtures/demo_users.py +2 -35
  71. lino/modlib/users/mixins.py +3 -4
  72. lino/modlib/users/models.py +53 -13
  73. lino/modlib/users/ui.py +30 -16
  74. lino/modlib/users/utils.py +5 -6
  75. lino/projects/std/settings.py +1 -1
  76. lino/sphinxcontrib/logo/templates/footer.html +1 -0
  77. lino/utils/ajax.py +1 -1
  78. lino/utils/cycler.py +5 -0
  79. lino/utils/dbhash.py +4 -9
  80. lino/utils/dpy.py +2 -2
  81. lino/utils/format_date.py +4 -3
  82. lino/utils/html.py +13 -5
  83. lino/utils/jsgen.py +1 -1
  84. lino/utils/quantities.py +8 -0
  85. lino/utils/soup.py +75 -106
  86. {lino-25.6.1.dist-info → lino-25.7.0.dist-info}/METADATA +1 -1
  87. {lino-25.6.1.dist-info → lino-25.7.0.dist-info}/RECORD +90 -87
  88. {lino-25.6.1.dist-info → lino-25.7.0.dist-info}/WHEEL +0 -0
  89. {lino-25.6.1.dist-info → lino-25.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
  90. {lino-25.6.1.dist-info → lino-25.7.0.dist-info}/licenses/COPYING +0 -0
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
@@ -441,7 +441,7 @@ class HtmlRenderer(Renderer):
441
441
  url = self.js2url(self.action_call(ar, ba, status))
442
442
  # ~ logger.info('20121002 window_action_button %s %r',a,unicode(label))
443
443
  return self.href_button_action(
444
- ba, url, str(label), title or ba.action.help_text, **kw
444
+ ba, url, str(label), title or ba.get_help_text(), **kw
445
445
  )
446
446
 
447
447
  def quick_add_buttons(self, ar):
@@ -543,7 +543,7 @@ class HtmlRenderer(Renderer):
543
543
  js = self.ar2js(ar, obj, **ar._status)
544
544
  uri = self.js2url(js)
545
545
  return self.href_button_action(
546
- ba, uri, label, title or ba.action.help_text, **kwargs
546
+ ba, uri, label, title or ba.get_help_text(), **kwargs
547
547
  )
548
548
 
549
549
  def menu_item_button(self, ar, mi, label=None, icon_name=None, **kwargs):
@@ -607,7 +607,7 @@ class HtmlRenderer(Renderer):
607
607
  js = self.action_call_on_instance(obj, ar, ba, request_kwargs)
608
608
  uri = self.js2url(js)
609
609
  return self.href_button_action(
610
- ba, uri, label, title or ba.action.help_text, **button_attrs
610
+ ba, uri, label, title or ba.get_help_text(), **button_attrs
611
611
  )
612
612
 
613
613
  def row_action_button_ar(
@@ -622,7 +622,7 @@ class HtmlRenderer(Renderer):
622
622
  js = self.action_call_on_instance(obj, ar, ba)
623
623
  uri = self.js2url(js)
624
624
  return self.href_button_action(
625
- ba, uri, label, title or ba.action.help_text, **kw
625
+ ba, uri, label, title or ba.get_help_text(), **kw
626
626
  )
627
627
 
628
628
  def show_story(self, ar, story, stripped=True, **kwargs):
@@ -859,7 +859,7 @@ class TextRenderer(HtmlRenderer):
859
859
  if cls.insert_action is not None and cls.editable:
860
860
  ir = cls.insert_action.request_from(ar)
861
861
  if ir.get_permission():
862
- items.append("(+) {}".format(cls.insert_action.action.help_text))
862
+ items.append("(+) {}".format(cls.insert_action.get_help_text()))
863
863
  # for i, obj in enumerate(ar.data_iterator):
864
864
  # if i == cls.preview_limit:
865
865
  # break
@@ -874,6 +874,15 @@ class TextRenderer(HtmlRenderer):
874
874
  yield rstgen.ul(items).strip()
875
875
  return
876
876
 
877
+ if display_mode == constants.DISPLAY_MODE_TILES:
878
+ prev = None
879
+ for obj in ar.sliced_data_iterator:
880
+ txt = obj.as_tile(ar, prev)
881
+ txt = html2text(txt)
882
+ yield txt.strip()
883
+ prev = obj
884
+ return
885
+
877
886
  # At this point, display_mode is one of story, grid or html.
878
887
 
879
888
  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
+ # 20250713 buttons.append(self.open_in_own_window_button())
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:
lino/core/tables.py CHANGED
@@ -403,7 +403,7 @@ class AbstractTable(actors.Actor):
403
403
  order_by = None
404
404
  """If specified, this must be a tuple or list of field names that
405
405
  will be passed to Django's `order_by
406
- <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#order-by>`__
406
+ <https://docs.djangoproject.com/en/5.2/ref/models/querysets/#order-by>`__
407
407
  method in order to sort the rows of the queryset.
408
408
 
409
409
  """
@@ -412,7 +412,7 @@ method in order to sort the rows of the queryset.
412
412
  """
413
413
  If specified, this must be a :class:`django.db.models.Q` object that will be
414
414
  passed to Django's `filter
415
- <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#filter>`__
415
+ <https://docs.djangoproject.com/en/5.2/ref/models/querysets/#filter>`__
416
416
  method.
417
417
 
418
418
  If you allow a user to insert rows into a filtered table, you should make
@@ -427,7 +427,7 @@ method in order to sort the rows of the queryset.
427
427
  One advantage of :attr:`filter` over
428
428
  :attr:`known_values <lino.core.actors.Actor.known_values>`
429
429
  is that this can use the full range of Django's `field lookup methods
430
- <https://docs.djangoproject.com/en/5.0/topics/db/queries/#field-lookups>`_
430
+ <https://docs.djangoproject.com/en/5.2/topics/db/queries/#field-lookups>`_
431
431
 
432
432
  """
433
433
 
@@ -435,7 +435,7 @@ method in order to sort the rows of the queryset.
435
435
  """
436
436
  If specified, this must be a :class:`django.db.models.Q` object that will be
437
437
  passed to Django's `exclude
438
- <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#exclude>`__
438
+ <https://docs.djangoproject.com/en/5.2/ref/models/querysets/#exclude>`__
439
439
  method.
440
440
 
441
441
  This is the logical opposite of :attr:`filter`.
@@ -485,6 +485,8 @@ method in order to sort the rows of the queryset.
485
485
  resolve_fields_list(cls, "mobile_columns", set, {})
486
486
  resolve_fields_list(cls, "popin_columns", set, {})
487
487
  if cls.model is not None:
488
+ if not isinstance(cls.model, type):
489
+ raise Exception(f"{cls}.model is {repr(cls.model)}")
488
490
  if not issubclass(cls.model, models.Model):
489
491
  if cls.model._lino_default_table is None:
490
492
  cls.model._lino_default_table = cls
@@ -526,13 +528,24 @@ method in order to sort the rows of the queryset.
526
528
  @classmethod
527
529
  def get_default_action(cls):
528
530
  if cls.default_record_id is not None:
531
+ # return cls._detail_action_class(
532
+ # label=cls.label, help_text=cls.help_text,
533
+ # default_record_id = cls.default_record_id)
529
534
  # assert cls.display_mode == ((None, constants.DISPLAY_MODE_DETAIL))
530
535
  assert cls.detail_action is not None
531
- cls.detail_action.action.label = cls.label
532
- cls.detail_action.action.help_text = cls.help_text
533
- cls.detail_action.action.default_record_id = cls.default_record_id
534
- return cls.detail_action
535
- return actions.ShowTable()
536
+ # if cls.detail_action.action.defining_actor is not cls:
537
+ # raise Exception(
538
+ # f"{cls.detail_action.action.defining_actor} is not {cls}")
539
+ a = cls.detail_action.action
540
+ return a.__class__(
541
+ a.owner,
542
+ label=cls.label, help_text=cls.help_text,
543
+ default_record_id=cls.default_record_id)
544
+ # cls.detail_action.action.label = cls.label
545
+ # cls.detail_action.help_text = cls.help_text
546
+ # cls.detail_action.action.default_record_id = cls.default_record_id
547
+ # return cls.detail_action
548
+ return actions.SHOW_TABLE
536
549
 
537
550
  @classmethod
538
551
  def get_actor_editable(self):
@@ -611,7 +624,7 @@ method in order to sort the rows of the queryset.
611
624
  if not isinstance(master_instance, self.master):
612
625
  # e.g. a ByUser table descendant called by AnonymousUser
613
626
  msg = "20240731 %r is not a %s (%s.master_key = '%s')" % (
614
- master_instance.__class__,
627
+ master_instance,
615
628
  self.master,
616
629
  self,
617
630
  self.master_key,
lino/core/utils.py CHANGED
@@ -13,6 +13,7 @@ import datetime
13
13
  # import yaml
14
14
  from importlib import import_module
15
15
  from django.utils.html import format_html, mark_safe, SafeString
16
+ from django.utils.functional import Promise
16
17
  from django.db import models
17
18
  from django.db.models import Q
18
19
  from django.core.exceptions import FieldDoesNotExist
@@ -90,7 +91,7 @@ def getrqdata(request):
90
91
  """Return the request data.
91
92
 
92
93
  Unlike the now defunct `REQUEST
93
- <https://docs.djangoproject.com/en/5.0/ref/request-response/#django.http.HttpRequest.REQUEST>`_
94
+ <https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest.REQUEST>`_
94
95
  attribute, this inspects the request's `method` in order to decide
95
96
  what to return.
96
97
 
@@ -1163,6 +1164,8 @@ class Panel:
1163
1164
 
1164
1165
  def resolve_layout(cls, k, spec, layout_class, **options):
1165
1166
  # k: just for naming the culprit in error messages
1167
+ if isinstance(spec, Promise):
1168
+ spec = str(spec)
1166
1169
  if isinstance(spec, str):
1167
1170
  if "\n" in spec or "." not in spec:
1168
1171
  return layout_class(spec, cls, **options)
lino/core/workflows.py CHANGED
@@ -255,7 +255,8 @@ class ChangeStateAction(actions.Action):
255
255
  self.button_text = target_state.button_text
256
256
 
257
257
  if self.icon_name:
258
- self.help_text = format_lazy("{}. {}", self.label, self.help_text)
258
+ self.help_text = format_lazy(
259
+ "{}. {}", self.label, self.help_text)
259
260
 
260
261
  # def get_action_permission(self, ar, obj, state):
261
262
  # if not super(ChangeStateAction, self).get_action_permission(ar, obj, state):
lino/help_texts.py CHANGED
@@ -110,7 +110,6 @@ help_texts = {
110
110
  'lino.modlib.bootstrap3.views.Element' : _("""Render a single record."""),
111
111
  'lino.modlib.bootstrap3.views.Index' : _("""Render the main page."""),
112
112
  'lino.modlib.checkdata.Plugin' : _("""The config descriptor for this plugin."""),
113
- 'lino.modlib.checkdata.Plugin.responsible_user' : _("""The username of the main checkdata responsible, i.e. a designated user who will be attributed to checkdata messages for which no specific responible could be designated (returned by the checker’s get_responsible_user method)."""),
114
113
  'lino.modlib.checkdata.Plugin.on_plugins_loaded' : _("""Set responsible_user to "'robin' if this is a demo site (is_demo_site)."""),
115
114
  'lino.modlib.checkdata.roles.CheckdataUser' : _("""Can see checkdata messages."""),
116
115
  'lino.modlib.comments.Plugin' : _("""See /dev/plugins."""),
@@ -560,7 +559,7 @@ help_texts = {
560
559
  'lino.modlib.jinja.XMLMaker.xml_validator_file' : _("""The name of a “validator” to use for validating the XML content."""),
561
560
  'lino.modlib.jinja.XMLMaker.get_xml_file' : _("""Get the name of the XML file to be generated for this database row."""),
562
561
  'lino.modlib.jinja.XMLMaker.make_xml_file' : _("""Make the XML file for this database row."""),
563
- 'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
562
+ 'lino.modlib.memo.Previewable' : _("""See dg.memo.Previewable."""),
564
563
  'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
565
564
  'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
566
565
  'lino.modlib.memo.Previewable.body_full_preview' : _("""A read-only full preview of body."""),