lino 25.2.3__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 (81) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +11 -48
  3. lino/api/doctest.py +34 -36
  4. lino/core/actions.py +25 -23
  5. lino/core/actors.py +37 -17
  6. lino/core/choicelists.py +10 -8
  7. lino/core/dbtables.py +1 -1
  8. lino/core/elems.py +46 -30
  9. lino/core/fields.py +19 -9
  10. lino/core/inject.py +7 -6
  11. lino/core/kernel.py +26 -66
  12. lino/core/model.py +44 -31
  13. lino/core/plugin.py +4 -4
  14. lino/core/requests.py +76 -55
  15. lino/core/site.py +84 -30
  16. lino/core/store.py +5 -2
  17. lino/core/utils.py +12 -7
  18. lino/help_texts.py +3 -8
  19. lino/management/commands/prep.py +1 -1
  20. lino/mixins/duplicable.py +6 -4
  21. lino/mixins/sequenced.py +17 -6
  22. lino/modlib/__init__.py +0 -2
  23. lino/modlib/changes/models.py +21 -10
  24. lino/modlib/checkdata/models.py +59 -24
  25. lino/modlib/comments/fixtures/demo2.py +12 -3
  26. lino/modlib/comments/models.py +7 -7
  27. lino/modlib/comments/ui.py +8 -5
  28. lino/modlib/export_excel/models.py +7 -5
  29. lino/modlib/extjs/views.py +39 -20
  30. lino/modlib/help/management/commands/makehelp.py +5 -2
  31. lino/modlib/jinja/mixins.py +25 -14
  32. lino/modlib/linod/__init__.py +1 -0
  33. lino/modlib/linod/choicelists.py +21 -0
  34. lino/modlib/linod/consumers.py +13 -4
  35. lino/modlib/linod/management/commands/linod.py +6 -2
  36. lino/modlib/linod/mixins.py +16 -11
  37. lino/modlib/linod/models.py +4 -2
  38. lino/modlib/notify/models.py +18 -10
  39. lino/modlib/printing/actions.py +41 -30
  40. lino/modlib/printing/choicelists.py +11 -9
  41. lino/modlib/printing/mixins.py +25 -20
  42. lino/modlib/publisher/models.py +5 -5
  43. lino/modlib/summaries/models.py +3 -2
  44. lino/modlib/system/models.py +28 -29
  45. lino/modlib/uploads/__init__.py +5 -5
  46. lino/modlib/uploads/actions.py +2 -8
  47. lino/modlib/uploads/choicelists.py +10 -10
  48. lino/modlib/uploads/fixtures/std.py +17 -0
  49. lino/modlib/uploads/mixins.py +20 -8
  50. lino/modlib/uploads/models.py +60 -35
  51. lino/modlib/uploads/ui.py +10 -7
  52. lino/utils/media.py +45 -23
  53. lino/utils/report.py +5 -4
  54. lino/utils/soup.py +22 -4
  55. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/METADATA +1 -1
  56. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/RECORD +59 -80
  57. lino/mixins/uploadable.py +0 -3
  58. lino/sandbox/bcss/PerformInvestigation.py +0 -2260
  59. lino/sandbox/bcss/SSDNReply.py +0 -3924
  60. lino/sandbox/bcss/SSDNRequest.py +0 -3723
  61. lino/sandbox/bcss/__init__.py +0 -0
  62. lino/sandbox/bcss/readme.txt +0 -1
  63. lino/sandbox/bcss/test.py +0 -92
  64. lino/sandbox/bcss/test2.py +0 -128
  65. lino/sandbox/bcss/test3.py +0 -161
  66. lino/sandbox/bcss/test4.py +0 -167
  67. lino/sandbox/contacts/__init__.py +0 -0
  68. lino/sandbox/contacts/fixtures/__init__.py +0 -0
  69. lino/sandbox/contacts/fixtures/demo.py +0 -365
  70. lino/sandbox/contacts/manage.py +0 -10
  71. lino/sandbox/contacts/models.py +0 -395
  72. lino/sandbox/contacts/settings.py +0 -67
  73. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.wsdl +0 -65
  74. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.xsd +0 -286
  75. lino/sandbox/tx25/XSD/rn25_Release201104.xsd +0 -2855
  76. lino/sandbox/tx25/xsd2py1.py +0 -68
  77. lino/sandbox/tx25/xsd2py2.py +0 -62
  78. lino/sandbox/tx25/xsd2py3.py +0 -56
  79. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/WHEEL +0 -0
  80. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/COPYING +0 -0
@@ -2,6 +2,7 @@
2
2
  # Copyright 2012-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
  from lino.api import rt, dd
6
7
 
7
8
  from .choicelists import PublishingStates, PageFillers, SpecialPages
@@ -31,6 +32,7 @@ from lino.mixins import Hierarchical, Sequenced, Referrable
31
32
  from lino.modlib.office.roles import OfficeUser
32
33
  from lino.modlib.publisher.mixins import Publishable, PublishableContent
33
34
  from lino.modlib.comments.mixins import Commentable
35
+ from lino.modlib.linod.choicelists import schedule_daily
34
36
  from lino.modlib.memo.mixins import Previewable
35
37
  from lino.mixins.polymorphic import Polymorphic
36
38
  from lino_xl.lib.topics.mixins import Taggable
@@ -50,7 +52,8 @@ class Page(
50
52
 
51
53
  memo_command = "page"
52
54
 
53
- ref = models.CharField(_("Reference"), max_length=200, blank=True, null=True)
55
+ ref = models.CharField(
56
+ _("Reference"), max_length=200, blank=True, null=True)
54
57
 
55
58
  title = dd.CharField(_("Title"), max_length=250, blank=True)
56
59
  child_node_depth = models.IntegerField(default=1)
@@ -382,7 +385,7 @@ if dd.plugins.memo.use_markup:
382
385
  # language = dd.LanguageField()
383
386
 
384
387
 
385
- @dd.schedule_daily()
388
+ @schedule_daily()
386
389
  def update_publisher_pages(ar):
387
390
  # BaseRequest(parent=ar).run(settings.SITE.site_config.check_all_summaries)
388
391
  # rt.login().run(settings.SITE.site_config.check_all_summaries)
@@ -398,6 +401,3 @@ def update_publisher_pages(ar):
398
401
  prev = obj
399
402
  count += 1
400
403
  ar.logger.info("%d pages have been updated.", count)
401
-
402
-
403
- from .ui import *
@@ -5,7 +5,8 @@ from django.conf import settings
5
5
 
6
6
  # from django.db import models
7
7
  from lino.api import dd, rt, _
8
- from lino.core.requests import BaseRequest
8
+ # from lino.core.requests import BaseRequest
9
+ from lino.modlib.linod.choicelists import schedule_daily
9
10
 
10
11
  from .mixins import UpdateSummariesByMaster, SlaveSummarized, Summarized
11
12
 
@@ -54,7 +55,7 @@ def masters_with_summaries():
54
55
  return summary_masters
55
56
 
56
57
 
57
- @dd.schedule_daily()
58
+ @schedule_daily()
58
59
  def checksummaries(ar):
59
60
  # BaseRequest(parent=ar).run(settings.SITE.site_config.check_all_summaries)
60
61
  # rt.login().run(settings.SITE.site_config.check_all_summaries)
@@ -2,6 +2,27 @@
2
2
  # Copyright 2009-2023 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
+ from lino.utils.report import EmptyTable
6
+ from lino.core.signals import testcase_setup
7
+ from django.test.signals import setting_changed
8
+ from .mixins import Lockable
9
+ from .choicelists import (
10
+ YesNo,
11
+ Genders,
12
+ PeriodEvents,
13
+ DurationUnits,
14
+ Recurrences,
15
+ Weekdays,
16
+ DisplayColors
17
+ )
18
+ from lino.modlib.checkdata.choicelists import Checker
19
+ from lino.modlib.printing.choicelists import BuildMethods
20
+ from lino.core.actors import resolve_action
21
+ from lino.core.roles import SiteStaff
22
+ from lino.core.utils import is_devserver
23
+ from lino.core.utils import full_model_name
24
+ from lino.core import actions
25
+ from lino.api import dd, rt
5
26
  from lino.utils.html import E
6
27
  from django.conf import settings
7
28
  from django.utils.encoding import force_str
@@ -14,27 +35,8 @@ from django.apps import apps
14
35
 
15
36
  get_models = apps.get_models
16
37
 
17
- from lino.api import dd, rt
18
- from lino.core import actions
19
- from lino.core.utils import full_model_name
20
- from lino.core.utils import is_devserver
21
- from lino.core.roles import SiteStaff
22
- from lino.core.actors import resolve_action
23
-
24
- from lino.modlib.printing.choicelists import BuildMethods
25
- from lino.modlib.checkdata.choicelists import Checker
26
38
 
27
39
  # import them here to have them on rt.models.system:
28
- from .choicelists import (
29
- YesNo,
30
- Genders,
31
- PeriodEvents,
32
- DurationUnits,
33
- Recurrences,
34
- Weekdays,
35
- DisplayColors
36
- )
37
- from .mixins import Lockable
38
40
 
39
41
 
40
42
  class BuildSiteCache(dd.Action):
@@ -89,7 +91,8 @@ class SiteConfig(dd.Model):
89
91
  verbose_name=_("Default build method"), blank=True, null=True
90
92
  )
91
93
 
92
- simulate_today = models.DateField(_("Simulated date"), blank=True, null=True)
94
+ simulate_today = models.DateField(
95
+ _("Simulated date"), blank=True, null=True)
93
96
 
94
97
  site_company = dd.ForeignKey(
95
98
  "contacts.Company",
@@ -190,10 +193,12 @@ class SiteConfig(dd.Model):
190
193
  oldval = getattr(cls._site_config, fld.attname)
191
194
  newval = getattr(self, fld.attname)
192
195
  if oldval != newval:
193
- diffs.append("{}: {} -> {}".format(fld.attname, oldval, newval))
196
+ diffs.append(
197
+ "{}: {} -> {}".format(fld.attname, oldval, newval))
194
198
  if len(diffs):
195
199
  print(
196
- "20220824 Overriding SiteConfig instance (diffs={})".format(diffs)
200
+ "20220824 Overriding SiteConfig instance (diffs={})".format(
201
+ diffs)
197
202
  )
198
203
  cls._site_config = self
199
204
  # cls._site_config.update_from(self)
@@ -212,9 +217,6 @@ def my_handler(sender, **kw):
212
217
  # ~ dd.database_connected.send(sender,**kw)
213
218
 
214
219
 
215
- from django.test.signals import setting_changed
216
- from lino.core.signals import testcase_setup
217
-
218
220
  setting_changed.connect(my_handler)
219
221
  testcase_setup.connect(my_handler)
220
222
  dd.connection_created.connect(my_handler)
@@ -244,9 +246,6 @@ class SiteConfigs(dd.Table):
244
246
  do_build = BuildSiteCache()
245
247
 
246
248
 
247
- from lino.utils.report import EmptyTable
248
-
249
-
250
249
  class Dashboard(EmptyTable):
251
250
  # label = _("D")
252
251
  hide_navigator = True
@@ -328,7 +327,7 @@ class BleachChecker(Checker):
328
327
  yield m
329
328
 
330
329
  def get_checkdata_problems(self, obj, fix=False):
331
- t = tuple(obj.fields_to_bleach())
330
+ t = tuple(obj.fields_to_bleach(save=False))
332
331
  if len(t):
333
332
  fldnames = ", ".join([f.name for f, old, new in t])
334
333
  yield (True, _("Fields {} have unbleached content.").format(fldnames))
@@ -1,9 +1,6 @@
1
1
  # Copyright 2010-2024 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
- """See :doc:`/specs/uploads`.
4
3
 
5
-
6
- """
7
4
  from os import symlink
8
5
  from os.path import join
9
6
  from lino import ad, _
@@ -12,6 +9,7 @@ from lino.modlib.memo.parser import split_name_rest
12
9
  UPLOADS_ROOT = 'uploads'
13
10
  VOLUMES_ROOT = 'volumes'
14
11
 
12
+
15
13
  class Plugin(ad.Plugin):
16
14
  "See :doc:`/dev/plugins`."
17
15
 
@@ -68,10 +66,12 @@ class Plugin(ad.Plugin):
68
66
 
69
67
  def gallery(ar, text, cmdname, mentions, context):
70
68
  Upload = site.models.uploads.Upload
71
- photos = [Upload.objects.get(pk=int(pk)) for pk in text.split()]
69
+ photos = [Upload.objects.get(pk=int(pk))
70
+ for pk in text.split()]
72
71
  # ctx = dict(width="{}%".format(int(100/len(photos))))
73
72
  mentions.update(photos)
74
- html = "".join([obj.memo2html(ar, obj.description) for obj in photos])
73
+ html = "".join([obj.memo2html(ar, obj.description)
74
+ for obj in photos])
75
75
  return '<p align="center">{}</p>'.format(html)
76
76
 
77
77
  site.plugins.memo.parser.register_command("gallery", gallery)
@@ -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
@@ -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()
@@ -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
 
@@ -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,19 @@ 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 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}]')
491
+
492
+
493
+ 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
 
@@ -187,12 +187,14 @@ class AreaUploads(Uploads):
187
187
  qs = qs.filter(upload_area=area)
188
188
  else:
189
189
  return E.div(
190
- "{} is not an UploadController!".format(model_class_path(obj.__class__))
190
+ "{} is not an UploadController!".format(
191
+ model_class_path(obj.__class__))
191
192
  )
192
193
  volume = obj.get_uploads_volume()
193
194
  # print(20190208, volume)
194
195
  for ut in qs:
195
- 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))
196
198
  # logger.info("20140430 %s", sar.data_iterator.query)
197
199
  files = []
198
200
  for m in sar:
@@ -211,7 +213,8 @@ class AreaUploads(Uploads):
211
213
  mf.get_download_url(),
212
214
  # u"\u21A7", # DOWNWARDS ARROW FROM BAR (↧)
213
215
  # u"\u21E8",
214
- "\u21f2", # SOUTH EAST ARROW TO CORNER (⇲)
216
+ # "\u21f2", # SOUTH EAST ARROW TO CORNER (⇲)
217
+ "⎙", # Unicode symbol Print Screen
215
218
  style="text-decoration:none;",
216
219
  # _(" [show]"), # fmt(m),
217
220
  target="_blank",
@@ -248,12 +251,12 @@ class AreaUploads(Uploads):
248
251
  else:
249
252
  if len(types) == 0:
250
253
  elems.append(str(ar.no_data_text))
251
- elems.append(" / ")
254
+ # elems.append(" / ")
252
255
  else:
253
256
  for chunks in types:
254
257
  elems.extend(chunks)
255
- elems.append(" / ")
256
- elems.append(obj.show_uploads.as_button_elem(ar))
258
+ # elems.append(" / ")
259
+ # elems.append(obj.show_uploads.as_button_elem(ar))
257
260
  # ba = self.find_action_by_name("show_uploads")
258
261
  return E.div(*elems)
259
262
 
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):