lino 25.2.1__py3-none-any.whl → 25.2.3__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 (43) hide show
  1. lino/__init__.py +8 -3
  2. lino/api/dd.py +13 -0
  3. lino/api/doctest.py +49 -17
  4. lino/api/selenium.py +1 -1
  5. lino/core/actors.py +16 -7
  6. lino/core/boundaction.py +1 -1
  7. lino/core/choicelists.py +1 -1
  8. lino/core/dbtables.py +15 -15
  9. lino/core/elems.py +1 -1
  10. lino/core/renderer.py +2 -2
  11. lino/core/requests.py +49 -13
  12. lino/core/site.py +5 -5
  13. lino/help_texts.py +8 -3
  14. lino/modlib/comments/models.py +1 -1
  15. lino/modlib/comments/ui.py +1 -1
  16. lino/modlib/extjs/__init__.py +2 -2
  17. lino/modlib/extjs/views.py +75 -29
  18. lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
  19. lino/modlib/jinja/mixins.py +62 -0
  20. lino/modlib/jinja/models.py +6 -0
  21. lino/modlib/linod/fixtures/__init__.py +0 -0
  22. lino/modlib/linod/fixtures/linod.py +32 -0
  23. lino/modlib/linod/mixins.py +15 -1
  24. lino/modlib/memo/mixins.py +11 -3
  25. lino/modlib/memo/parser.py +1 -1
  26. lino/modlib/notify/models.py +1 -1
  27. lino/modlib/printing/actions.py +6 -12
  28. lino/modlib/printing/choicelists.py +7 -7
  29. lino/modlib/uploads/__init__.py +9 -6
  30. lino/modlib/uploads/models.py +3 -3
  31. lino/modlib/uploads/ui.py +5 -2
  32. lino/modlib/users/mixins.py +4 -0
  33. lino/utils/__init__.py +0 -1
  34. lino/utils/jscompressor.py +4 -4
  35. lino/utils/restify.py +2 -2
  36. lino/utils/soup.py +4 -4
  37. lino/utils/xml.py +19 -5
  38. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/METADATA +1 -1
  39. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/RECORD +42 -39
  40. lino/utils/requests.py +0 -55
  41. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/WHEEL +0 -0
  42. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/AUTHORS.rst +0 -0
  43. {lino-25.2.1.dist-info → lino-25.2.3.dist-info}/licenses/COPYING +0 -0
