lino 25.3.0__py3-none-any.whl → 25.3.1__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 (39) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +34 -36
  3. lino/core/inject.py +7 -6
  4. lino/core/kernel.py +0 -46
  5. lino/core/model.py +17 -15
  6. lino/core/plugin.py +4 -4
  7. lino/core/site.py +84 -30
  8. lino/management/commands/prep.py +1 -1
  9. lino/modlib/help/management/commands/makehelp.py +5 -2
  10. lino/modlib/jinja/mixins.py +2 -2
  11. lino/modlib/linod/mixins.py +3 -0
  12. lino/modlib/printing/mixins.py +3 -0
  13. lino/modlib/uploads/models.py +7 -6
  14. {lino-25.3.0.dist-info → lino-25.3.1.dist-info}/METADATA +1 -1
  15. {lino-25.3.0.dist-info → lino-25.3.1.dist-info}/RECORD +18 -39
  16. lino/sandbox/bcss/PerformInvestigation.py +0 -2260
  17. lino/sandbox/bcss/SSDNReply.py +0 -3924
  18. lino/sandbox/bcss/SSDNRequest.py +0 -3723
  19. lino/sandbox/bcss/__init__.py +0 -0
  20. lino/sandbox/bcss/readme.txt +0 -1
  21. lino/sandbox/bcss/test.py +0 -92
  22. lino/sandbox/bcss/test2.py +0 -128
  23. lino/sandbox/bcss/test3.py +0 -161
  24. lino/sandbox/bcss/test4.py +0 -167
  25. lino/sandbox/contacts/__init__.py +0 -0
  26. lino/sandbox/contacts/fixtures/__init__.py +0 -0
  27. lino/sandbox/contacts/fixtures/demo.py +0 -365
  28. lino/sandbox/contacts/manage.py +0 -10
  29. lino/sandbox/contacts/models.py +0 -395
  30. lino/sandbox/contacts/settings.py +0 -67
  31. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.wsdl +0 -65
  32. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.xsd +0 -286
  33. lino/sandbox/tx25/XSD/rn25_Release201104.xsd +0 -2855
  34. lino/sandbox/tx25/xsd2py1.py +0 -68
  35. lino/sandbox/tx25/xsd2py2.py +0 -62
  36. lino/sandbox/tx25/xsd2py3.py +0 -56
  37. {lino-25.3.0.dist-info → lino-25.3.1.dist-info}/WHEEL +0 -0
  38. {lino-25.3.0.dist-info → lino-25.3.1.dist-info}/licenses/AUTHORS.rst +0 -0
  39. {lino-25.3.0.dist-info → lino-25.3.1.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.3.0'
29
+ __version__ = '25.3.1'
30
30
 
31
31
  # import setuptools # avoid UserWarning "Distutils was imported before Setuptools"?
32
32
 
lino/api/doctest.py CHANGED
@@ -14,6 +14,38 @@ tested document. It includes
14
14
 
15
15
  """
16
16
 
17
+ from lino.core.boundaction import BoundAction
18
+ from lino.core.tables import AbstractTable
19
+ from lino.core.actions import Action
20
+ from django.db.models import Model
21
+ from lino.core.layouts import BaseLayout
22
+ from lino.core.actions import register_params
23
+ from lino.core.actions import ShowTable
24
+ from lino.core.menus import Menu
25
+ from lino.utils.html import html2text
26
+ from lino.core.utils import full_model_name, get_models
27
+ from lino.utils.diag import visible_for
28
+ from lino.sphinxcontrib.actordoc import menuselection_text
29
+ from lino import logger
30
+ from lino.core.menus import find_menu_item
31
+ from lino.core import constants
32
+ from lino.core import actors, kernel
33
+ from lino.utils.sql import sql_summary
34
+ from lino.utils import diag
35
+ from lino.utils.diag import analyzer
36
+ from lino.utils.html import E, tostring, to_rst
37
+ from lino.utils import i2d
38
+ from lino.utils import AttrDict
39
+ from rstgen.utils import unindent, rmu, sixprint
40
+ from rstgen import attrtable
41
+ import rstgen
42
+ import pytest
43
+ from django.db import connection, reset_queries as reset_sql_queries
44
+ from django.test import Client
45
+ from django.utils.encoding import force_str
46
+ from django.utils import translation
47
+ from lino.api.shell import *
48
+ from lino.core.constants import *
17
49
  import os
18
50
  import sys
19
51
  import datetime
@@ -31,39 +63,11 @@ from urllib.parse import urlencode
31
63
  import django
32
64
  django.setup()
33
65
 
34
- from lino.core.constants import *
35
- from lino.api.shell import *
36
- from django.utils import translation
37
- from django.utils.encoding import force_str
38
- from django.test import Client
39
- from django.db import connection, reset_queries as reset_sql_queries
40
66
 
41
- import pytest
42
67
  # from rstgen import table, ul
43
- import rstgen
44
- from rstgen import attrtable
45
- from rstgen.utils import unindent, rmu, sixprint
46
68
 
47
- from lino.utils import AttrDict
48
- from lino.utils import i2d
49
- from lino.utils.html import E, tostring, to_rst
50
- from lino.utils.diag import analyzer
51
- from lino.utils import diag
52
- from lino.utils.sql import sql_summary
53
- from lino.core import actors, kernel
54
- from lino.core import constants
55
- from lino.core.menus import find_menu_item
56
- from lino import logger
57
- from lino.sphinxcontrib.actordoc import menuselection_text
58
- from lino.utils.diag import visible_for
59
- from lino.core.utils import full_model_name, get_models
60
69
 
61
70
  # from lino.core.utils import PseudoRequest
62
- from lino.utils.html import html2text
63
- from lino.core.menus import Menu
64
- from lino.core.actions import ShowTable
65
- from lino.core.actions import register_params
66
- from lino.core.layouts import BaseLayout
67
71
 
68
72
  test_client = Client()
69
73
  """An instance of :class:`django.test.Client`.
@@ -275,12 +279,6 @@ def show_choices(username, url, show_count=False):
275
279
  print("{} rows".format(result["count"]))
276
280
 
277
281
 
278
- from django.db.models import Model
279
- from lino.core.actions import Action
280
- from lino.core.tables import AbstractTable
281
- from lino.core.boundaction import BoundAction
282
-
283
-
284
282
  def show_workflow(actions, all=False, language=None):
285
283
  """
286
284
  Show the given actions as a table. Usage example in
@@ -768,7 +766,6 @@ def walk_store_fields(only_detail_fields=False):
768
766
  yield a, sf
769
767
 
770
768
 
771
- # settings.SITE.kernel.check_virgin()
772
769
  def set_log_level(level):
773
770
  logger.setLevel(level)
774
771
  for handler in logger.handlers:
@@ -800,6 +797,7 @@ def show_change_watchers():
800
797
  )
801
798
  print(rstgen.table(headers, rows, max_width=40))
802
799
 
800
+
803
801
  def show_display_modes():
804
802
  """
805
803
  Show the availble display modes per actor.
@@ -813,7 +811,7 @@ def show_display_modes():
813
811
  rows.append(
814
812
  [str(a)] + [
815
813
  ("x" if dm in a.extra_display_modes else "")
816
- for dm in dml]
814
+ for dm in dml]
817
815
  )
