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.
Files changed (70) hide show
  1. lino/__init__.py +8 -3
  2. lino/api/dd.py +11 -35
  3. lino/api/doctest.py +49 -17
  4. lino/api/selenium.py +1 -1
  5. lino/core/actions.py +25 -23
  6. lino/core/actors.py +52 -23
  7. lino/core/choicelists.py +10 -8
  8. lino/core/dbtables.py +1 -1
  9. lino/core/elems.py +47 -31
  10. lino/core/fields.py +19 -9
  11. lino/core/kernel.py +26 -20
  12. lino/core/model.py +27 -16
  13. lino/core/renderer.py +2 -2
  14. lino/core/requests.py +103 -56
  15. lino/core/site.py +5 -5
  16. lino/core/store.py +5 -2
  17. lino/core/utils.py +12 -7
  18. lino/help_texts.py +7 -8
  19. lino/mixins/duplicable.py +6 -4
  20. lino/mixins/sequenced.py +17 -6
  21. lino/modlib/__init__.py +0 -2
  22. lino/modlib/changes/models.py +21 -10
  23. lino/modlib/checkdata/models.py +59 -24
  24. lino/modlib/comments/fixtures/demo2.py +12 -3
  25. lino/modlib/comments/models.py +7 -7
  26. lino/modlib/comments/ui.py +8 -5
  27. lino/modlib/export_excel/models.py +7 -5
  28. lino/modlib/extjs/__init__.py +2 -2
  29. lino/modlib/extjs/views.py +66 -22
  30. lino/modlib/help/config/makehelp/conf.tpl.py +1 -1
  31. lino/modlib/jinja/mixins.py +73 -0
  32. lino/modlib/jinja/models.py +6 -0
  33. lino/modlib/linod/__init__.py +1 -0
  34. lino/modlib/linod/choicelists.py +21 -0
  35. lino/modlib/linod/consumers.py +13 -4
  36. lino/modlib/linod/fixtures/__init__.py +0 -0
  37. lino/modlib/linod/fixtures/linod.py +32 -0
  38. lino/modlib/linod/management/commands/linod.py +6 -2
  39. lino/modlib/linod/mixins.py +18 -14
  40. lino/modlib/linod/models.py +4 -2
  41. lino/modlib/memo/mixins.py +2 -1
  42. lino/modlib/memo/parser.py +1 -1
  43. lino/modlib/notify/models.py +19 -11
  44. lino/modlib/printing/actions.py +47 -42
  45. lino/modlib/printing/choicelists.py +17 -15
  46. lino/modlib/printing/mixins.py +22 -20
  47. lino/modlib/publisher/models.py +5 -5
  48. lino/modlib/summaries/models.py +3 -2
  49. lino/modlib/system/models.py +28 -29
  50. lino/modlib/uploads/__init__.py +14 -11
  51. lino/modlib/uploads/actions.py +2 -8
  52. lino/modlib/uploads/choicelists.py +10 -10
  53. lino/modlib/uploads/fixtures/std.py +17 -0
  54. lino/modlib/uploads/mixins.py +20 -8
  55. lino/modlib/uploads/models.py +62 -38
  56. lino/modlib/uploads/ui.py +15 -9
  57. lino/utils/__init__.py +0 -1
  58. lino/utils/jscompressor.py +4 -4
  59. lino/utils/media.py +45 -23
  60. lino/utils/report.py +5 -4
  61. lino/utils/restify.py +2 -2
  62. lino/utils/soup.py +26 -8
  63. lino/utils/xml.py +19 -5
  64. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/METADATA +1 -1
  65. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/RECORD +68 -65
  66. lino/mixins/uploadable.py +0 -3
  67. lino/utils/requests.py +0 -55
  68. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/WHEEL +0 -0
  69. {lino-25.2.2.dist-info → lino-25.3.0.dist-info}/licenses/AUTHORS.rst +0 -0
  70. {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-2024 Rumma & Ko Ltd
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 copy, deepcopy
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, iselement
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
- "logger", "show_urls"] )
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
- # self.obvious_fields.update(obvious_fields.split())
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(pk=master_type).model_class()
511
+ master = ContentType.objects.get(
512
+ pk=master_type).model_class()
502
513
  except ContentType.DoesNotExist:
503
- raise exceptions.BadRequest("Invalid master_type {}".format(master_type)) from None
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(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(constants.URL_PARAM_PROJECT, None))
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, self.actor.preview_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, *args, **kwargs: self.logger.warning(*args, **kwargs)
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", subject, sender, recipients)
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
- if ba.action.show_in_plain:
1515
- ir = ba.request_from(self)
1516
- # assert ir.user is self.user
1517
- if ir.get_permission():
1518
- # try:
1519
- # btn = ir.ar2button(**btnattrs)
1520
- # except AttributeError:
1521
- # raise Exception("20200513 {}".format(ir))
1522
- btn = ir.ar2button(**btnattrs)
1523
- # assert iselement(btn)
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(sar, list_title, icon_name=None))
1641
- return list_title + " » " + self.get_detail_title(elem)
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(self.actor.params_layout, self.actor)
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
- raise Exception(
1841
- "Invalid key '%s' in param_values of %s "
1842
- "request (possible keys are %s)"
1843
- % (k, self.actor, list(pv.keys()))
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(action.params_layout.params_store.parse_params(request))
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
- # rather often and since exception loggers usually send an
1980
- # email to the local system admin, make sure to log each
1981
- # exception only once.
1982
- self.no_data_text = f"{e} (set catch_layout_exceptions to see details)"
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(str(e))
2021
+ w = WARNINGS_LOGGED.get(e)
1985
2022
  if w is None:
1986
- WARNINGS_LOGGED[str(e)] = True
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
- "(Subsequent warnings will be silenced.)")
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(self._data_iterator, self)
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(self, self.request.POST or self.rqdata, e, True)
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(self, self.request.POST or self.rqdata, elem, True)
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(self, self.param_values)
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)) + "%" for w in cellwidths]
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, **self.renderer.cellattrs)
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(constants.URL_PARAM_COLUMNS)]
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("hidden", False)]
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) for e in fields]
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 = "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/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("extract_form_data found no relto_model for %s" % self)
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" : ev, f2+"__gte" : sv})
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("No model named '%s.%s'" % (app_label, l[0]))
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)) for k, v in md.items()]
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(renderer=settings.SITE.kernel.text_renderer)
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 name and url."""),
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 actor for the SystemTask model."""),
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. This was named lino.mixins.uploadable.Uploadable until 20210217. It encapsulates some really basic functionality. Its usage is deprecated. If you were inheriting from lino.mixins.Uploadable, you should convert that model to point to an Upload instead."""),
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 = "td"
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(Duplicate, self).get_view_permission(user_type)
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(old=obj, new=new)
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(obj), _("Are you sure?")
112
+ ok, _("This will create a copy of {}.").format(
113
+ obj), _("Are you sure?")
112
114
  )
113
115
 
114
116