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 +8 -3
- lino/api/dd.py +13 -0
- lino/api/doctest.py +49 -17
- lino/api/selenium.py +1 -1
- lino/core/actors.py +16 -7
- lino/core/elems.py +1 -1
- lino/core/renderer.py +2 -2
- lino/core/requests.py +27 -1
- lino/core/site.py +5 -5
- lino/help_texts.py +4 -0
- lino/modlib/extjs/__init__.py +2 -2
- lino/modlib/extjs/views.py +37 -12
- lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
- lino/modlib/jinja/mixins.py +62 -0
- lino/modlib/jinja/models.py +6 -0
- lino/modlib/linod/fixtures/__init__.py +0 -0
- lino/modlib/linod/fixtures/linod.py +32 -0
- lino/modlib/linod/mixins.py +5 -3
- lino/modlib/memo/mixins.py +2 -1
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/models.py +1 -1
- lino/modlib/printing/actions.py +6 -12
- lino/modlib/printing/choicelists.py +7 -7
- lino/modlib/uploads/__init__.py +9 -6
- lino/modlib/uploads/models.py +3 -3
- lino/modlib/uploads/ui.py +5 -2
- lino/utils/__init__.py +0 -1
- lino/utils/jscompressor.py +4 -4
- lino/utils/restify.py +2 -2
- lino/utils/soup.py +4 -4
- lino/utils/xml.py +19 -5
- {lino-25.2.2.dist-info → lino-25.2.3.dist-info}/METADATA +1 -1
- {lino-25.2.2.dist-info → lino-25.2.3.dist-info}/RECORD +36 -33
- lino/utils/requests.py +0 -55
- {lino-25.2.2.dist-info → lino-25.2.3.dist-info}/WHEEL +0 -0
- {lino-25.2.2.dist-info → lino-25.2.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.2.2.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.
|
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-
|
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
|
-
|
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
|
-
|
53
|
-
# `lino_welfare.pcsw.models.Client`
|
69
|
+
"""An instance of :class:`django.test.Client`.
|
54
70
|
|
55
|
-
|
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
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
|
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
|
-
|
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.
|
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.
|
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
|
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
|
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 = "
|
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."""),
|
lino/modlib/extjs/__init__.py
CHANGED
@@ -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:
|
lino/modlib/extjs/views.py
CHANGED
@@ -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
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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)
|
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 []
|
lino/modlib/linod/mixins.py
CHANGED
@@ -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
|
-
|
85
|
-
|
86
|
-
|
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:
|
lino/modlib/memo/mixins.py
CHANGED
@@ -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
|
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:
|
lino/modlib/memo/parser.py
CHANGED
lino/modlib/notify/models.py
CHANGED
@@ -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,
|
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.
|