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/modlib/uploads/actions.py
CHANGED
@@ -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
|
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 =
|
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)
|
lino/modlib/uploads/mixins.py
CHANGED
@@ -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(
|
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
|
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.
|
72
|
-
|
73
|
-
|
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(
|
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
|
-
|
79
|
-
|
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
|
|
lino/modlib/uploads/models.py
CHANGED
@@ -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.
|
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(
|
125
|
-
|
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.
|
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"),
|
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.
|
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(
|
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(
|
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(
|
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
|
-
|
388
|
+
"⎙", # Unicode symbol Print Screen
|
381
389
|
target="_blank",
|
382
|
-
icon_name="page_go",
|
383
|
-
style="vertical-align:-30%;",
|
384
|
-
title=_(
|
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
|
-
|
393
|
-
icon_name="application_form",
|
394
|
-
title=_("Edit
|
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
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
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,
|
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(
|
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,
|
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
lino/utils/jscompressor.py
CHANGED
@@ -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:`
|
21
|
+
two properties :attr:`path` and :attr:`url`.
|
21
22
|
|
22
|
-
It
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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(
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
41
|
-
|
40
|
+
def must_build_printable(self, bm):
|
41
|
+
return True
|
42
42
|
|
43
|
-
def
|
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(
|
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
|
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(
|
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
|
-
|
332
|
-
|
333
|
-
|
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
|
-
|
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(
|
355
|
+
return str(soup).strip()
|
356
|
+
|
357
|
+
|
358
|
+
SANITIZERS = []
|
359
|
+
|
360
|
+
|
361
|
+
def register_sanitizer(func):
|
362
|
+
SANITIZERS.append(func)
|