lino 25.2.2__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.
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.2'
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/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
@@ -1638,7 +1663,8 @@ class BaseRequest:
1638
1663
  # print("20190703", self.actor, self.actor.default_action)
1639
1664
  sar = self.spawn_request(actor=self.actor)
1640
1665
  list_title = tostring(sar.href_to_request(sar, list_title, icon_name=None))
1641
- 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))
1642
1668
 
1643
1669
  def form2obj_and_save(ar, data, elem, is_new):
1644
1670
  """
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
@@ -553,6 +553,10 @@ help_texts = {
553
553
  'lino.modlib.gkfs.GenericForeignKey' : _("""Add verbose_name and help_text to Django’s GFK."""),
554
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."""),
555
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."""),
556
560
  'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
557
561
  'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
558
562
  'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
@@ -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:
@@ -27,6 +27,7 @@ import os
27
27
  from pathlib import Path
28
28
 
29
29
  from django import http
30
+ from django.contrib.messages import success
30
31
  from django.db import models
31
32
  from django.conf import settings
32
33
  from django.core.cache import cache
@@ -38,6 +39,7 @@ from django.utils.decorators import method_decorator
38
39
  from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
39
40
  from django.utils.translation import gettext as _
40
41
  from django.utils.encoding import force_str
42
+ from django.utils.html import mark_safe
41
43
  from lino.core import auth
42
44
 
43
45
  from lino.core.signals import pre_ui_delete
@@ -343,19 +345,44 @@ class ApiElement(View):
343
345
  ba = rpt.detail_action
344
346
  if ba is None:
345
347
  raise http.Http404("%s has no detail_action" % rpt)
348
+
349
+ fmt = request.GET.get(constants.URL_PARAM_FORMAT, ba.action.default_format)
350
+
346
351
  try:
347
352
  if pk and pk != "-99999" and pk != "-99998":
348
353
  sr = [pk]
349
- # if issubclass(rpt.model, models.Model):
350
- # try:
351
- # ar = ba.request(request=request, selected_pks=sr)
352
- # # except ObjectDoesNotExist as e: # 20250212
353
- # except rpt.model.DoesNotExist as e:
354
- # # print("20240911", e)
355
- # raise http.Http404(f"Object {sr} does not exist on {rpt}")
356
- # else:
357
- # ar = ba.request(request=request, selected_pks=sr)
358
- ar = ba.request(request=request, selected_pks=sr)
354
+ if issubclass(rpt.model, models.Model):
355
+ try:
356
+ ar = ba.request(request=request, selected_pks=sr)
357
+ # except ObjectDoesNotExist as e: # 20250212
358
+ except rpt.model.DoesNotExist as e:
359
+ if fmt == constants.URL_FORMAT_JSON:
360
+ # rescue_ar: without sr and even request, to render a table request (grid view action) on breadcrumb
361
+ rescue_ar = rpt.request(renderer=settings.SITE.kernel.default_renderer)
362
+ default_table = rpt.model.get_default_table()
363
+
364
+ title = tostring(rescue_ar.href_to_request(rescue_ar, icon_name=None))
365
+ def get_response():
366
+ msg = mark_safe(f'Record (pk={pk}) is no longer available on current table.')
367
+ datarec = dict(success=False, message=msg, title=title)
368
+ datarec.update(**vm)
369
+ return datarec
370
+
371
+ try:
372
+ # take default table and try to show the row
373
+ ar = default_table.detail_action.request(request=request, selected_pks=sr)
374
+ except default_table.model.DoesNotExist as e:
375
+ return json_response(get_response())
376
+
377
+ url = ar.obj2url(ar.selected_rows[0])
378
+ datarec = get_response()
379
+ datarec['message'] += mark_safe(f' Reload in <a href="{url}">{default_table}</a>.')
380
+ return json_response(datarec)
381
+ # print("20240911", e)
382
+ raise http.Http404(f"Object {sr} does not exist on {rpt}")
383
+ else:
384
+ ar = ba.request(request=request, selected_pks=sr)
385
+ # ar = ba.request(request=request, selected_pks=sr)
359
386
  elem = ar.selected_rows[0]
360
387
  # print(
361
388
  # "20170116 views.ApiElement.get", ba,
@@ -398,8 +425,6 @@ class ApiElement(View):
398
425
 
399
426
  # print("20240402 permission", ar, "granted to", ar.get_user(), ar.bound_action.action.select_rows, ar.selected_rows)
400
427
 
401
- fmt = request.GET.get(constants.URL_PARAM_FORMAT, ba.action.default_format)
402
-
403
428
  if ba.action.opens_a_window:
404
429
  if fmt == constants.URL_FORMAT_JSON:
405
430
  if pk == "-99999":
@@ -10,7 +10,7 @@ intersphinx_mapping = {}
10
10
 
11
11
  {% if makehelp.language.index == 0 -%}
12
12
 
13
- html_context = dict(public_url="{{settings.SITE.server_url}}media/cache/help")
13
+ html_context = dict(public_url="{{settings.SITE.server_url}}/media/cache/help")
14
14
 
15
15
  from rstgen.sphinxconf import configure ; configure(globals())
16
16
  from lino.sphinxcontrib import configure ; configure(globals())
@@ -0,0 +1,62 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2022 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from lxml import etree
8
+
9
+ from django.conf import settings
10
+ from django.utils import translation
11
+ from django.utils.html import mark_safe, escape
12
+
13
+ from lino.api import dd
14
+ from lino.utils.xml import validate_xml
15
+
16
+ def xml_element(name, value):
17
+ if value:
18
+ return f"<{name}>{escape(str(value))}</{name}>"
19
+ return ""
20
+
21
+
22
+ class XMLMaker(dd.Model):
23
+
24
+ class Meta:
25
+ abstract = True
26
+
27
+ xml_validator_file = None
28
+ xml_file_template = None
29
+ xml_file_name = None
30
+
31
+ def make_xml_file(self, ar):
32
+ renderer = settings.SITE.plugins.jinja.renderer
33
+ tpl = renderer.jinja_env.get_template(self.xml_file_template)
34
+ context = self.get_printable_context(ar)
35
+ context.update(xml_element=xml_element)
36
+ xml = tpl.render(**context)
37
+ parts = [
38
+ dd.plugins.accounting.xml_media_dir,
39
+ self.xml_file_name.format(self=self)]
40
+ xmlfile = Path(settings.MEDIA_ROOT, *parts)
41
+ ar.logger.info("Make %s from %s ...", xmlfile, self)
42
+ xmlfile.parent.mkdir(exist_ok=True, parents=True)
43
+ xmlfile.write_text(xml)
44
+ # xmlfile.write_text(etree.tostring(xml))
45
+
46
+ if self.xml_validator_file:
47
+ # print("20250218 {xml[:100]}")
48
+ # doc = etree.fromstring(xml.encode("utf-8"))
49
+ ar.logger.info("Validate %s against %s ...", xmlfile.name, self.xml_validator_file)
50
+ if True:
51
+ validate_xml(xmlfile, self.xml_validator_file)
52
+ else:
53
+ try:
54
+ validate_xml(xmlfile, self.xml_validator_file)
55
+ except Exception as e:
56
+ msg = _("XML validation failed: {}").format(e)
57
+ # print(msg)
58
+ raise Warning(msg)
59
+
60
+ url = settings.SITE.build_media_url(*parts)
61
+ # return mark_safe(f"""<a href="{url}">{url}</a>""")
62
+ return (xmlfile, url)
@@ -0,0 +1,6 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from .mixins import XMLMaker
6
+ from .choicelists import JinjaBuildMethod
File without changes
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+ from asgiref.sync import async_to_sync
5
+ from lino.api import rt
6
+ from lino.modlib.linod.mixins import start_task_runner
7
+
8
+ # import logging
9
+ # from lino import logger
10
+ # def getHandlerByName(name):
11
+ # for l in logger.handlers:
12
+ # if l.name == name:
13
+ # return l
14
+
15
+ def objects():
16
+ raise Exception("""
17
+
18
+ This fixture isn't used at the moment. I wrote it because I thought it
19
+ would be nice to run the system task runner automatically when ``pm prep``
20
+ in order to cover the sync_ibanity system task. But (1) this would require
21
+ me to integrate also the ``checkdata`` and ``checksummaries`` fixtures into
22
+ it (otherwise they would run again as a system task) and (2) we don't want
23
+ to start `sync_ibanity` automatically on GitLab because it can't work
24
+ without credentials.
25
+
26
+ """)
27
+ ar = rt.login("robin")
28
+ # logger.setLevel(logging.DEBUG)
29
+ # getHandlerByName('console').setLevel(logging.DEBUG)
30
+ # ar.debug("Coucou")
31
+ async_to_sync(start_task_runner)(ar, max_count=1)
32
+ return []
@@ -81,9 +81,11 @@ class Runnable(Sequenced, RecurrenceSet):
81
81
 
82
82
  def full_clean(self, *args, **kwargs):
83
83
  super().full_clean(*args, **kwargs)
84
- class_name = dd.full_model_name(self.__class__)
85
- if self.procedure.class_name != class_name:
86
- raise ValidationError(f"Invalid procedure for {class_name}")
84
+ # 20250213 The following caused 'Invalid procedure invoicing.Task for
85
+ # linod.SystemTask' during restore.py:
86
+ # class_name = dd.full_model_name(self.__class__)
87
+ # if self.procedure.class_name != class_name:
88
+ # raise ValidationError(f"Invalid procedure {self.procedure.class_name} for {class_name}")
87
89
  if self.every_unit is None:
88
90
  self.every_unit = Recurrences.never
89
91
  if not self.name:
@@ -43,7 +43,8 @@ def rich_text_to_elems(ar, description):
43
43
 
44
44
  # After 20250213 #5929 (Links in the description of a ticket aren't rendered
45
45
  # correctly) we no longer try to automatically detect reSTructuredText
46
- # markup in a RichTextField (anyway nobody has ever used this feature).
46
+ # markup in a RichTextField. Anyway nobody has ever used this feature
47
+ # (except for the furniture fixture of the products plugin).
47
48
 
48
49
  # if description.startswith("<"):
49
50
  if True:
@@ -135,7 +135,7 @@ class Parser:
135
135
  def compile_suggester_regex(self):
136
136
  triggers = "".join(
137
137
  [
138
- "\\" if key in "[\^$.|?*+(){}" else "" + key
138
+ r"\\" if key in r"[\^$.|?*+(){}" else "" + key
139
139
  for key in self.suggesters.keys()
140
140
  ]
141
141
  )
@@ -386,7 +386,7 @@ class MyMessages(My, Messages):
386
386
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
387
387
 
388
388
  @classmethod
389
- def unused_get_table_summary(cls, mi, ar):
389
+ def unused_get_table_summary(cls, ar):
390
390
  # 20240710 Replaced by table_as_summary(), which is now more simple. But
391
391
  # I leave the old version here in case some unexpected regression
392
392
  # occurs.