lino 25.2.2__py3-none-any.whl → 25.3.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.
- lino/__init__.py +8 -3
- lino/api/dd.py +11 -35
- lino/api/doctest.py +49 -17
- lino/api/selenium.py +1 -1
- lino/core/actions.py +25 -23
- lino/core/actors.py +52 -23
- lino/core/choicelists.py +10 -8
- lino/core/dbtables.py +1 -1
- lino/core/elems.py +47 -31
- lino/core/fields.py +19 -9
- lino/core/kernel.py +26 -20
- lino/core/model.py +27 -16
- lino/core/renderer.py +2 -2
- lino/core/requests.py +103 -56
- lino/core/site.py +5 -5
- lino/core/store.py +5 -2
- lino/core/utils.py +12 -7
- lino/help_texts.py +7 -8
- lino/mixins/duplicable.py +6 -4
- lino/mixins/sequenced.py +17 -6
- lino/modlib/__init__.py +0 -2
- lino/modlib/changes/models.py +21 -10
- lino/modlib/checkdata/models.py +59 -24
- lino/modlib/comments/fixtures/demo2.py +12 -3
- lino/modlib/comments/models.py +7 -7
- lino/modlib/comments/ui.py +8 -5
- lino/modlib/export_excel/models.py +7 -5
- lino/modlib/extjs/__init__.py +2 -2
- lino/modlib/extjs/views.py +66 -22
- lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
- lino/modlib/jinja/mixins.py +73 -0
- lino/modlib/jinja/models.py +6 -0
- lino/modlib/linod/__init__.py +1 -0
- lino/modlib/linod/choicelists.py +21 -0
- lino/modlib/linod/consumers.py +13 -4
- lino/modlib/linod/fixtures/__init__.py +0 -0
- lino/modlib/linod/fixtures/linod.py +32 -0
- lino/modlib/linod/management/commands/linod.py +6 -2
- lino/modlib/linod/mixins.py +18 -14
- lino/modlib/linod/models.py +4 -2
- lino/modlib/memo/mixins.py +2 -1
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/models.py +19 -11
- lino/modlib/printing/actions.py +47 -42
- lino/modlib/printing/choicelists.py +17 -15
- lino/modlib/printing/mixins.py +22 -20
- lino/modlib/publisher/models.py +5 -5
- lino/modlib/summaries/models.py +3 -2
- lino/modlib/system/models.py +28 -29
- lino/modlib/uploads/__init__.py +14 -11
- lino/modlib/uploads/actions.py +2 -8
- lino/modlib/uploads/choicelists.py +10 -10
- lino/modlib/uploads/fixtures/std.py +17 -0
- lino/modlib/uploads/mixins.py +20 -8
- lino/modlib/uploads/models.py +62 -38
- lino/modlib/uploads/ui.py +15 -9
- lino/utils/__init__.py +0 -1
- lino/utils/jscompressor.py +4 -4
- lino/utils/media.py +45 -23
- lino/utils/report.py +5 -4
- lino/utils/restify.py +2 -2
- lino/utils/soup.py +26 -8
- lino/utils/xml.py +19 -5
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/METADATA +1 -1
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/RECORD +68 -65
- lino/mixins/uploadable.py +0 -3
- lino/utils/requests.py +0 -55
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/WHEEL +0 -0
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/COPYING +0 -0
lino/core/requests.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2009-
|
2
|
+
# Copyright 2009-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
"""
|
5
5
|
See introduction in :doc:`/dev/ar` and API reference in
|
@@ -10,13 +10,12 @@ See introduction in :doc:`/dev/ar` and API reference in
|
|
10
10
|
|
11
11
|
import sys
|
12
12
|
import json
|
13
|
-
import html
|
14
13
|
import logging
|
15
14
|
from io import StringIO
|
16
15
|
from lino import logger
|
17
16
|
from contextlib import contextmanager
|
18
17
|
from types import GeneratorType
|
19
|
-
from copy import
|
18
|
+
from copy import deepcopy
|
20
19
|
from xml.sax.saxutils import escape
|
21
20
|
from etgen import html as xghtml
|
22
21
|
|
@@ -24,6 +23,7 @@ from django.conf import settings
|
|
24
23
|
from django.utils.translation import gettext_lazy as _
|
25
24
|
from django.utils.translation import gettext
|
26
25
|
from django.utils.translation import get_language, activate
|
26
|
+
from django.utils.html import format_html
|
27
27
|
from django.utils import translation
|
28
28
|
from django.utils import timezone
|
29
29
|
from django.core.mail import send_mail
|
@@ -33,7 +33,7 @@ from django.db import models
|
|
33
33
|
from django.db.models.query import QuerySet
|
34
34
|
from asgiref.sync import sync_to_async
|
35
35
|
|
36
|
-
from lino.utils.html import E, tostring
|
36
|
+
from lino.utils.html import E, tostring
|
37
37
|
from lino.utils import AttrDict
|
38
38
|
from lino.utils import capture_output
|
39
39
|
from lino.utils import MissingRow
|
@@ -48,11 +48,9 @@ from lino.core.diff import ChangeWatcher
|
|
48
48
|
from lino.core.utils import getrqdata
|
49
49
|
from lino.core.utils import obj2unicode
|
50
50
|
from lino.core.utils import obj2str
|
51
|
-
from lino.core.utils import UnresolvedModel
|
52
51
|
from lino.core.utils import format_request
|
53
52
|
from lino.core.utils import PhantomRow
|
54
53
|
from lino.core.store import get_atomizer
|
55
|
-
from lino.core.exceptions import ChangedAPI
|
56
54
|
|
57
55
|
# try:
|
58
56
|
# from django.contrib.contenttypes.models import ContentType
|
@@ -81,6 +79,7 @@ Subject: {subject}
|
|
81
79
|
def noop(ar):
|
82
80
|
return ar.success(gettext("Aborted"))
|
83
81
|
|
82
|
+
|
84
83
|
def mi2bp(master_instance, bp):
|
85
84
|
if master_instance is not None:
|
86
85
|
if isinstance(master_instance, (models.Model, TableRow)):
|
@@ -103,6 +102,17 @@ def mi2bp(master_instance, bp):
|
|
103
102
|
# self.master_instance.__class__, self.master_instance))
|
104
103
|
|
105
104
|
|
105
|
+
class PrintLogger(logging.Logger):
|
106
|
+
format = "%(message)s"
|
107
|
+
|
108
|
+
def __init__(self, level):
|
109
|
+
super().__init__("~", level)
|
110
|
+
h = logging.StreamHandler(stream=sys.stdout)
|
111
|
+
h.setFormatter(logging.Formatter(self.format))
|
112
|
+
self.addHandler(h)
|
113
|
+
# print("20231012", logging.getLevelName(self.level), self.__class__)
|
114
|
+
|
115
|
+
|
106
116
|
class StringLogger(logging.Logger):
|
107
117
|
# Instantiated by BaseRequest.capture_logger()
|
108
118
|
|
@@ -260,7 +270,7 @@ class ValidActionResponses(object):
|
|
260
270
|
|
261
271
|
inheritable_attrs = frozenset(
|
262
272
|
["user", "subst_user", "renderer", "requesting_panel", "master_instance",
|
263
|
-
|
273
|
+
"logger", "show_urls"])
|
264
274
|
|
265
275
|
|
266
276
|
def bool2text(x):
|
@@ -269,7 +279,6 @@ def bool2text(x):
|
|
269
279
|
return _("No")
|
270
280
|
|
271
281
|
|
272
|
-
|
273
282
|
class SearchQuerySet:
|
274
283
|
pass
|
275
284
|
|
@@ -429,7 +438,7 @@ class BaseRequest:
|
|
429
438
|
self.known_values.setdefault(k, v)
|
430
439
|
|
431
440
|
self.obvious_fields |= self.actor.obvious_fields
|
432
|
-
|
441
|
+
# self.obvious_fields.update(obvious_fields.split())
|
433
442
|
# if parent.obvious_fields is not None:
|
434
443
|
if parent is not None and parent.actor is self.actor and parent.obvious_fields:
|
435
444
|
self.obvious_fields |= parent.obvious_fields
|
@@ -442,7 +451,6 @@ class BaseRequest:
|
|
442
451
|
self.actor, locals()
|
443
452
|
)
|
444
453
|
)
|
445
|
-
# if isinstance(self.master_instance, UnresolvedModel):
|
446
454
|
if isinstance(self.master_instance, MissingRow):
|
447
455
|
raise exceptions.BadRequest(self.master_instance)
|
448
456
|
# raise Exception(
|
@@ -454,6 +462,7 @@ class BaseRequest:
|
|
454
462
|
user=None,
|
455
463
|
subst_user=None,
|
456
464
|
current_project=None,
|
465
|
+
display_mode=None,
|
457
466
|
master=None,
|
458
467
|
master_instance=None,
|
459
468
|
master_key=None,
|
@@ -483,6 +492,7 @@ class BaseRequest:
|
|
483
492
|
else:
|
484
493
|
self.user = user
|
485
494
|
self.current_project = current_project
|
495
|
+
self.display_mode = display_mode
|
486
496
|
if renderer is None:
|
487
497
|
renderer = settings.SITE.kernel.text_renderer
|
488
498
|
# renderer = settings.SITE.kernel.default_renderer
|
@@ -498,9 +508,11 @@ class BaseRequest:
|
|
498
508
|
master = self.actor.master
|
499
509
|
if master_type and ContentType is not None:
|
500
510
|
try:
|
501
|
-
master = ContentType.objects.get(
|
511
|
+
master = ContentType.objects.get(
|
512
|
+
pk=master_type).model_class()
|
502
513
|
except ContentType.DoesNotExist:
|
503
|
-
raise exceptions.BadRequest(
|
514
|
+
raise exceptions.BadRequest(
|
515
|
+
"Invalid master_type {}".format(master_type)) from None
|
504
516
|
self.master = master
|
505
517
|
|
506
518
|
if self.master is not None and self.master_instance is None:
|
@@ -509,7 +521,8 @@ class BaseRequest:
|
|
509
521
|
master_instance = self.get_master_instance(
|
510
522
|
self.master, master_key, master_type
|
511
523
|
)
|
512
|
-
self.master_instance = self.actor.cast_master_instance(
|
524
|
+
self.master_instance = self.actor.cast_master_instance(
|
525
|
+
master_instance)
|
513
526
|
|
514
527
|
self.row_meta = dict(meta=True)
|
515
528
|
|
@@ -535,12 +548,26 @@ class BaseRequest:
|
|
535
548
|
try:
|
536
549
|
# We instantiate a temporary Logger object, which is not known by the
|
537
550
|
# root logger.
|
551
|
+
# self.logger = PrintLogger(level)
|
538
552
|
self.logger = StringLogger(old_logger, level)
|
539
553
|
# self.logger.parent =
|
540
554
|
yield self.logger
|
541
555
|
|
542
556
|
finally:
|
543
557
|
self.logger.streamer.flush()
|
558
|
+
# print(self.logger.getvalue())
|
559
|
+
self.logger = old_logger
|
560
|
+
|
561
|
+
@contextmanager
|
562
|
+
def print_logger(self, level=logging.INFO):
|
563
|
+
old_logger = self.logger
|
564
|
+
try:
|
565
|
+
# We instantiate a temporary Logger object, which is not known by the
|
566
|
+
# root logger.
|
567
|
+
self.logger = PrintLogger(level)
|
568
|
+
yield None
|
569
|
+
|
570
|
+
finally:
|
544
571
|
self.logger = old_logger
|
545
572
|
|
546
573
|
@contextmanager
|
@@ -617,7 +644,10 @@ class BaseRequest:
|
|
617
644
|
kw.update(user=request.user)
|
618
645
|
kw.update(subst_user=request.subst_user)
|
619
646
|
kw.update(requesting_panel=request.requesting_panel)
|
620
|
-
kw.update(current_project=rqdata.get(
|
647
|
+
kw.update(current_project=rqdata.get(
|
648
|
+
constants.URL_PARAM_PROJECT, None))
|
649
|
+
kw.update(display_mode=rqdata.get(
|
650
|
+
constants.URL_PARAM_DISPLAY_MODE, None))
|
621
651
|
|
622
652
|
# If the incoming request specifies an active tab, then the
|
623
653
|
# response must forward this information. Otherwise Lino would
|
@@ -688,7 +718,8 @@ class BaseRequest:
|
|
688
718
|
offset = rqdata.get(constants.URL_PARAM_START, None)
|
689
719
|
if offset:
|
690
720
|
kw.update(offset=int(offset))
|
691
|
-
limit = rqdata.get(constants.URL_PARAM_LIMIT,
|
721
|
+
limit = rqdata.get(constants.URL_PARAM_LIMIT,
|
722
|
+
self.actor.preview_limit)
|
692
723
|
if limit:
|
693
724
|
kw.update(limit=int(limit))
|
694
725
|
except ValueError:
|
@@ -982,7 +1013,8 @@ class BaseRequest:
|
|
982
1013
|
|
983
1014
|
debug = lambda self, *args, **kwargs: self.logger.debug(*args, **kwargs)
|
984
1015
|
info = lambda self, *args, **kwargs: self.logger.info(*args, **kwargs)
|
985
|
-
warning = lambda self, *
|
1016
|
+
warning = lambda self, * \
|
1017
|
+
args, **kwargs: self.logger.warning(*args, **kwargs)
|
986
1018
|
|
987
1019
|
async def adebug(self, *args, **kwargs):
|
988
1020
|
return await self.alogger.debug(*args, **kwargs)
|
@@ -1018,7 +1050,8 @@ class BaseRequest:
|
|
1018
1050
|
)
|
1019
1051
|
return
|
1020
1052
|
|
1021
|
-
self.logger.info("Send email '%s' from %s to %s",
|
1053
|
+
self.logger.info("Send email '%s' from %s to %s",
|
1054
|
+
subject, sender, recipients)
|
1022
1055
|
|
1023
1056
|
recipients = [a for a in recipients if "@example.com" not in a]
|
1024
1057
|
if not len(recipients):
|
@@ -1510,18 +1543,17 @@ class BaseRequest:
|
|
1510
1543
|
if cls is not None:
|
1511
1544
|
user_type = self.get_user().user_type
|
1512
1545
|
for ba in cls.get_toolbar_actions(self.bound_action.action, user_type):
|
1513
|
-
if not ba.action.select_rows:
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
buttons.append(btn)
|
1546
|
+
if ba.action.show_in_plain and not ba.action.select_rows:
|
1547
|
+
ir = ba.request_from(self)
|
1548
|
+
# assert ir.user is self.user
|
1549
|
+
if ir.get_permission():
|
1550
|
+
# try:
|
1551
|
+
# btn = ir.ar2button(**btnattrs)
|
1552
|
+
# except AttributeError:
|
1553
|
+
# raise Exception("20200513 {}".format(ir))
|
1554
|
+
btn = ir.ar2button(**btnattrs)
|
1555
|
+
# assert iselement(btn)
|
1556
|
+
buttons.append(btn)
|
1525
1557
|
# print("20181106", cls, self.bound_action, buttons)
|
1526
1558
|
return buttons
|
1527
1559
|
# if len(buttons) == 0:
|
@@ -1637,8 +1669,10 @@ class BaseRequest:
|
|
1637
1669
|
else:
|
1638
1670
|
# print("20190703", self.actor, self.actor.default_action)
|
1639
1671
|
sar = self.spawn_request(actor=self.actor)
|
1640
|
-
list_title = tostring(sar.href_to_request(
|
1641
|
-
|
1672
|
+
list_title = tostring(sar.href_to_request(
|
1673
|
+
sar, list_title, icon_name=None))
|
1674
|
+
# return list_title + " » " + self.get_detail_title(elem)
|
1675
|
+
return format_html("{} » {}", list_title, self.get_detail_title(elem))
|
1642
1676
|
|
1643
1677
|
def form2obj_and_save(ar, data, elem, is_new):
|
1644
1678
|
"""
|
@@ -1832,16 +1866,17 @@ class ActionRequest(BaseRequest):
|
|
1832
1866
|
else:
|
1833
1867
|
raise Exception(
|
1834
1868
|
"20160329 params_layout {0} has no params_store "
|
1835
|
-
"in {1!r}".format(
|
1869
|
+
"in {1!r}".format(
|
1870
|
+
self.actor.params_layout, self.actor)
|
1836
1871
|
)
|
1837
1872
|
else:
|
1838
1873
|
for k in param_values.keys():
|
1839
1874
|
if k not in pv:
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1843
|
-
|
1844
|
-
)
|
1875
|
+
msg = ("Invalid key '%s' in param_values of %s "
|
1876
|
+
"request (possible keys are %s)"
|
1877
|
+
% (k, self.actor, list(pv.keys())))
|
1878
|
+
# print(msg) # 20250309
|
1879
|
+
raise Exception(msg)
|
1845
1880
|
pv.update(param_values)
|
1846
1881
|
# print("20160329 ok", pv)
|
1847
1882
|
self.param_values = AttrDict(**pv)
|
@@ -1857,7 +1892,8 @@ class ActionRequest(BaseRequest):
|
|
1857
1892
|
# self.selected_rows, action)
|
1858
1893
|
# raise Exception(msg)
|
1859
1894
|
if request is not None:
|
1860
|
-
apv.update(
|
1895
|
+
apv.update(
|
1896
|
+
action.params_layout.params_store.parse_params(request))
|
1861
1897
|
self.action_param_values = AttrDict(**apv)
|
1862
1898
|
# action.check_params(action_param_values)
|
1863
1899
|
self.set_action_param_values(**action_param_values)
|
@@ -1975,19 +2011,20 @@ class ActionRequest(BaseRequest):
|
|
1975
2011
|
except Exception as e:
|
1976
2012
|
if not settings.SITE.catch_layout_exceptions:
|
1977
2013
|
raise
|
1978
|
-
# Report this exception. But since such errors may occur
|
1979
|
-
#
|
1980
|
-
#
|
1981
|
-
|
1982
|
-
self.no_data_text = f"{e} (set catch_layout_exceptions to see
|
2014
|
+
# Report this exception. But since such errors may occur rather
|
2015
|
+
# often and since exception loggers usually send an email to the
|
2016
|
+
# local system admin, make sure to log each exception only once.
|
2017
|
+
e = str(e)
|
2018
|
+
self.no_data_text = f"{e} (set catch_layout_exceptions to see \
|
2019
|
+
details)"
|
1983
2020
|
self._data_iterator = []
|
1984
|
-
w = WARNINGS_LOGGED.get(
|
2021
|
+
w = WARNINGS_LOGGED.get(e)
|
1985
2022
|
if w is None:
|
1986
|
-
WARNINGS_LOGGED[
|
2023
|
+
WARNINGS_LOGGED[e] = True
|
1987
2024
|
# raise
|
1988
2025
|
# logger.exception(e)
|
1989
2026
|
logger.warning(f"Error while executing {repr(self)}: {e}\n"
|
1990
|
-
|
2027
|
+
"(Subsequent warnings will be silenced.)")
|
1991
2028
|
|
1992
2029
|
if self._data_iterator is None:
|
1993
2030
|
raise Exception(f"No data iterator for {self}")
|
@@ -1997,7 +2034,8 @@ class ActionRequest(BaseRequest):
|
|
1997
2034
|
self._data_iterator = tuple(self._data_iterator)
|
1998
2035
|
if isinstance(self._data_iterator, (SearchQuery, SearchQuerySet)):
|
1999
2036
|
self._sliced_data_iterator = tuple(
|
2000
|
-
self.actor.get_rows_from_search_query(
|
2037
|
+
self.actor.get_rows_from_search_query(
|
2038
|
+
self._data_iterator, self)
|
2001
2039
|
)
|
2002
2040
|
else:
|
2003
2041
|
self._sliced_data_iterator = sliced_data_iterator(
|
@@ -2121,7 +2159,8 @@ class ActionRequest(BaseRequest):
|
|
2121
2159
|
|
2122
2160
|
if self.request is not None:
|
2123
2161
|
for e in elems:
|
2124
|
-
self.ah.store.form2obj(
|
2162
|
+
self.ah.store.form2obj(
|
2163
|
+
self, self.request.POST or self.rqdata, e, True)
|
2125
2164
|
for e in elems:
|
2126
2165
|
e.full_clean()
|
2127
2166
|
return elems
|
@@ -2132,7 +2171,8 @@ class ActionRequest(BaseRequest):
|
|
2132
2171
|
self.actor.handle_uploaded_files(elem, self.request)
|
2133
2172
|
|
2134
2173
|
if self.request is not None:
|
2135
|
-
self.ah.store.form2obj(
|
2174
|
+
self.ah.store.form2obj(
|
2175
|
+
self, self.request.POST or self.rqdata, elem, True)
|
2136
2176
|
elem.full_clean()
|
2137
2177
|
return elem
|
2138
2178
|
|
@@ -2145,7 +2185,8 @@ class ActionRequest(BaseRequest):
|
|
2145
2185
|
if self._status is not None and not kw:
|
2146
2186
|
return self._status
|
2147
2187
|
if self.actor.parameters:
|
2148
|
-
pv = self.actor.params_layout.params_store.pv2dict(
|
2188
|
+
pv = self.actor.params_layout.params_store.pv2dict(
|
2189
|
+
self, self.param_values)
|
2149
2190
|
# print(f"20250121c {self}\n{self.actor.params_layout.params_store.__class__}\n{self.actor.params_layout.params_store.param_fields}")
|
2150
2191
|
# print(f"20250121c {self} {self.param_values.keys()}\n{self.actor.params_layout}\n{pv.keys()}")
|
2151
2192
|
# print(f"20250121c {self}|{self.actor.params_layout.params_store}\n{}")
|
@@ -2155,6 +2196,8 @@ class ActionRequest(BaseRequest):
|
|
2155
2196
|
bp = kw.setdefault("base_params", {})
|
2156
2197
|
if self.current_project is not None:
|
2157
2198
|
bp[constants.URL_PARAM_PROJECT] = self.current_project
|
2199
|
+
if self.display_mode is not None:
|
2200
|
+
bp[constants.URL_PARAM_DISPLAY_MODE] = self.display_mode
|
2158
2201
|
if self.subst_user is not None:
|
2159
2202
|
# raise Exception("20230331")
|
2160
2203
|
bp[constants.URL_PARAM_SUBST_USER] = self.subst_user.id
|
@@ -2258,7 +2301,6 @@ class ActionRequest(BaseRequest):
|
|
2258
2301
|
# ~ s = self.get_title()
|
2259
2302
|
# ~ return s.encode('us-ascii','replace')
|
2260
2303
|
|
2261
|
-
|
2262
2304
|
def to_rst(self, *args, **kw):
|
2263
2305
|
"""Returns a string representing this table request in
|
2264
2306
|
reStructuredText markup.
|
@@ -2347,7 +2389,8 @@ class ActionRequest(BaseRequest):
|
|
2347
2389
|
# if cellwidths and self.renderer.is_interactive:
|
2348
2390
|
if cellwidths:
|
2349
2391
|
totwidth = sum([int(w) for w in cellwidths])
|
2350
|
-
widths = [str(int(int(w) * 100 / totwidth))
|
2392
|
+
widths = [str(int(int(w) * 100 / totwidth))
|
2393
|
+
+ "%" for w in cellwidths]
|
2351
2394
|
for i, td in enumerate(headers):
|
2352
2395
|
# td.set('width', six.text_type(cellwidths[i]))
|
2353
2396
|
td.set("width", widths[i])
|
@@ -2355,7 +2398,8 @@ class ActionRequest(BaseRequest):
|
|
2355
2398
|
# ~ print 20120623, ar.actor
|
2356
2399
|
recno = 0
|
2357
2400
|
for obj in data_iterator:
|
2358
|
-
cells = ar.row2html(recno, columns, obj, sums,
|
2401
|
+
cells = ar.row2html(recno, columns, obj, sums,
|
2402
|
+
**self.renderer.cellattrs)
|
2359
2403
|
if cells is not None:
|
2360
2404
|
recno += 1
|
2361
2405
|
tble.body.append(xghtml.E.tr(*cells))
|
@@ -2394,7 +2438,8 @@ class ActionRequest(BaseRequest):
|
|
2394
2438
|
columns = None
|
2395
2439
|
else:
|
2396
2440
|
data = getrqdata(ar.request)
|
2397
|
-
columns = [str(x) for x in data.getlist(
|
2441
|
+
columns = [str(x) for x in data.getlist(
|
2442
|
+
constants.URL_PARAM_COLUMNS)]
|
2398
2443
|
if columns:
|
2399
2444
|
all_widths = data.getlist(constants.URL_PARAM_WIDTHS)
|
2400
2445
|
hiddens = [
|
@@ -2438,10 +2483,12 @@ class ActionRequest(BaseRequest):
|
|
2438
2483
|
# except AttributeError as ex:
|
2439
2484
|
# raise AttributeError("20160529 %s : %s" % (e, ex))
|
2440
2485
|
#
|
2441
|
-
fields = [e for e in fields if not e.value.get(
|
2486
|
+
fields = [e for e in fields if not e.value.get(
|
2487
|
+
"hidden", False)]
|
2442
2488
|
fields = [e for e in fields if not e.hidden]
|
2443
2489
|
|
2444
|
-
widths = ["%d" % (e.width or e.preferred_width)
|
2490
|
+
widths = ["%d" % (e.width or e.preferred_width)
|
2491
|
+
for e in fields]
|
2445
2492
|
columns = [e.name for e in fields]
|
2446
2493
|
|
2447
2494
|
headers = [column_header(col) for col in fields]
|
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/core/store.py
CHANGED
@@ -343,7 +343,8 @@ class ForeignKeyStoreField(RelatedMixin, ComboStoreField):
|
|
343
343
|
|
344
344
|
relto_model = self.get_rel_to(obj)
|
345
345
|
if not relto_model:
|
346
|
-
raise Warning(
|
346
|
+
raise Warning(
|
347
|
+
"extract_form_data found no relto_model for %s" % self)
|
347
348
|
# logger.info("20111209 get_value_text: no relto_model")
|
348
349
|
# return
|
349
350
|
|
@@ -1134,7 +1135,7 @@ class ParameterStore(BaseStore):
|
|
1134
1135
|
data = getrqdata(request)
|
1135
1136
|
# print(20160329, data)
|
1136
1137
|
# assert 'pv' in data
|
1137
|
-
pv = data.getlist(self.url_param) #'fv', 'pv', post[fn] post[fv][fn]
|
1138
|
+
pv = data.getlist(self.url_param) # 'fv', 'pv', post[fn] post[fv][fn]
|
1138
1139
|
|
1139
1140
|
# logger.info("20120221 ParameterStore.parse_params(%s) --> %s",self.url_param,pv)
|
1140
1141
|
|
@@ -1220,6 +1221,8 @@ class Store(BaseStore):
|
|
1220
1221
|
|
1221
1222
|
form = rh.actor.insert_layout
|
1222
1223
|
if form:
|
1224
|
+
if isinstance(form, str):
|
1225
|
+
raise Exception(f"20250306 insert_layout {repr(rh.actor)}")
|
1223
1226
|
dh = form.get_layout_handle()
|
1224
1227
|
self.collect_fields(self.detail_fields, dh)
|
1225
1228
|
|
lino/core/utils.py
CHANGED
@@ -6,6 +6,8 @@ A collection of utilities which require Django settings to be
|
|
6
6
|
importable.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from .exceptions import ChangedAPI
|
10
|
+
from lino.utils import IncompleteDate
|
9
11
|
import copy
|
10
12
|
import sys
|
11
13
|
import datetime
|
@@ -32,8 +34,6 @@ from django.apps import apps
|
|
32
34
|
|
33
35
|
get_models = apps.get_models
|
34
36
|
|
35
|
-
from lino.utils import IncompleteDate
|
36
|
-
from .exceptions import ChangedAPI
|
37
37
|
|
38
38
|
validate_url = URLValidator()
|
39
39
|
|
@@ -311,6 +311,7 @@ def range_filter(value, f1, f2):
|
|
311
311
|
q2 = Q(**{f2 + "__isnull": True}) | Q(**{f2 + "__gte": value})
|
312
312
|
return Q(q1, q2)
|
313
313
|
|
314
|
+
|
314
315
|
def inrange_filter(fld, rng, **kw):
|
315
316
|
"""Assuming a database model with a field named `fld`, return a Q
|
316
317
|
object to select the rows having value for `fld` within the given range `rng`.
|
@@ -343,7 +344,7 @@ def overlap_range_filter(sv, ev, f1, f2, **kw):
|
|
343
344
|
# raise ValueError(f"{rng} is not a valid range")
|
344
345
|
if not ev:
|
345
346
|
ev = sv
|
346
|
-
return Q(**{f1+"__lte"
|
347
|
+
return Q(**{f1+"__lte": ev, f2+"__gte": sv})
|
347
348
|
|
348
349
|
|
349
350
|
def babelkw(*args, **kw):
|
@@ -544,7 +545,8 @@ def resolve_field(name, app_label=None):
|
|
544
545
|
if len(l) == 2:
|
545
546
|
model = apps.get_model(app_label, l[0])
|
546
547
|
if model is None:
|
547
|
-
raise FieldDoesNotExist(
|
548
|
+
raise FieldDoesNotExist(
|
549
|
+
"No model named '%s.%s'" % (app_label, l[0]))
|
548
550
|
return model._meta.get_field(l[1])
|
549
551
|
# fld, remote_model, direct, m2m = model._meta.get_field_by_name(l[1])
|
550
552
|
# assert remote_model is None or issubclass(model, remote_model), \
|
@@ -812,7 +814,8 @@ def error2str(self, e):
|
|
812
814
|
return str(getattr(de, "verbose_name", name))
|
813
815
|
|
814
816
|
return "\n".join(
|
815
|
-
["%s : %s" % (fieldlabel(k), self.error2str(v))
|
817
|
+
["%s : %s" % (fieldlabel(k), self.error2str(v))
|
818
|
+
for k, v in md.items()]
|
816
819
|
)
|
817
820
|
return "\n".join(e.messages)
|
818
821
|
return str(e)
|
@@ -1067,7 +1070,8 @@ class InstanceAction:
|
|
1067
1070
|
"""
|
1068
1071
|
if len(args) and isinstance(args[0], BaseRequest):
|
1069
1072
|
raise ChangedAPI("20181004")
|
1070
|
-
ar = self.bound_action.request(
|
1073
|
+
ar = self.bound_action.request(
|
1074
|
+
renderer=settings.SITE.kernel.text_renderer)
|
1071
1075
|
self.run_from_code(ar, *args, **kwargs)
|
1072
1076
|
return ar.response
|
1073
1077
|
|
@@ -1114,7 +1118,6 @@ class PhantomRow(VirtualRow):
|
|
1114
1118
|
return str(self._ar.get_action_title())
|
1115
1119
|
|
1116
1120
|
|
1117
|
-
|
1118
1121
|
def login(username=None, **kwargs):
|
1119
1122
|
"""Return a basic :term:`action request` with the specified user signed in.
|
1120
1123
|
"""
|
@@ -1131,10 +1134,12 @@ def login(username=None, **kwargs):
|
|
1131
1134
|
# import lino.core.urls # hack: trigger ui instantiation
|
1132
1135
|
return BaseRequest(**kwargs)
|
1133
1136
|
|
1137
|
+
|
1134
1138
|
def show(*args, **kwargs):
|
1135
1139
|
"""Print the specified data table to stdout."""
|
1136
1140
|
return login().show(*args, **kwargs)
|
1137
1141
|
|
1142
|
+
|
1138
1143
|
def shows(*args, **kwargs):
|
1139
1144
|
"""Return the output of :func:`show`."""
|
1140
1145
|
return capture_output(show, *args, **kwargs)
|
lino/help_texts.py
CHANGED
@@ -171,10 +171,6 @@ help_texts = {
|
|
171
171
|
'lino.modlib.tinymce.Plugin.window_buttons2' : _("""The second row of toolbar buttons when editing in own window."""),
|
172
172
|
'lino.modlib.tinymce.Plugin.window_buttons3' : _("""The third row of toolbar buttons when editing in own window."""),
|
173
173
|
'lino.modlib.tinymce.Plugin.media_name' : _("""Lino currently includes three versions of TinyMCE, but for production sites we still use the eldest version 3.4.8."""),
|
174
|
-
'lino.modlib.uploads.Plugin' : _("""See /dev/plugins."""),
|
175
|
-
'lino.modlib.uploads.Plugin.remove_orphaned_files' : _("""Whether checkdata –fix should automatically delete orphaned files in the uploads folder."""),
|
176
|
-
'lino.modlib.uploads.Plugin.with_thumbnails' : _("""Whether to use PIL, the Python Imaging Library."""),
|
177
|
-
'lino.modlib.uploads.Plugin.with_volumes' : _("""Whether to use library files (volumes)."""),
|
178
174
|
'lino.modlib.weasyprint.Plugin' : _("""See /dev/plugins."""),
|
179
175
|
'lino.modlib.weasyprint.Plugin.header_height' : _("""Height of header in mm. Set to None if you want no header."""),
|
180
176
|
'lino.modlib.weasyprint.Plugin.footer_height' : _("""Height of footer in mm. Set to None if you want no header."""),
|
@@ -262,8 +258,7 @@ help_texts = {
|
|
262
258
|
'lino.utils.jsgen.Component.walk' : _("""Walk over this component and its children."""),
|
263
259
|
'lino.utils.jsgen.VisibleComponent' : _("""A visible component"""),
|
264
260
|
'lino.utils.jsgen.VisibleComponent.install_permission_handler' : _("""Define the allow_read handler used by get_view_permission(). This must be done only once, but after having configured debug_permissions and required_roles."""),
|
265
|
-
'lino.utils.media.MediaFile' : _("""Represents a file on the server below MEDIA_ROOT with two properties
|
266
|
-
'lino.utils.media.MediaFile.get_url' : _("""return the url that points to file on the server"""),
|
261
|
+
'lino.utils.media.MediaFile' : _("""Represents a file on the server below MEDIA_ROOT with two properties path and url."""),
|
267
262
|
'lino.utils.mldbc.fields.BabelCharField' : _("""Define a variable number of CharField database fields, one for each language of your lino.core.site.Site.languages. See mldbc."""),
|
268
263
|
'lino.utils.mldbc.fields.BabelTextField' : _("""Used for the clones of the master field, one for each non-default language. See mldbc."""),
|
269
264
|
'lino.utils.mldbc.fields.LanguageField' : _("""A field that lets the user select a language from the available lino.core.site.Site.languages."""),
|
@@ -379,7 +374,7 @@ help_texts = {
|
|
379
374
|
'lino.modlib.linod.SystemTask.disabled' : _("""Tells whether the task should be ignored."""),
|
380
375
|
'lino.modlib.linod.SystemTask.log_level' : _("""The logging level to apply when running this task."""),
|
381
376
|
'lino.modlib.linod.SystemTask.run' : _("""Performs a routine job."""),
|
382
|
-
'lino.modlib.linod.SystemTasks' : _("""The default
|
377
|
+
'lino.modlib.linod.SystemTasks' : _("""The default table for the SystemTask model."""),
|
383
378
|
'lino.modlib.linod.Runnable' : _("""Model mixin used by SystemTask and other models."""),
|
384
379
|
'lino.modlib.linod.Runnable.procedure' : _("""The background procedure to run in this task."""),
|
385
380
|
'lino.modlib.periods.StoredYear' : _("""The Django model used to store a fiscal year."""),
|
@@ -553,6 +548,10 @@ help_texts = {
|
|
553
548
|
'lino.modlib.gkfs.GenericForeignKey' : _("""Add verbose_name and help_text to Django’s GFK."""),
|
554
549
|
'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
550
|
'lino.modlib.jinja.JinjaBuildMethod' : _("""Inherits from lino.modlib.printing.DjangoBuildMethod."""),
|
551
|
+
'lino.modlib.jinja.XMLMaker' : _("""Usage example in /topics/xml"""),
|
552
|
+
'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."""),
|
553
|
+
'lino.modlib.jinja.XMLMaker.xml_file_template' : _("""The name of a Jinja template to render for generating the XML content."""),
|
554
|
+
'lino.modlib.jinja.XMLMaker.xml_validator_file' : _("""The name of a “validator” to use for validating the XML content."""),
|
556
555
|
'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
|
557
556
|
'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
|
558
557
|
'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
|
@@ -686,7 +685,7 @@ help_texts = {
|
|
686
685
|
'lino.modlib.uploads.AllUploads' : _("""Shows all upload files on this Lino site."""),
|
687
686
|
'lino.modlib.uploads.AreaUploads' : _("""Mixin for tables of upload files where the upload area is known."""),
|
688
687
|
'lino.modlib.uploads.MyUploads' : _("""Shows my uploads (i.e. those whose author is the requesting user)."""),
|
689
|
-
'lino.modlib.uploads.UploadBase' : _("""Abstract base class of Upload
|
688
|
+
'lino.modlib.uploads.UploadBase' : _("""Abstract base class of Upload encapsulating some really basic functionality."""),
|
690
689
|
'lino.modlib.uploads.UploadType' : _("""Django model representing an upload type."""),
|
691
690
|
'lino.modlib.uploads.UploadType.shortcut' : _("""Optional pointer to a virtual upload shortcut field. If this is not empty, then the given shortcut field will manage uploads of this type. See also Shortcuts."""),
|
692
691
|
'lino.modlib.uploads.UploadTypes' : _("""The table with all existing upload types."""),
|
lino/mixins/duplicable.py
CHANGED
@@ -29,7 +29,7 @@ class Duplicate(actions.Action):
|
|
29
29
|
sort_index = 11
|
30
30
|
show_in_workflow = False
|
31
31
|
# readonly = False # like ShowInsert. See docs/blog/2012/0726
|
32
|
-
callable_from = "
|
32
|
+
callable_from = "t"
|
33
33
|
|
34
34
|
# required_roles = set([Expert])
|
35
35
|
|
@@ -42,7 +42,7 @@ class Duplicate(actions.Action):
|
|
42
42
|
return False
|
43
43
|
# if not user_type.has_required_roles([Expert]):
|
44
44
|
# return False
|
45
|
-
return super(
|
45
|
+
return super().get_view_permission(user_type)
|
46
46
|
|
47
47
|
def run_from_code(self, ar, **known_values):
|
48
48
|
obj = ar.selected_rows[0]
|
@@ -97,7 +97,8 @@ class Duplicate(actions.Action):
|
|
97
97
|
kw = dict()
|
98
98
|
# kw.update(refresh=True)
|
99
99
|
kw.update(
|
100
|
-
message=_("Duplicated %(old)s to %(new)s.") % dict(
|
100
|
+
message=_("Duplicated %(old)s to %(new)s.") % dict(
|
101
|
+
old=obj, new=new)
|
101
102
|
)
|
102
103
|
# ~ kw.update(new_status=dict(record_id=new.pk))
|
103
104
|
ar2.success(**kw)
|
@@ -108,7 +109,8 @@ class Duplicate(actions.Action):
|
|
108
109
|
|
109
110
|
obj = ar.selected_rows[0]
|
110
111
|
ar.confirm(
|
111
|
-
ok, _("This will create a copy of {}.").format(
|
112
|
+
ok, _("This will create a copy of {}.").format(
|
113
|
+
obj), _("Are you sure?")
|
112
114
|
)
|
113
115
|
|
114
116
|
|