818
816
  print(rstgen.table(headers, rows))
819
817
 
lino/core/inject.py CHANGED
@@ -132,9 +132,9 @@ def check_pending_injects(sender, models_list=None, **kw):
132
132
  # ~ logger.info("20131110 no pending injects")
133
133
  """
134
134
  20130106
135
- now we loop a last time over each model and fill it's _meta._field_cache
136
- otherwise if some application module used inject_field() on a model which
137
- has subclasses, then the new field would not be seen by subclasses
135
+ now we loop a last time over each model and fill its _meta._field_cache
136
+ otherwise if some plugin used inject_field() on a model that
137
+ has subclasses, the new field would not be seen by subclasses
138
138
  """
139
139
  for model in models_list:
140
140
  model._meta._expire_cache()
@@ -225,7 +225,8 @@ def update_model(model_spec, **actions):
225
225
  def todo(model):
226
226
  for k, v in actions.items():
227
227
  if not hasattr(model, k):
228
- raise Exception("%s has no attribute %s to update." % (model, k))
228
+ raise Exception(
229
+ "%s has no attribute %s to update." % (model, k))
229
230
  setattr(model, k, v)
230
231
 
231
232
  if isinstance(model_spec, models.Model):
@@ -389,8 +390,8 @@ def inject_quick_add_buttons(model, name, target):
389
390
  inject_field(
390
391
  model,
391
392
  name,
392
- fields.VirtualField(fields.DisplayField(tm._meta.verbose_name_plural), fn),
393
- )
393
+ fields.VirtualField(fields.DisplayField(
394
+ tm._meta.verbose_name_plural), fn))
394
395
 
395
396
 
396
397
  # def django_patch():
lino/core/kernel.py CHANGED
@@ -26,7 +26,6 @@ import atexit
26
26
  import signal
27
27
  import threading
28
28
  from importlib import import_module
29
- import json
30
29
 
31
30
  # from django.apps import AppConfig
32
31
  from django.apps import apps
@@ -953,51 +952,6 @@ class Kernel(object):
953
952
  # def setup_static_link(self, urlpatterns, short_name,
954
953
  # attr_name=None, source=None):
955
954
 
956
- def mark_virgin(self):
957
- dbhash = self.get_dbhash()
958
- fn = self.site.site_dir / "dbhash.json"
959
- with fn.open("w") as fp:
960
- json.dump(dbhash, fp)
961
-
962
- def check_virgin(self):
963
- new = self.get_dbhash()
964
- fn = self.site.site_dir / "dbhash.json"
965
- if not fn.exists():
966
- raise Exception(
967
- "No `dbhash.json` in {} " "(did you run `django-admin prep`?)".format(
968
- self.site.site_dir
969
- )
970
- )
971
- with fn.open("r") as fp:
972
- old = json.load(fp)
973
-
974
- # noi1r has noi1e as master_site, but the react front end removes the
975
- # tinymce plugin, i.e. noi1r doesn't care about
976
- # tinymce.TextFieldTemplate model.
977
-
978
- diffs = {}
979
- for k, v in new.items():
980
- oldv = old.get(k, None)
981
- if oldv != v:
982
- diffs[k] = (oldv, v)
983
- # diff = set(old.items()) ^ set(new.items())
984
- if diffs:
985
- # db = self.site.django_settings.get('SETTINGS_MODULE')
986
- db = self.site.site_dir
987
- raise Exception("Database {} isn't virgin: {}".format(db, diffs))
988
-
989
- # logger.info("Database certified as virgin")
990
-
991
- def get_dbhash(self):
992
- # TODO: we currently check only the number of rows per model. That's
993
- # very naive.
994
- rv = dict()
995
- for m in get_models(include_auto_created=True):
996
- k = fmn(m)
997
- if k != "sessions.Session":
998
- rv[k] = m.objects.count()
999
- return rv
1000
-
1001
955
 
1002
956
  def site_startup(self):
1003
957
  """This is being imported and called from
lino/core/model.py CHANGED
@@ -534,6 +534,7 @@ class Model(models.Model, fields.TableRow):
534
534
  """
535
535
  watcher = ChangeWatcher(row)
536
536
  # assert hasattr(row, state_field.attname)
537
+ row.before_ui_save(ar, watcher) # added 20250312 for #5976
537
538
  old = getattr(row, state_field.attname)
538
539
  target_state.choicelist.before_state_change(row, ar, old, target_state)
539
540
  row.before_state_change(ar, old, target_state)
@@ -680,21 +681,22 @@ class Model(models.Model, fields.TableRow):
680
681
 
681
682
  df = actor.get_disabled_fields(obj, ar)
682
683
  # print(20170909, df)
683
- for ba in actor.get_actions():
684
- assert ba.actor == actor # 20170102
685
- if ba.action.show_in_workflow:
686
- # if actor.model.__name__ == 'Vote':
687
- # if ba.action.__class__.__name__ == 'MarkVoteAssigned':
688
- # print(20170115, actor, ar.get_user())
689
- if ba.action.action_name not in df:
690
- if actor.get_row_permission(obj, ar, state, ba):
691
- if show and isinstance(ba.action, ChangeStateAction):
692
- show_state()
693
- sep = " \u2192 " # "→"
694
- show = False
695
- l.append(sep)
696
- l.append(ar.action_button(ba, obj))
697
- sep = " "
684
+ if 'workflow_buttons' not in df:
685
+ for ba in actor.get_actions():
686
+ assert ba.actor == actor # 20170102
687
+ if ba.action.show_in_workflow:
688
+ # if actor.model.__name__ == 'Vote':
689
+ # if ba.action.__class__.__name__ == 'MarkVoteAssigned':
690
+ # print(20170115, actor, ar.get_user())
691
+ if ba.action.action_name not in df:
692
+ if actor.get_row_permission(obj, ar, state, ba):
693
+ if show and isinstance(ba.action, ChangeStateAction):
694
+ show_state()
695
+ sep = " \u2192 " # "→"
696
+ show = False
697
+ l.append(sep)
698
+ l.append(ar.action_button(ba, obj))
699
+ sep = " "
698
700
  if state and show:
699
701
  show_state()
700
702
  return E.span(*l)
lino/core/plugin.py CHANGED
@@ -1,10 +1,9 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2008-2024 Rumma & Ko Ltd
2
+ # Copyright 2008-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- import os
6
5
  import inspect
7
- from os.path import exists, join, dirname, isdir, abspath
6
+ from os.path import join, dirname, isdir, abspath
8
7
  from collections.abc import Iterable
9
8
  from urllib.parse import urlencode
10
9
  from lino.core.exceptions import ChangedAPI
@@ -56,7 +55,8 @@ class Plugin:
56
55
 
57
56
  def hide(self):
58
57
  if self.site._startup_done:
59
- raise Exception("Tried to deactivate plugin {} after startup".format(self))
58
+ raise Exception(
59
+ "Tried to deactivate plugin {} after startup".format(self))
60
60
  self.hidden = True
61
61
 
62
62
  def configure(self, **kw):
lino/core/site.py CHANGED
@@ -3,17 +3,37 @@
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
  # doctest lino/core/site.py
5
5
 
6
+ import json
7
+ from lino.core.exceptions import ChangedAPI
8
+ from lino.core.utils import get_models, is_logserver
9
+ from lino.utils.html import E, tostring
10
+ from lino import assert_django_code, DJANGO_DEFAULT_LANGUAGE
11
+ # from lino.core import constants
12
+ from lino.core.plugin import Plugin
13
+ from lino.core.utils import full_model_name as fmn
14
+ from rstgen.confparser import ConfigParser
15
+ from django.utils import translation
16
+ from django.utils.html import mark_safe
17
+ from django.utils.translation import get_language
18
+ from django.utils.translation import gettext_lazy as _
19
+ from django.conf import settings
20
+ import rstgen
21
+ from lino.utils import AttrDict, date_offset, i2d, buildurl
22
+ from lino import logger, __version__
23
+ from importlib.util import find_spec
24
+ from importlib import import_module, reload
25
+ from pathlib import Path
6
26
  import os
7
27
  import re
8
28
  import sys
9
- from os.path import normpath, dirname, join, isdir, relpath, exists, abspath
29
+ from os.path import dirname, join, isdir, relpath, exists
10
30
  import inspect
11
31
  import datetime
12
32
  import warnings
13
33
  import collections
14
34
  import locale
15
35
  import logging
16
- from pprint import pprint
36
+ # from pprint import pprint
17
37
  from logging.handlers import SocketHandler
18
38
  import time
19
39
 
@@ -30,34 +50,12 @@ ASYNC_LOGGING = False
30
50
  # activated, accesses settings.DEFAULT_EXCEPTION_REPORTER, which fails at this
31
51
  # moment because the settings aren't yet loaded.
32
52
 
33
- from pathlib import Path
34
- from importlib import import_module, reload
35
- from importlib.util import find_spec
36
-
37
- from lino import logger, __version__
38
- from lino.utils import AttrDict, date_offset, i2d, buildurl
39
- import rstgen
40
-
41
- from django.conf import settings
42
- from django.utils.translation import gettext_lazy as _
43
- from django.utils.translation import get_language
44
- from django.utils.html import mark_safe
45
- from django.db.utils import DatabaseError
46
- from django.utils import translation
47
53
 
48
54
  has_socialauth = find_spec("social_django") is not None
49
55
  has_elasticsearch = find_spec("elasticsearch_django") is not None
50
56
  has_haystack = find_spec("haystack") is not None
51
57
 
52
- from rstgen.confparser import ConfigParser
53
- from lino.core.plugin import Plugin
54
- from lino.core import constants
55
-
56
- from lino import assert_django_code, DJANGO_DEFAULT_LANGUAGE
57
- from lino.utils.html import E, join_elems, tostring
58
- from lino.core.utils import get_models, is_logserver
59
58
 
60
- from lino.core.exceptions import ChangedAPI
61
59
  # from .roles import SiteUser
62
60
 
63
61
 
@@ -88,14 +86,14 @@ def to_locale(language):
88
86
  p = language.find("-")
89
87
  if p >= 0:
90
88
  # Get correct locale for sr-latn
91
- if len(language[p + 1 :]) > 2:
89
+ if len(language[p + 1:]) > 2:
92
90
  return (
93
91
  language[:p].lower()
94
92
  + "_"
95
93
  + language[p + 1].upper()
96
- + language[p + 2 :].lower()
94
+ + language[p + 2:].lower()
97
95
  )
98
- return language[:p].lower() + "_" + language[p + 1 :].upper()
96
+ return language[:p].lower() + "_" + language[p + 1:].upper()
99
97
  return language.lower()
100
98
 
101
99
 
@@ -103,7 +101,8 @@ def class2str(cl):
103
101
  return cl.__module__ + "." + cl.__name__
104
102
 
105
103
 
106
- gettext_noop = lambda s: s
104
+ def gettext_noop(s): return s
105
+
107
106
 
108
107
  PLUGIN_CONFIGS = {}
109
108
 
@@ -718,7 +717,6 @@ class Site(object):
718
717
  else:
719
718
  d["logger_ok"] = True
720
719
  # self.update_settings(LOGGING=d)
721
- # from pprint import pprint
722
720
  # pprint(d)
723
721
  # print("20161126 Site %s " % d['loggers'].keys())
724
722
  # import yaml
@@ -1796,6 +1794,61 @@ class Site(object):
1796
1794
  # ~ return v
1797
1795
  # ~ return getattr(obj,attrname,*args)
1798
1796
 
1797
+ def mark_virgin(self):
1798
+ """
1799
+ Mark the database as virgin. This is called by :manage:`prep`.
1800
+ """
1801
+ dbhash = self.get_dbhash()
1802
+ fn = self.site_dir / "dbhash.json"
1803
+ with fn.open("w") as fp:
1804
+ json.dump(dbhash, fp)
1805
+ # self.site.logger.info("Wrote %s", fn)
1806
+
1807
+ def check_virgin(self):
1808
+ """
1809
+ Verify whether the database is virgin. Print the differences if there
1810
+ are any.
1811
+ """
1812
+ new = self.get_dbhash()
1813
+ db = self.site_dir
1814
+ fn = db / "dbhash.json"
1815
+ if not fn.exists():
1816
+ raise Exception(
1817
+ f"No `dbhash.json` in {db} (did you run `django-admin prep`?)")
1818
+ with fn.open("r") as fp:
1819
+ old = json.load(fp)
1820
+
1821
+ # noi1r has noi1e as master_site, but the react front end removes the
1822
+ # tinymce plugin, i.e. noi1r doesn't care about
1823
+ # tinymce.TextFieldTemplate model.
1824
+
1825
+ ok = True
1826
+ for k, v in new.items():
1827
+ v = set(v)
1828
+ oldv = set(old.get(k, None))
1829
+ if oldv != v:
1830
+ if ok:
1831
+ print(f"Database {db} isn't virgin:")
1832
+ ok = False
1833
+ diffs = []
1834
+ if (added := len(oldv-v)):
1835
+ diffs.append(f"{added} rows added")
1836
+ if (removed := len(v-oldv)):
1837
+ diffs.append(f"{removed} rows removed")
1838
+ print(f"- {k}: {', '.join(diffs)}")
1839
+
1840
+ def get_dbhash(self):
1841
+ """
1842
+ Return a dictionary with a hash value of the current database content.
1843
+ """
1844
+ rv = dict()
1845
+ for m in get_models(include_auto_created=True):
1846
+ k = fmn(m)
1847
+ if k != "sessions.Session":
1848
+ # rv[k] = m.objects.count()
1849
+ rv[k] = list(m.objects.values_list('pk', flat=True))
1850
+ return rv
1851
+
1799
1852
  def diagnostic_report_rst(self, *args):