lino/__init__.py CHANGED
@@ -26,7 +26,7 @@ defines no models, some template files, a series of :term:`django-admin commands
26
26
 
27
27
  """
28
28
 
29
- __version__ = '25.2.1'
29
+ __version__ = '25.2.3'
30
30
 
31
31
  # import setuptools # avoid UserWarning "Distutils was imported before Setuptools"?
32
32
 
@@ -64,14 +64,19 @@ import warnings
64
64
 
65
65
  warnings.filterwarnings(
66
66
  "error",
67
- "DateTimeField .* received a naive datetime (.*) while time zone support is active.",
67
+ r"DateTimeField .* received a naive datetime (.*) while time zone support is active.",
68
68
  RuntimeWarning,
69
69
  "django.db.models.fields",
70
70
  )
71
71
 
72
+ # TODO: Is it okay to ignore the followgin warning? It's because e.g.
73
+ # lino.modlib.excerpts has a pre_analyze receiver set_excerpts_actions(), which
74
+ # accesses the database to set actions before Lino analyzes the models. It
75
+ # catches OperationalError & Co because --of course-- it fails e.g. for
76
+ # admin-commands like "pm prep".
72
77
  warnings.filterwarnings(
73
78
  "ignore",
74
- "Accessing the database during app initialization is discouraged\. To fix this warning, avoid executing queries in AppConfig\.ready\(\) or when your app modules are imported\.",
79
+ r"Accessing the database during app initialization is discouraged\. To fix this warning, avoid executing queries in AppConfig\.ready\(\) or when your app modules are imported\.",
75
80
  RuntimeWarning,
76
81
  "django.db.backends.utils",
77
82
  )
lino/api/dd.py CHANGED
@@ -209,6 +209,19 @@ from lino.modlib.linod.choicelists import Procedures
209
209
 
210
210
 
211
211
  def background_task(**kwargs):
212
+ """
213
+ Register the decorated function as a :term:`background task`.
214
+
215
+ Keyword arguments are used as default values when checkdata creates a
216
+ :class:`lino.modlib.linod.SystemTask` instance for this procedure.
217
+
218
+ Except for the special keyword ``class_name``, which defaults to
219
+ "linod.SystemTask". It is used by :mod:`lino_xl.lib.invoicing` to register a
220
+ procedure that will create an :term:`invoicing task` instead of a normal
221
+ :term:`background task`. :class:`lino_xl.lib.invoicing.InvoicingTask`
222
+ instead of :class:`lino.modlib.linod.SystemTask`.
223
+
224
+ """
212
225
  if "class_name" not in kwargs:
213
226
  kwargs["class_name"] = "linod.SystemTask"
214
227
 
lino/api/doctest.py CHANGED
@@ -1,28 +1,45 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2015-2024 Rumma & Ko Ltd
2
+ # Copyright 2015-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  """
5
- A selection of names to be used in tested documents.
5
+ A selection of names to be used in tested documents as follows:
6
+
7
+ >>> from lino.api.doctest import *
8
+
9
+ This is by convention everything we want to have in the global namespace of a
10
+ tested document. It includes
11
+
12
+ - well-known Python standard modules like os, sys, datetime and collections.
13
+ - A variable :data:`test_client`
14
+
6
15
  """
7
16
 
17
+ import os
18
+ import sys
19
+ import datetime
20
+ import collections
8
21
  import six # TODO: remove here and then run all doctests
9
22
  import logging
10
23
  import sqlparse
24
+ import json
25
+ import textwrap
26
+
27
+ from bs4 import BeautifulSoup
28
+ from pprint import pprint, pformat
11
29
  from urllib.parse import urlencode
30
+
12
31
  import django
13
32
  django.setup()
33
+
14
34
  from lino.core.constants import *
15
35
  from lino.api.shell import *
16
36
  from django.utils import translation
17
37
  from django.utils.encoding import force_str
18
38
  from django.test import Client
19
39
  from django.db import connection, reset_queries as reset_sql_queries
20
- import json
21
- from bs4 import BeautifulSoup
22
- import textwrap
23
- from pprint import pprint, pformat
24
40
 
25
- from rstgen import table, ul
41
+ import pytest
42
+ # from rstgen import table, ul
26
43
  import rstgen
27
44
  from rstgen import attrtable
28
45
  from rstgen.utils import unindent, rmu, sixprint
@@ -49,10 +66,12 @@ from lino.core.actions import register_params
49
66
  from lino.core.layouts import BaseLayout
50
67
 
51
68
  test_client = Client()
52
- # naming it simply "client" caused conflict with a
53
- # `lino_welfare.pcsw.models.Client`
69
+ """An instance of :class:`django.test.Client`.
54
70
 
55
- import collections
71
+ N.B. Naming it simply "client" caused conflict with a
72
+ :class:`lino_welfare.pcsw.models.Client`
73
+
74
+ """
56
75
 
57
76
  HttpQuery = collections.namedtuple(
58
77
  "HttpQuery", ["username", "url_base", "json_fields", "expected_rows", "kwargs"]
@@ -293,7 +312,7 @@ def show_workflow(actions, all=False, language=None):
293
312
  # required_roles
294
313
  ]
295
314
  )
296
- print(table(cols, cells).strip())
315
+ print(rstgen.table(cols, cells).strip())
297
316
 
298
317
  if language:
299
318
  with translation.override(language):
@@ -363,7 +382,7 @@ def fields_help(model, fieldnames=None, columns=False, all=None):
363
382
 
364
383
  # return table(cols, cells).strip()
365
384
  items = ["{} ({}) : {}".format(row[1], row[0], row[2]) for row in cells]
366
- return ul(items).strip()
385
+ return rstgen.ul(items).strip()
367
386
 
368
387
 
369
388
  def show_fields(*args, **kwargs):
@@ -552,7 +571,7 @@ def show_choicelist(cls):
552
571
  for i in cls.get_list_items():
553
572
  row = [i.value, i.name] + str2languages(i.text)
554
573
  rows.append(row)
555
- print(table(headers, rows))
574
+ print(rstgen.table(headers, rows))
556
575
 
557
576
 
558
577
  def show_choicelists():
@@ -568,7 +587,7 @@ def show_choicelists():
568
587
  i.verbose_name_plural
569
588
  )
570
589
  rows.append(row)
571
- print(table(headers, rows))
590
+ print(rstgen.table(headers, rows))
572
591
 
573
592
 
574
593
  def show_permissions(*args):
@@ -590,7 +609,7 @@ def show_translations(things, fmt, languages=None):
590
609
  x, txt = fmt(thing)
591
610
  cells.append(txt)
592
611
  rows.append(cells)
593
- print(table(headers, rows))
612
+ print(rstgen.table(headers, rows))
594
613
 
595
614
 
596
615
  def show_model_translations(*models, **kwargs):
@@ -779,7 +798,7 @@ def show_change_watchers():
779
798
  rows.append(
780
799
  [full_model_name(m), ws.master_key, " ".join(sorted(ws.ignored_fields))]
781
800
  )
782
- print(table(headers, rows, max_width=40))
801
+ print(rstgen.table(headers, rows, max_width=40))
783
802
 
784
803
  def show_display_modes():
785
804
  """
@@ -796,4 +815,17 @@ def show_display_modes():
796
815
  ("x" if dm in a.extra_display_modes else "")
797
816
  for dm in dml]
798
817
  )
799
- print(table(headers, rows))
818
+ print(rstgen.table(headers, rows))
819
+
820
+
821
+ def checkdb(m, num):
822
+ """
823
+ Raise an exception if the database doesn't contain the specified number of
824
+ rows of the specified model.
825
+
826
+ This is for usage in :xfile:`startup.py` scripts.
827
+
828
+ """
829
+ if m.objects.count() != num:
830
+ raise Exception(
831
+ f"Model {m} should have {num} rows but has {m.objects.count()}")
lino/api/selenium.py CHANGED
@@ -115,7 +115,7 @@ class Tour(object):
115
115
  error_message = None
116
116
  language = None
117
117
  languages = []
118
- server_url = "http://127.0.0.1:8000/"
118
+ server_url = "http://127.0.0.1:8000"
119
119
  images_width = 90
120
120
 
121
121
  def __init__(
lino/core/actors.py CHANGED
@@ -1948,17 +1948,26 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1948
1948
  return html
1949
1949
 
1950
1950
  @classmethod
1951
- def get_table_summary(cls, obj, ar=None):
1951
+ def get_slave_summary(cls, obj, ar=None):
1952
+ """
1953
+ :param cls: Slave table
1954
+ :param obj: Master instance
1955
+ :param ar: Action request on master table
1956
+ """
1957
+ if ar is None:
1958
+ return ''
1959
+ sar = cls.request(parent=ar, master_instance=obj, is_on_main_actor=False)
1960
+ return cls.get_table_summary(sar)
1961
+
1962
+ @classmethod
1963
+ def get_table_summary(cls, ar):
1952
1964
  """
1953
1965
  Return the HTML `<div>` to be displayed by
1954
1966
  :class:`lino.core.elems.TableSummaryPanel`.
1955
1967
  It basically just calls :meth:`table_as_summary`.
1956
1968
 
1957
1969
  """
1958
- if ar is None:
1959
- return ''
1960
- sar = cls.request(parent=ar, master_instance=obj, is_on_main_actor=False)
1961
- p = cls.table_as_summary(sar)
1970
+ p = cls.table_as_summary(ar)
1962
1971
  # assert_safe(p) # temporary 20240506
1963
1972
  # print("20240712", p)
1964
1973
  # return format_html(DIVTPL, p)
@@ -1981,9 +1990,9 @@ class Actor(actions.Parametrizable, Permittable, metaclass=ActorMetaClass):
1981
1990
  """
1982
1991
  p = qs2summary(
1983
1992
  ar,
1984
- ar.data_iterator,
1993
+ ar.sliced_data_iterator,
1985
1994
  separator=cls.summary_sep,
1986
- max_items=cls.preview_limit,
1995
+ max_items=ar.limit or cls.preview_limit,
1987
1996
  wraptpl=None,
1988
1997
  )
1989
1998
  # assert isinstance(p, str)
lino/core/boundaction.py CHANGED
@@ -184,4 +184,4 @@ class BoundAction(object):
184
184
  return "<%s(%s, %r)>" % (self.__class__.__name__, self.actor, self.action)
185
185
 
186
186
  def __str__(self):
187
- return "{} on {}".format(self.action, self.actor)
187
+ return f"{self.action.__class__.__name__} on {self.actor}"
lino/core/choicelists.py CHANGED
@@ -823,7 +823,7 @@ class ChoiceList(with_metaclass(ChoiceListMeta, tables.AbstractTable)):
823
823
  @classmethod
824
824
  def filter(self, **fkw):
825
825
  def f(item):
826
- for k, v in list(fkw.items()):
826
+ for k, v in fkw.items():
827
827
  if getattr(item, k) != v:
828
828
  return False
829
829
  return True
lino/core/dbtables.py CHANGED
@@ -315,22 +315,22 @@ class Table(AbstractTable):
315
315
  # qs = self.model.get_request_queryset(ar)
316
316
  # str(qs.query)
317
317
 
318
- # return qs.get(pk=pk)
319
- try:
318
+ if not settings.SITE.catch_layout_exceptions:
320
319
  return qs.get(pk=pk)
321
- # except ValueError:
322
- # return None
323
- except self.model.DoesNotExist:
324
- # sql = qs.query
325
- # import sqlparse
326
- # sql = str(sql).replace('"', '')
327
- # sql = sqlparse.format(sql, reindent=True, keyword_case='upper')
328
- # raise self.model.DoesNotExist(f"No row {pk} in {ar} ({sql})")
329
- raise self.model.DoesNotExist(f"No row {pk} in {ar}") from None
330
- # from django.core.exceptions import ObjectDoesNotExist
331
- # assert isinstance(exc, ObjectDoesNotExist)
332
- # raise exc
333
- # return None
320
+ else:
321
+ try:
322
+ return qs.get(pk=pk)
323
+ except self.model.DoesNotExist:
324
+ # sql = qs.query
325
+ # import sqlparse
326
+ # sql = str(sql).replace('"', '')
327
+ # sql = sqlparse.format(sql, reindent=True, keyword_case='upper')
328
+ # raise self.model.DoesNotExist(f"No row {pk} in {ar} ({sql})")
329
+ raise self.model.DoesNotExist(f"No row {pk} in {ar}") from None
330
+ # from django.core.exceptions import ObjectDoesNotExist
331
+ # assert isinstance(exc, ObjectDoesNotExist)
332
+ # raise exc
333
+ # return None
334
334
 
335
335
  # @classmethod
336
336
  # def disabled_actions(self, ar, obj): # no longer used since 20170909
lino/core/elems.py CHANGED
@@ -1830,7 +1830,7 @@ class SlaveSummaryPanel(LightWeightContainer):
1830
1830
  oui5_field_template = "openui5/elems/field/SlaveSummaryElement.xml"
1831
1831
 
1832
1832
  def __init__(self, lh, slave, name, **kw):
1833
- super().__init__(lh, slave, name, slave.get_table_summary, **kw)
1833
+ super().__init__(lh, slave, name, slave.get_slave_summary, **kw)
1834
1834
 
1835
1835
 
1836
1836
  class StoryElement(LightWeightContainer):
lino/core/renderer.py CHANGED
@@ -284,7 +284,7 @@ class HtmlRenderer(Renderer):
284
284
  raise Exception("Both nosummary and display_mode were specified")
285
285
 
286
286
  if display_mode == constants.DISPLAY_MODE_SUMMARY:
287
- yield ar.actor.get_table_summary(ar.master_instance, ar)
287
+ yield ar.actor.get_table_summary(ar)
288
288
  return
289
289
 
290
290
  if header_level is not None:
@@ -834,7 +834,7 @@ class TextRenderer(HtmlRenderer):
834
834
  # yield "20240506 {}".format(ar)
835
835
  if display_mode == constants.DISPLAY_MODE_SUMMARY:
836
836
  s = to_rst(
837
- ar.actor.get_table_summary(ar.master_instance, ar),
837
+ ar.actor.get_table_summary(ar),
838
838
  stripped=stripped,
839
839
  )
840
840
  if stripped:
lino/core/requests.py CHANGED
@@ -24,6 +24,7 @@ from django.conf import settings
24
24
  from django.utils.translation import gettext_lazy as _
25
25
  from django.utils.translation import gettext
26
26
  from django.utils.translation import get_language, activate
27
+ from django.utils.html import format_html
27
28
  from django.utils import translation
28
29
  from django.utils import timezone
29
30
  from django.core.mail import send_mail
@@ -103,6 +104,16 @@ def mi2bp(master_instance, bp):
103
104
  # self.master_instance.__class__, self.master_instance))
104
105
 
105
106
 
107
+ class PrintLogger(logging.Logger):
108
+ format = "%(message)s"
109
+
110
+ def __init__(self, level):
111
+ super().__init__("~", level)
112
+ h = logging.StreamHandler(stream=sys.stdout)
113
+ h.setFormatter(logging.Formatter(self.format))
114
+ self.addHandler(h)
115
+ # print("20231012", logging.getLevelName(self.level), self.__class__)
116
+
106
117
  class StringLogger(logging.Logger):
107
118
  # Instantiated by BaseRequest.capture_logger()
108
119
 
@@ -535,12 +546,26 @@ class BaseRequest:
535
546
  try:
536
547
  # We instantiate a temporary Logger object, which is not known by the
537
548
  # root logger.
549
+ # self.logger = PrintLogger(level)
538
550
  self.logger = StringLogger(old_logger, level)
539
551
  # self.logger.parent =
540
552
  yield self.logger
541
553
 
542
554
  finally:
543
555
  self.logger.streamer.flush()
556
+ # print(self.logger.getvalue())
557
+ self.logger = old_logger
558
+
559
+ @contextmanager
560
+ def print_logger(self, level=logging.INFO):
561
+ old_logger = self.logger
562
+ try:
563
+ # We instantiate a temporary Logger object, which is not known by the
564
+ # root logger.
565
+ self.logger = PrintLogger(level)
566
+ yield None
567
+
568
+ finally:
544
569
  self.logger = old_logger
545
570
 
546
571
  @contextmanager
@@ -820,22 +845,29 @@ class BaseRequest:
820
845
  Given a tuple of primary keys, set :attr:`selected_rows` to a list
821
846
  of corresponding database objects.
822
847
 
823
- The special pks's -99998 and -99999 are filtered out.
848
+ TODO: Explain why the special primary keys -99998 and -99999 are
849
+ filtered out.
824
850
 
825
851
  """
826
852
  # ~ print 20131003, selected_pks
827
853
  self.selected_rows = []
828
- # for pk in selected_pks:
829
- # if pk and pk != "-99998" and pk != "-99999":
830
- # self.selected_rows.append(self.get_row_by_pk(pk))
831
- try:
854
+
855
+ # Until 20250212 we catched for ObjectDoesNotExist here in order to
856
+ # reformulate the message. This made #5924 (Menu "My invoicing plan"
857
+ # fails) difficult to diagnose.
858
+ if True:
832
859
  for pk in selected_pks:
833
860
  if pk and pk != "-99998" and pk != "-99999":
834
861
  self.selected_rows.append(self.get_row_by_pk(pk))
835
- except ObjectDoesNotExist:
836
- # raise exceptions.BadRequest(
837
- raise ObjectDoesNotExist(
838
- f"No row with primary key {pk} in {self.actor}") from None
862
+ else:
863
+ try:
864
+ for pk in selected_pks:
865
+ if pk and pk != "-99998" and pk != "-99999":
866
+ self.selected_rows.append(self.get_row_by_pk(pk))
867
+ except ObjectDoesNotExist:
868
+ # raise exceptions.BadRequest(
869
+ raise ObjectDoesNotExist(
870
+ f"No row with primary key {repr(pk)} in {self.actor}") from None
839
871
  # self.selected_rows = filter(lambda x: x, self.selected_rows)
840
872
  # note: ticket #523 was because the GET contained an empty pk ("&sr=")
841
873
 
@@ -1631,7 +1663,8 @@ class BaseRequest:
1631
1663
  # print("20190703", self.actor, self.actor.default_action)
1632
1664
  sar = self.spawn_request(actor=self.actor)
1633
1665
  list_title = tostring(sar.href_to_request(sar, list_title, icon_name=None))
1634
- return list_title + " » " + self.get_detail_title(elem)
1666
+ # return list_title + " » " + self.get_detail_title(elem)
1667
+ return format_html("{} » {}", list_title, self.get_detail_title(elem))
1635
1668
 
1636
1669
  def form2obj_and_save(ar, data, elem, is_new):
1637
1670
  """
@@ -1885,10 +1918,13 @@ class ActionRequest(BaseRequest):
1885
1918
  self.set_selected_pks(*selected_pks)
1886
1919
 
1887
1920
  def __str__(self):
1888
- return "{0} {1}".format(self.__class__.__name__, self.bound_action)
1921
+ return f"{self.__class__.__name__} for {self.bound_action}"
1889
1922
 
1890
- def __repr__(self):
1891
- return "{0} {1}".format(self.__class__.__name__, self.bound_action)
1923
+ # def __str__(self):
1924
+ # return "{0} {1}".format(self.__class__.__name__, self.bound_action)
1925
+ #
1926
+ # def __repr__(self):
1927
+ # return "{0} {1}".format(self.__class__.__name__, self.bound_action)
1892
1928
 
1893
1929
  def gen_insert_button(
1894
1930
  self, target=None, button_attrs=dict(style="float: right;"), **values
lino/core/site.py CHANGED
@@ -356,7 +356,7 @@ class Site(object):
356
356
  verbose_client_info_message = False
357
357
 
358
358
  stopsignal = "SIGTERM"
359
- help_url = "http://www.lino-framework.org"
359
+ help_url = "https://www.lino-framework.org"
360
360
 
361
361
  help_email = "users@lino-framework.org"
362
362
 
@@ -395,11 +395,11 @@ class Site(object):
395
395
 
396
396
  date_format_strftime = "%d.%m.%Y"
397
397
 
398
- date_format_regex = "/^[0123]?\d\.[01]?\d\.-?\d+$/"
398
+ date_format_regex = r"/^[0123]?\d\.[01]?\d\.-?\d+$/"
399
399
 
400
400
  datetime_format_strftime = "%Y-%m-%dT%H:%M:%S"
401
401
 
402
- datetime_format_extjs = "Y-m-d\TH:i:s"
402
+ datetime_format_extjs = r"Y-m-d\TH:i:s"
403
403
 
404
404
  quick_startup = False
405
405
 
@@ -973,6 +973,7 @@ class Site(object):
973
973
 
974
974
  def install_settings(self):
975
975
  assert not self.help_url.endswith("/")
976
+ assert not self.server_url.endswith("/")
976
977
 
977
978
  for p in self.installed_plugins:
978
979
  p.install_django_settings(self)
@@ -2136,13 +2137,12 @@ class Site(object):
2136
2137
 
2137
2138
  # yield "lino.modlib.lino_startup"
2138
2139
 
2139
- # server_url = None
2140
2140
  copyright_name = None
2141
2141
  """Name of copyright holder of the site's content."""
2142
2142
 
2143
2143
  copyright_url = None
2144
2144
 
2145
- server_url = "http://127.0.0.1:8000/"
2145
+ server_url = "http://127.0.0.1:8000"
2146
2146
  """The "official" URL used by "normal" users when accessing this Lino
2147
2147
  site.
2148
2148
 
lino/help_texts.py CHANGED
@@ -374,13 +374,14 @@ help_texts = {
374
374
  'lino.modlib.linod.Procedures' : _("""The choicelist of background procedures available in this application."""),
375
375
  'lino.modlib.linod.LogLevels' : _("""A choicelist of logging levels available in this application."""),
376
376
  'lino.modlib.linod.SystemTask' : _("""Django model used to represent a background task."""),
377
- 'lino.modlib.linod.SystemTask.procedure' : _("""Pointer to an instance of Procedure."""),
378
377
  'lino.modlib.linod.SystemTask.start_datetime' : _("""Tells at what time exactly this job started."""),
379
378
  'lino.modlib.linod.SystemTask.message' : _("""Stores information about the job, mostly logs."""),
380
379
  'lino.modlib.linod.SystemTask.disabled' : _("""Tells whether the task should be ignored."""),
381
380
  'lino.modlib.linod.SystemTask.log_level' : _("""The logging level to apply when running this task."""),
382
381
  'lino.modlib.linod.SystemTask.run' : _("""Performs a routine job."""),
383
382
  'lino.modlib.linod.SystemTasks' : _("""The default actor for the SystemTask model."""),
383
+ 'lino.modlib.linod.Runnable' : _("""Model mixin used by SystemTask and other models."""),
384
+ 'lino.modlib.linod.Runnable.procedure' : _("""The background procedure to run in this task."""),
384
385
  'lino.modlib.periods.StoredYear' : _("""The Django model used to store a fiscal year."""),
385
386
  'lino.modlib.periods.StoredPeriod' : _("""The Django model used to store an accounting period."""),
386
387
  'lino.modlib.periods.StoredYears' : _("""The fiscal years defined in this database."""),
@@ -511,14 +512,14 @@ help_texts = {
511
512
  'lino.modlib.comments.AllComments' : _("""Show all comments."""),
512
513
  'lino.modlib.comments.MyComments' : _("""Show the comments posted by the current user."""),
513
514
  'lino.modlib.comments.RecentComments' : _("""Show the most recent comments that have been posted on this site."""),
514
- 'lino.modlib.comments.CommentsByRFC' : _("""Shows the comments for a given database object."""),
515
+ 'lino.modlib.comments.CommentsByRFC' : _("""Shows the comments about a given database row."""),
515
516
  'lino.modlib.comments.CommentEvents' : _("""The choicelist with selections for Comments.observed_event."""),
516
517
  'lino.modlib.comments.Emotions' : _("""The list of available values for the Comment.emotion field."""),
517
518
  'lino.modlib.comments.CommentType' : _("""The CommentType model is not being used in production, one day we will probably remove it."""),
518
519
  'lino.modlib.comments.CommentTypes' : _("""The table with all existing comment types."""),
519
520
  'lino.modlib.comments.Commentable' : _("""Mixin for models that are commentable, i.e. the rows of which can become discussion topic of comments."""),
520
521
  'lino.modlib.comments.Commentable.add_comments_filter' : _("""Add filters to the given queryset of comments, requested by the given user."""),
521
- 'lino.modlib.comments.Commentable.get_rfc_description' : _("""Return a HTML formatted string with the description of this Commentable as it should be displayed by the slave summary of CommentsByOwner."""),
522
+ 'lino.modlib.comments.Commentable.get_rfc_description' : _("""Return a HTML formatted string with the description of this Commentable as it should be displayed by the slave summary of CommentsByRFC."""),
522
523
  'lino.modlib.comments.Commentable.on_commented' : _("""This is automatically called when a comment has been created or modified."""),
523
524
  'lino.modlib.files.Volume' : _("""The Django model representing a file volume."""),
524
525
  'lino.modlib.files.Volume.id' : _("""The primary key used to point to this volume from a database object."""),
@@ -552,6 +553,10 @@ help_texts = {
552
553
  'lino.modlib.gkfs.GenericForeignKey' : _("""Add verbose_name and help_text to Django’s GFK."""),
553
554
  'lino.modlib.gkfs.GenericForeignKeyIdField' : _("""Use this instead of models.PositiveIntegerField for fields that are part of a GFK and you want Lino to render them using a Combobox."""),
554
555
  'lino.modlib.jinja.JinjaBuildMethod' : _("""Inherits from lino.modlib.printing.DjangoBuildMethod."""),
556
+ 'lino.modlib.jinja.XMLMaker' : _("""Usage example in /topics/xml"""),
557
+ 'lino.modlib.jinja.XMLMaker.xml_file_name' : _("""The name of the XML file to generate. This file will be overwritten without asking. The name formatted with one name self in the context."""),
558
+ 'lino.modlib.jinja.XMLMaker.xml_file_template' : _("""The name of a Jinja template to render for generating the XML content."""),
559
+ 'lino.modlib.jinja.XMLMaker.xml_validator_file' : _("""The name of a “validator” to use for validating the XML content."""),
555
560
  'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
556
561
  'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
557
562
  'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
@@ -290,7 +290,7 @@ class Comment(
290
290
  def full_clean(self):
291
291
  super().full_clean()
292
292
  if self.reply_to_id and not self.owner_id:
293
- # added only 2023-11-19
293
+ # added only 2023-11-19, that's why we have CommentChecker
294
294
  self.owner = self.reply_to.owner
295
295
  # self.owner.setup_comment(self)
296
296
 
@@ -206,7 +206,7 @@ class CommentsByType(CommentsByX):
206
206
  master_key = "comment_type"
207
207
  column_names = "body created user *"
208
208
 
209
-
209
+ # TODO: rename CommentsByRFC to CommentsByOwner
210
210
  class CommentsByRFC(CommentsByX):
211
211
  master_key = "owner"
212
212
  details_of_master_template = _("%(details)s about %(master)s")
@@ -250,12 +250,12 @@ class Plugin(Plugin):
250
250
  ),
251
251
  url(
252
252
  rx + r"choices/(?P<app_label>\w+)/(?P<rptname>\w+)/"
253
- "(?P<fldname>\w+)$",
253
+ r"(?P<fldname>\w+)$",
254
254
  views.Choices.as_view(),
255
255
  ),
256
256
  url(
257
257
  rx + r"apchoices/(?P<app_label>\w+)/(?P<actor>\w+)/"
258
- "(?P<an>\w+)/(?P<field>\w+)$",
258
+ r"(?P<an>\w+)/(?P<field>\w+)$",
259
259
  views.ActionParamChoices.as_view(),
260
260
  ),
261
261
  # the thread_id can be a negative number: