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
@@ -2,10 +2,8 @@
2
2
  # Copyright 2023 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- import base64
6
- from django.utils import timezone
7
5
  from lino.api import rt, dd, _
8
- from .mixins import make_captured_image
6
+ from .mixins import base64_to_image
9
7
 
10
8
 
11
9
  class CameraStream(dd.Action): # TODO: rename this to CaptureImage
@@ -29,12 +27,8 @@ class CameraStream(dd.Action): # TODO: rename this to CaptureImage
29
27
  description
30
28
  """
31
29
 
32
- def base64_to_image(self, imgstring):
33
- imgdata = base64.b64decode(imgstring.split("base64,")[1])
34
- return make_captured_image(imgdata, timezone.now())
35
-
36
30
  def handle_uploaded_file(self, ar, **kwargs):
37
- file = self.base64_to_image(ar.request.POST["image"])
31
+ file = base64_to_image(ar.request.POST["image"])
38
32
  upload = rt.models.uploads.Upload(file=file, user=ar.get_user(), **kwargs)
39
33
  upload.save_new_instance(ar)
40
34
  return upload
@@ -6,6 +6,16 @@ from lino.api import dd, rt, _
6
6
  from lino.modlib.office.roles import OfficeStaff
7
7
 
8
8
 
9
+ class UploadAreas(dd.ChoiceList):
10
+ required_roles = dd.login_required(OfficeStaff)
11
+ verbose_name = _("Upload area")
12
+ verbose_name_plural = _("Upload areas")
13
+
14
+
15
+ add = UploadAreas.add_item
16
+ add("90", _("Uploads"), "general")
17
+
18
+
9
19
  class Shortcut(dd.Choice):
10
20
  """Represents a shortcut field."""
11
21
 
@@ -31,15 +41,5 @@ class Shortcuts(dd.ChoiceList):
31
41
  max_length = 50 # fields get created before the values are known
32
42
 
33
43
 
34
- class UploadAreas(dd.ChoiceList):
35
- required_roles = dd.login_required(OfficeStaff)
36
- verbose_name = _("Upload area")
37
- verbose_name_plural = _("Upload areas")
38
-
39
-
40
- add = UploadAreas.add_item
41
- add("90", _("Uploads"), "general")
42
-
43
-
44
44
  def add_shortcut(*args, **kw):
45
45
  return Shortcuts.add_item(*args, **kw)
@@ -0,0 +1,17 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2015-2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from lino.api import dd, rt
6
+ from lino.modlib.system.choicelists import Recurrences
7
+ from lino.modlib.uploads.choicelists import Shortcuts
8
+
9
+
10
+ def objects():
11
+ UploadType = rt.models.uploads.UploadType
12
+
13
+ kw = dict(max_number=1, wanted=True)
14
+
15
+ for us in Shortcuts.get_list_items():
16
+ kw.update(dd.str2kw('name', us.text))
17
+ yield UploadType(shortcut=us, **kw)
@@ -2,6 +2,7 @@
2
2
  # Copyright 2008-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
+ import base64
5
6
  import os
6
7
  import shutil
7
8
  import uuid
@@ -17,6 +18,7 @@ from django.core.files.storage import default_storage
17
18
  from django.core.exceptions import ValidationError, FieldError
18
19
  from django.template.defaultfilters import filesizeformat
19
20
  from django.utils.html import format_html
21
+ from django.utils import timezone
20
22
 
21
23
  from lino.core import constants
22
24
  from lino.utils import DATE_TO_DIR_TPL
@@ -57,7 +59,8 @@ def make_uploaded_file(filename, src=None, upload_date=None):
57
59
  raise Exception(f"Source {src} does not exist")
58
60
  filename = default_storage.generate_filename(safe_filename(filename))
59
61
  upload_to = Path(upload_date.strftime(upload_to_tpl))
60
- upload_filename = default_storage.generate_filename(str(upload_to / filename))
62
+ upload_filename = default_storage.generate_filename(
63
+ str(upload_to / filename))
61
64
  dest = settings.SITE.media_root / upload_filename
62
65
  if needs_update(src, dest):
63
66
  print("cp {} {}".format(src, dest))
@@ -66,17 +69,26 @@ def make_uploaded_file(filename, src=None, upload_date=None):
66
69
  return upload_filename
67
70
 
68
71
 
69
- def make_captured_image(imgdata, upload_date=None):
72
+ def base64_to_image(imgstring):
73
+ type, file = imgstring.split(";base64,")
74
+ imgdata = base64.b64decode(file)
75
+ return make_captured_image(imgdata, timezone.now(), ext=f".{type.split('/')[1]}")
76
+
77
+
78
+ def make_captured_image(imgdata, upload_date=None, filename=None, ext='.jpg'):
70
79
  if upload_date is None:
71
- upload_date = dd.demo_date()
72
- filename = default_storage.generate_filename(
73
- safe_filename(str(uuid.uuid4()) + ".jpg"))
80
+ upload_date = dd.today()
81
+ if not filename:
82
+ filename = str(uuid.uuid4()) + ext
83
+ filename = default_storage.generate_filename(safe_filename(filename))
74
84
  upload_to = Path(upload_date.strftime(upload_to_tpl))
75
- upload_filename = default_storage.generate_filename(str(upload_to / filename))
85
+ upload_filename = default_storage.generate_filename(
86
+ str(upload_to / filename))
76
87
  dest = Path(settings.MEDIA_ROOT) / upload_filename
77
88
  dest.parent.mkdir(parents=True, exist_ok=True)
78
- with dest.open("wb") as f:
79
- f.write(imgdata)
89
+ dest.write_bytes(imgdata)
90
+ # with dest.open("wb") as f:
91
+ # f.write(imgdata)
80
92
  return upload_filename
81
93
 
82
94
 
@@ -2,6 +2,7 @@
2
2
  # Copyright 2008-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
+ from .ui import *
5
6
  import os
6
7
  from os.path import join, exists
7
8
  from pathlib import Path
@@ -26,13 +27,14 @@ from lino.modlib.users.mixins import UserAuthored, My
26
27
  # from lino.modlib.office.roles import OfficeUser, OfficeStaff, OfficeOperator
27
28
  from lino.modlib.office.roles import OfficeStaff
28
29
  from lino.mixins import Referrable
30
+ from lino.utils.soup import register_sanitizer
29
31
  from lino.utils.mldbc.mixins import BabelNamed
30
32
  from lino.modlib.checkdata.choicelists import Checker
31
33
  from lino.modlib.publisher.mixins import Publishable
32
34
 
33
35
  from .actions import CameraStream
34
- from .choicelists import Shortcuts, UploadAreas
35
- from .mixins import UploadBase
36
+ from .choicelists import Shortcuts, UploadAreas, add_shortcut
37
+ from .mixins import UploadBase, base64_to_image
36
38
  from .utils import previewer, UploadMediaFile
37
39
 
38
40
  from . import VOLUMES_ROOT
@@ -57,7 +59,7 @@ class Volume(Referrable):
57
59
 
58
60
  def full_clean(self, *args, **kw):
59
61
  super().full_clean(*args, **kw)
60
- pth = dd.plugins.uploads.get_volumes_root() / self.ref
62
+ pth = dd.plugins.uploads.volumes_root / self.ref
61
63
  if pth.exists():
62
64
  if pth.resolve().absolute() != Path(self.root_dir).resolve().absolute():
63
65
  raise ValidationError(
@@ -121,8 +123,10 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
121
123
  upload_area = UploadAreas.field(default="general")
122
124
  type = dd.ForeignKey("uploads.UploadType", blank=True, null=True)
123
125
  volume = dd.ForeignKey("uploads.Volume", blank=True, null=True)
124
- library_file = models.CharField(_("Library file"), max_length=255, blank=True)
125
- description = models.CharField(_("Description"), max_length=200, blank=True)
126
+ library_file = models.CharField(
127
+ _("Library file"), max_length=255, blank=True)
128
+ description = models.CharField(
129
+ _("Description"), max_length=200, blank=True)
126
130
  source = dd.ForeignKey("sources.Source", blank=True, null=True)
127
131
 
128
132
  camera_stream = CameraStream()
@@ -176,7 +180,7 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
176
180
  if self.file:
177
181
  return self.file.size
178
182
  if self.volume_id and self.library_file:
179
- pth = dd.plugins.uploads.get_volumes_root() / self.volume.ref / self.library_file
183
+ pth = dd.plugins.uploads.volumes_root / self.volume.ref / self.library_file
180
184
  return pth.stat().st_size
181
185
  # return os.path.getsize(pth)
182
186
 
@@ -286,7 +290,8 @@ class UploadChecker(Checker):
286
290
  if not exists(join(settings.MEDIA_ROOT, obj.file.name)):
287
291
  yield (
288
292
  False,
289
- format_lazy(_("Upload entry {} has no file"), obj.file.name),
293
+ format_lazy(_("Upload entry {} has no file"),
294
+ obj.file.name),
290
295
  )
291
296
  return
292
297
 
@@ -299,6 +304,7 @@ class UploadChecker(Checker):
299
304
  for i in obj.check_previews(fix):
300
305
  yield i
301
306
 
307
+
302
308
  UploadChecker.activate()
303
309
 
304
310
 
@@ -308,7 +314,7 @@ class UploadsFolderChecker(Checker):
308
314
  def get_checkdata_problems(self, obj, fix=False):
309
315
  assert obj is None # this is an unbound checker
310
316
  Upload = rt.models.uploads.Upload
311
- pth = dd.plugins.uploads.get_uploads_root()
317
+ pth = dd.plugins.uploads.uploads_root
312
318
  assert str(pth).startswith(settings.MEDIA_ROOT)
313
319
  start = len(settings.MEDIA_ROOT) + 1
314
320
  for filename in Path(pth).rglob("*"):
@@ -319,7 +325,8 @@ class UploadsFolderChecker(Checker):
319
325
  qs = Upload.objects.filter(file=rel_filename)
320
326
  n = qs.count()
321
327
  if n == 0:
322
- msg = format_lazy(_("File {} has no upload entry."), rel_filename)
328
+ msg = format_lazy(
329
+ _("File {} has no upload entry."), rel_filename)
323
330
  # print(msg)
324
331
  yield (dd.plugins.uploads.remove_orphaned_files, msg)
325
332
  if fix and dd.plugins.uploads.remove_orphaned_files:
@@ -357,31 +364,33 @@ def before_analyze(sender, **kwargs):
357
364
  items = []
358
365
  target = sender.modules.resolve(i.target)
359
366
  sar = ar.spawn_request(
360
- actor=target, master_instance=obj, known_values=dict(type=utype)
361
- )
367
+ actor=target, master_instance=obj, known_values=dict(
368
+ type=utype))
362
369
  # param_values=dict(pupload_type=et))
363
370
  n = sar.get_total_count()
364
371
  if n == 0:
365
- iar = target.insert_action.request_from(sar, master_instance=obj)
372
+ iar = target.insert_action.request_from(
373
+ sar, master_instance=obj)
366
374
  btn = iar.ar2button(
367
375
  None,
368
- _("Upload"),
369
- icon_name="page_add",
370
- title=_("Upload a file from your PC to the server."),
371
- )
376
+ "⊕", # _("Upload"),
377
+ # icon_name="page_add",
378
+ title=_("Upload a file from your PC to the server."))
372
379
  items.append(btn)
373
- elif n == 1:
380
+ # elif n == 1:
381
+ else:
374
382
  after_show = ar.get_status()
375
383
  obj = sar.data_iterator[0]
376
384
  if (mf := obj.get_media_file()) is not None:
377
385
  items.append(
378
386
  sar.renderer.href_button(
379
387
  mf.get_download_url(),
380
- _("show"),
388
+ "", # Unicode symbol Print Screen
381
389
  target="_blank",
382
- icon_name="page_go",
383
- style="vertical-align:-30%;",
384
- title=_("Open the uploaded file in a new browser window"),
390
+ # icon_name="page_go",
391
+ # style="vertical-align:-30%;",
392
+ title=_(
393
+ "Open the uploaded file in a new browser window"),
385
394
  )
386
395
  )
387
396
  after_show.update(record_id=obj.pk)
@@ -389,23 +398,25 @@ def before_analyze(sender, **kwargs):
389
398
  sar.window_action_button(
390
399
  sar.ah.actor.detail_action,
391
400
  after_show,
392
- _("Edit"),
393
- icon_name="application_form",
394
- title=_("Edit metadata of the uploaded file."),
401
+ "", # Unicode symbol Enter
402
+ # icon_name="application_form",
403
+ title=_("Edit the information about the uploaded file."),
395
404
  )
396
405
  )
397
- else:
398
- obj = sar.sliced_data_iterator[0]
399
- items.append(ar.obj2html(obj, pgettext("uploaded file", "Last")))
400
-
401
- btn = sar.renderer.action_button(
402
- obj,
403
- sar,
404
- sar.bound_action,
405
- _("All {0} files").format(n),
406
- icon_name=None,
407
- )
408
- items.append(btn)
406
+ # else:
407
+ # obj = sar.sliced_data_iterator[0]
408
+ # items.append(ar.obj2html(
409
+ # obj, pgettext("uploaded file", "Last")))
410
+
411
+ btn = sar.renderer.action_button(
412
+ obj,
413
+ sar,
414
+ sar.bound_action,
415
+ "⏏", # _("All {0} files").format(n),
416
+ icon_name=None,
417
+ title=_("Manage the list of uploaded files.")
418
+ )
419
+ items.append(btn)
409
420
 
410
421
  return E.div(*join_elems(items, ", "))
411
422
 
@@ -415,8 +426,6 @@ def before_analyze(sender, **kwargs):
415
426
  # i.model_spec, i.name)
416
427
 
417
428
 
418
- from .ui import *
419
-
420
429
  # raise Exception("20241112")
421
430
 
422
431
 
@@ -466,3 +475,18 @@ def setup_memo_commands(sender=None, **kwargs):
466
475
 
467
476
  mp = sender.plugins.memo.parser
468
477
  mp.register_django_model('file', rt.models.uploads.Upload, rnd=file2html)
478
+
479
+
480
+ def on_sanitize(soup, save=False, ar=None):
481
+ # raise Exception(f"20250301")
482
+ for tag in soup.find_all():
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}]')
490
+
491
+
492
+ register_sanitizer(on_sanitize)
lino/modlib/uploads/ui.py CHANGED
@@ -21,7 +21,7 @@ from lino.core import constants
21
21
  def filename_leaf(name):
22
22
  i = name.rfind("/")
23
23
  if i != -1:
24
- return name[i + 1 :]
24
+ return name[i + 1:]
25
25
  return name
26
26
 
27
27
 
@@ -153,6 +153,7 @@ class AreaUploads(Uploads):
153
153
  required_roles = dd.login_required(UploadsReader)
154
154
  stay_in_grid = True
155
155
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
156
+ detailed_summary = False
156
157
 
157
158
  # 20180119
158
159
  # @classmethod
@@ -170,7 +171,8 @@ class AreaUploads(Uploads):
170
171
  return obj.description or filename_leaf(obj.file.name) or str(obj.id)
171
172
 
172
173
  @classmethod
173
- def get_table_summary(self, obj, ar):
174
+ def get_table_summary(self, ar):
175
+ obj = ar.master_instance
174
176
  if obj is None:
175
177
  return
176
178
  UploadType = rt.models.uploads.UploadType
@@ -185,12 +187,14 @@ class AreaUploads(Uploads):
185
187
  qs = qs.filter(upload_area=area)
186
188
  else:
187
189
  return E.div(
188
- "{} is not an UploadController!".format(model_class_path(obj.__class__))
190
+ "{} is not an UploadController!".format(
191
+ model_class_path(obj.__class__))
189
192
  )
190
193
  volume = obj.get_uploads_volume()
191
194
  # print(20190208, volume)
192
195
  for ut in qs:
193
- sar = ar.spawn(self, master_instance=obj, known_values=dict(type_id=ut.id))
196
+ sar = ar.spawn(self, master_instance=obj,
197
+ known_values=dict(type_id=ut.id))
194
198
  # logger.info("20140430 %s", sar.data_iterator.query)
195
199
  files = []
196
200
  for m in sar:
@@ -209,7 +213,8 @@ class AreaUploads(Uploads):
209
213
  mf.get_download_url(),
210
214
  # u"\u21A7", # DOWNWARDS ARROW FROM BAR (↧)
211
215
  # u"\u21E8",
212
- "\u21f2", # SOUTH EAST ARROW TO CORNER (⇲)
216
+ # "\u21f2", # SOUTH EAST ARROW TO CORNER (⇲)
217
+ "⎙", # Unicode symbol Print Screen
213
218
  style="text-decoration:none;",
214
219
  # _(" [show]"), # fmt(m),
215
220
  target="_blank",
@@ -237,7 +242,8 @@ class AreaUploads(Uploads):
237
242
  types.append(chunks)
238
243
  # logger.info("20140430 %s", [tostring(e) for e in types])
239
244
  # elems += [str(ar.bound_action.action.__class__), " "]
240
- if ar.bound_action.action.window_type == "d":
245
+ # if ar.bound_action.action.window_type == "d":
246
+ if self.detailed_summary:
241
247
  if len(types) == 0:
242
248
  elems.append(E.ul(E.li(str(ar.no_data_text))))
243
249
  else:
@@ -245,12 +251,12 @@ class AreaUploads(Uploads):
245
251
  else:
246
252
  if len(types) == 0:
247
253
  elems.append(str(ar.no_data_text))
248
- elems.append(" / ")
254
+ # elems.append(" / ")
249
255
  else:
250
256
  for chunks in types:
251
257
  elems.extend(chunks)
252
- elems.append(" / ")
253
- elems.append(obj.show_uploads.as_button_elem(ar))
258
+ # elems.append(" / ")
259
+ # elems.append(obj.show_uploads.as_button_elem(ar))
254
260
  # ba = self.find_action_by_name("show_uploads")
255
261
  return E.div(*elems)
256
262
 
lino/utils/__init__.py CHANGED
@@ -40,7 +40,6 @@ function for general use. It has also many subpackages and submodules.
40
40
  pyuca
41
41
  quantities
42
42
  ranges
43
- requests
44
43
  restify
45
44
  screenshots
46
45
  sqllog
@@ -39,18 +39,18 @@ class JSCompressor(object):
39
39
 
40
40
  literalMarker = "@_@%d@_@" # temporary replacement
41
41
  # put the string literals back in
42
- backSubst = re.compile("@_@(\d+)@_@")
42
+ backSubst = re.compile(r"@_@(\d+)@_@")
43
43
 
44
44
  # /* ... */ comments on single line
45
45
  mlc1 = re.compile(r"(\/\*.*?\*\/)")
46
46
  mlc = re.compile(r"(\/\*.*?\*\/)", re.DOTALL) # real multiline comments
47
- slc = re.compile("\/\/.*") # remove single line comments
47
+ slc = re.compile(r"\/\/.*") # remove single line comments
48
48
 
49
49
  # collapse successive non-leading white space characters into one
50
- collapseWs = re.compile("(?<=\S)[ \t]+")
50
+ collapseWs = re.compile(r"(?<=\S)[ \t]+")
51
51
 
52
52
  squeeze = re.compile(
53
- """
53
+ r"""
54
54
  \s+(?=[\}\]\)\:\&\|\=\;\,\.\+]) | # remove whitespace preceding control characters
55
55
  (?<=[\{\[\(\:\&\|\=\;\,\.\+])\s+ | # ... or following such
56
56
  [ \t]+(?=\W) | # remove spaces or tabs preceding non-word characters
lino/utils/media.py CHANGED
@@ -5,6 +5,7 @@
5
5
  """
6
6
 
7
7
  from os.path import join
8
+ from pathlib import Path
8
9
 
9
10
  from django.conf import settings
10
11
  from lino.core.utils import is_devserver
@@ -17,10 +18,10 @@ has_davlink = False
17
18
  class MediaFile(object):
18
19
  """
19
20
  Represents a file on the server below :setting:`MEDIA_ROOT` with
20
- two properties :attr:`name` and :attr:`url`.
21
+ two properties :attr:`path` and :attr:`url`.
21
22
 
22
- It also takes into consideration the settings
23
- :attr:`webdav_root <lino.core.site.Site.webdav_root>`
23
+ It takes into consideration the settings
24
+ :attr:`webdav_root <lino.core.site.Site.webdav_root>`,
24
25
  :attr:`webdav_protocol <lino.core.site.Site.webdav_protocol>`
25
26
  and
26
27
  :attr:`webdav_url <lino.core.site.Site.webdav_url>`
@@ -28,28 +29,49 @@ class MediaFile(object):
28
29
 
29
30
  def __init__(self, editable, *parts):
30
31
  self.editable = editable
31
- self.parts = parts
32
-
33
- @property
34
- def name(self):
35
- "return the filename on the server"
36
- if self.editable and (has_davlink or settings.SITE.webdav_protocol):
37
- return join(settings.SITE.webdav_root, *self.parts)
38
- return join(settings.MEDIA_ROOT, *self.parts)
39
-
40
- def get_url(self, request):
41
- "return the url that points to file on the server"
42
- if self.editable and request is not None:
43
- if is_devserver():
44
- url = "file://" + join(settings.SITE.webdav_root, *self.parts)
32
+ # self.parts = parts
33
+ if editable and settings.SITE.webdav_protocol:
34
+ path = Path(settings.SITE.webdav_root, *parts)
35
+ # 20250302 Removed the file:// trick on a devserver because anyway
36
+ # it doesn't work anymore. For editable media files we need a a
37
+ # webdav server and a protocol handler.
38
+ # if is_devserver():
39
+ if False:
40
+ url = "file://" + join(settings.SITE.webdav_root, *parts)
45
41
  else:
46
- url = settings.SITE.webdav_url + "/".join(self.parts)
47
- url = request.build_absolute_uri(url)
48
- if settings.SITE.webdav_protocol:
49
- url = settings.SITE.webdav_protocol + "://" + url
50
- return url
42
+ url = settings.SITE.webdav_url + "/".join(parts)
43
+ if settings.SITE.webdav_protocol:
44
+ url = settings.SITE.webdav_protocol + "://" + url
45
+ else:
46
+ path = Path(settings.MEDIA_ROOT, *parts)
47
+ url = settings.SITE.build_media_url(*parts)
48
+ self.url = url
49
+ self.path = path
51
50
 
52
- return settings.SITE.build_media_url(*self.parts)
51
+ # @property
52
+ # def path(self):
53
+ # "Return the full filename on the server as a Path object."
54
+
55
+ # @property
56
+ # def name(self):
57
+ # "return the filename on the server"
58
+ # if self.editable and (has_davlink or settings.SITE.webdav_protocol):
59
+ # return join(settings.SITE.webdav_root, *self.parts)
60
+ # return join(settings.MEDIA_ROOT, *self.parts)
61
+
62
+ # def get_url(self, request):
63
+ # "return the url that points to file on the server"
64
+ # if self.editable and request is not None:
65
+ # if is_devserver():
66
+ # url = "file://" + join(settings.SITE.webdav_root, *self.parts)
67
+ # else:
68
+ # url = settings.SITE.webdav_url + "/".join(self.parts)
69
+ # url = request.build_absolute_uri(url)
70
+ # if settings.SITE.webdav_protocol:
71
+ # url = settings.SITE.webdav_protocol + "://" + url
72
+ # return url
73
+ #
74
+ # return settings.SITE.build_media_url(*self.parts)
53
75
 
54
76
 
55
77
  class TmpMediaFile(MediaFile):
lino/utils/report.py CHANGED
@@ -37,10 +37,10 @@ class EmptyTableRow(VirtualRow, Printable):
37
37
  def __str__(self):
38
38
  return str(self._table.label)
39
39
 
40
- def before_printable_build(self, bm):
41
- pass
40
+ def must_build_printable(self, bm):
41
+ return True
42
42
 
43
- def filename_root(self):
43
+ def get_printable_target_stem(self):
44
44
  return self._table.app_label + "." + self._table.__name__
45
45
 
46
46
  # def get_print_language(self):
@@ -239,7 +239,8 @@ class Report(EmptyTable):
239
239
 
240
240
  @classmethod
241
241
  def as_appy_pod_xml(cls, self, apr):
242
- chunks = tuple(apr.story2odt(self.get_story(apr.ar), master_instance=self))
242
+ chunks = tuple(apr.story2odt(
243
+ self.get_story(apr.ar), master_instance=self))
243
244
  return str("").join(chunks) # must be utf8 encoded
244
245
 
245
246
  @classmethod
lino/utils/restify.py CHANGED
@@ -17,7 +17,7 @@ import re
17
17
 
18
18
  # This regular expression finds the indentation of every non-blank
19
19
  # line in a string.
20
- _INDENT_RE = re.compile("^([ ]*)(?=\S)", re.MULTILINE)
20
+ _INDENT_RE = re.compile(r"^([ ]*)(?=\S)", re.MULTILINE)
21
21
 
22
22
 
23
23
  def min_indent(s):
@@ -451,7 +451,7 @@ def rst2latex(
451
451
 
452
452
 
453
453
  if __name__ == "__main__":
454
- test = """
454
+ test = r"""
455
455
  Test example
456
456
  ============
457
457
 
lino/utils/soup.py CHANGED
@@ -7,7 +7,8 @@
7
7
  import re
8
8
  from bs4 import BeautifulSoup, NavigableString, Comment, Doctype
9
9
  from bs4.element import Tag
10
- import logging; logger = logging.getLogger(__file__)
10
+ import logging
11
+ logger = logging.getLogger(__file__)
11
12
  # from lino.api import dd
12
13
 
13
14
 
@@ -39,6 +40,7 @@ WHITESPACE_TAGS = PARAGRAPH_TAGS | {
39
40
  SHORT_PREVIEW_IMAGE_HEIGHT = "8em"
40
41
  REMOVED_IMAGE_PLACEHOLDER = "⌧"
41
42
 
43
+
42
44
  class Style:
43
45
  # TODO: Extend rstgen.sphinxconf.sigal_image.Format to incoroporate this.
44
46
  def __init__(self, s):
@@ -289,6 +291,7 @@ ALLOWED_ATTRIBUTES["p"] = GENERALLY_ALLOWED_ATTRS | {"align"}
289
291
  # return re.sub("(width|height):[^;]+;", "", css)
290
292
  # return css
291
293
 
294
+
292
295
  def sanitized_soup(old):
293
296
 
294
297
  # Inspired by https://chase-seibert.github.io/blog/2011/01/28/sanitize-html-with-beautiful-soup.html
@@ -322,23 +325,38 @@ def sanitized_soup(old):
322
325
  tag.attrs = dict()
323
326
 
324
327
  # remove all comments because they might contain scripts
325
- comments = soup.find_all(text=lambda text:isinstance(text, (Comment, Doctype)))
328
+ comments = soup.find_all(
329
+ text=lambda text: isinstance(text, (Comment, Doctype)))
326
330
  for comment in comments:
327
331
  comment.extract()
328
332
 
329
333
  # remove the wrapper tag if it is useless
330
- if len(soup.contents) == 1:
331
- main_tag = soup.contents[0]
332
- if main_tag.name in useless_main_tags and not main_tag.attrs:
333
- main_tag.unwrap()
334
+ # if len(soup.contents) == 1:
335
+ # main_tag = soup.contents[0]
336
+ # if main_tag.name in useless_main_tags and not main_tag.attrs:
337
+ # main_tag.unwrap()
334
338
 
335
339
  return soup
336
340
 
337
- def sanitize(s):
341
+
342
+ def sanitize(s, **kwargs):
338
343
  s = s.strip()
339
344
  if not s:
340
345
  return s
346
+
347
+ soup = sanitized_soup(s)
348
+
349
+ for func in SANITIZERS:
350
+ func(soup, **kwargs)
351
+
341
352
  # do we want to remove whitespace between tags?
342
353
  # s = re.sub(">\s+<", "><", s)
343
354
  # return sanitized_soup(s).decode(formatter="html").strip()
344
- return str(sanitized_soup(s)).strip()
355
+ return str(soup).strip()
356
+
357
+
358
+ SANITIZERS = []
359
+
360
+
361
+ def register_sanitizer(func):
362
+ SANITIZERS.append(func)