1800
1853
  """Returns a string with a diagnostic report about this
1801
1854
  site. :manage:`diag` is a command-line shortcut to this.
@@ -1992,7 +2045,8 @@ class Site(object):
1992
2045
  ):
1993
2046
  yield "social_django.middleware.SocialAuthExceptionMiddleware"
1994
2047
 
1995
- if False: # removed 20240921, see #5755 (Should we remove AjaxExceptionResponse?)
2048
+ # removed 20240921, see #5755 (Should we remove AjaxExceptionResponse?)
2049
+ if False:
1996
2050
  yield "lino.utils.ajax.AjaxExceptionResponse"
1997
2051
 
1998
2052
  if self.use_security_features:
@@ -66,4 +66,4 @@ class Command(BaseCommand):
66
66
  kwargs["removemedia"] = True
67
67
  call_command("initdb", *args, **kwargs)
68
68
 
69
- settings.SITE.kernel.mark_virgin()
69
+ settings.SITE.mark_virgin()
@@ -42,7 +42,10 @@ from lino.core.utils import model_class_path
42
42
  from lino.modlib.help.utils import HelpTextsLoader, simplify_name
43
43
  from lino.modlib.gfks.fields import GenericForeignKey
44
44
  from lino.api.dd import full_model_name
45
- from lino.api import doctest
45
+
46
+ # removed import doctest because it caused "pytest not installed" during
47
+ # makehelp on LF:
48
+ # from lino.api import doctest
46
49
 
47
50
  use_dirhtml = False
48
51
 
@@ -255,7 +258,7 @@ class Command(GeneratingCommand):
255
258
  settings=settings,
256
259
  actors=actors,
257
260
  # actors_list=[a for a in actors.actors_list if not a.abstract],
258
- doctest=doctest,
261
+ # doctest=doctest,
259
262
  translation=translation,
260
263
  use_dirhtml=use_dirhtml,
261
264
  include_useless=include_useless,
@@ -55,8 +55,8 @@ class XMLMaker(dd.Model):
55
55
  if self.xml_validator_file:
56
56
  # print("20250218 {xml[:100]}")
57
57
  # doc = etree.fromstring(xml.encode("utf-8"))
58
- ar.logger.info("Validate %s against %s ...",
59
- xmlfile.path.name, self.xml_validator_file)
58
+ # ar.logger.info("Validate %s against %s ...",
59
+ # xmlfile.path.name, self.xml_validator_file)
60
60
  if True:
61
61
  validate_xml(xmlfile.path, self.xml_validator_file)
62
62
  else:
@@ -132,6 +132,9 @@ class Runnable(Sequenced, RecurrenceSet):
132
132
  await ar.adebug("Successfully terminated %s", self)
133
133
  # ar.info("Successfully terminated %s", astr(self))
134
134
  self.message = out.getvalue()
135
+ except Warning as e:
136
+ await ar.adebug("Terminated %s with warning %s", self, str(e))
137
+ self.message = out.getvalue()
135
138
  except Exception as e:
136
139
  self.message = out.getvalue()
137
140
  self.message += "\n" + "".join(traceback.format_exception(e))
@@ -183,6 +183,9 @@ class CachedPrintable(Duplicable, Printable):
183
183
  def get_target_url(self):
184
184
  return self.build_method.get_target_url(self.do_print, self)
185
185
 
186
+ def get_target_file(self):
187
+ return self.build_method.get_target_file(self.do_print, self)
188
+
186
189
  def get_cache_mtime(self):
187
190
  """Return the modification time (a `datetime`) of the generated cache
188
191
  file, or `None` if no such file exists.
@@ -481,12 +481,13 @@ def on_sanitize(soup, save=False, ar=None):
481
481
  # raise Exception(f"20250301")
482
482
  for tag in soup.find_all():
483
483
  tag_name = tag.name.lower()
484
- if tag_name == "img" and tag['src'].startswith("data:image") and ar is not None and save:
485
- file = base64_to_image(tag['src'])
486
- upload = rt.models.uploads.Upload(file=file, user=ar.get_user())
487
- sar = upload.get_default_table().request(parent=ar)
488
- upload.save_new_instance(sar)
489
- tag.replace_with(f'[file {upload.pk}]')
484
+ if tag_name == "img" and ar is not None and save:
485
+ if (src := tag.get('src')) and src.startswith("data:image"):
486
+ file = base64_to_image(src)
487
+ upload = rt.models.uploads.Upload(file=file, user=ar.get_user())
488
+ sar = upload.get_default_table().request(parent=ar)
489
+ upload.save_new_instance(sar)
490
+ tag.replace_with(f'[file {upload.pk}]')
490
491
 
491
492
 
492
493
  register_sanitizer(on_sanitize)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lino
3
- Version: 25.3.0
3
+ Version: 25.3.1
4
4
  Summary: A framework for writing desktop-like web applications using Django and ExtJS or React
5
5
  Project-URL: Homepage, https://www.lino-framework.org
6
6
  Project-URL: Repository, https://gitlab.com/lino-framework/lino