lino 24.11.0__py3-none-any.whl → 25.1.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 +1 -1
- lino/core/actions.py +2 -4
- lino/core/actors.py +35 -14
- lino/core/dbtables.py +12 -12
- lino/core/fields.py +27 -18
- lino/core/inject.py +9 -2
- lino/core/kernel.py +11 -12
- lino/core/model.py +25 -39
- lino/core/renderer.py +3 -4
- lino/core/requests.py +91 -92
- lino/core/site.py +5 -46
- lino/core/store.py +4 -4
- lino/core/tables.py +0 -16
- lino/core/utils.py +5 -2
- lino/help_texts.py +7 -5
- lino/locale/bn/LC_MESSAGES/django.po +1225 -909
- lino/locale/de/LC_MESSAGES/django.mo +0 -0
- lino/locale/de/LC_MESSAGES/django.po +1748 -1392
- lino/locale/django.pot +1150 -909
- lino/locale/es/LC_MESSAGES/django.po +1721 -1347
- lino/locale/et/LC_MESSAGES/django.po +1267 -954
- lino/locale/fr/LC_MESSAGES/django.mo +0 -0
- lino/locale/fr/LC_MESSAGES/django.po +1207 -925
- lino/locale/nl/LC_MESSAGES/django.po +1261 -942
- lino/locale/pt_BR/LC_MESSAGES/django.po +1204 -906
- lino/locale/zh_Hant/LC_MESSAGES/django.po +1204 -906
- lino/mixins/dupable.py +8 -7
- lino/mixins/periods.py +10 -8
- lino/modlib/checkdata/__init__.py +1 -1
- lino/modlib/comments/choicelists.py +1 -1
- lino/modlib/comments/fixtures/demo2.py +4 -1
- lino/modlib/comments/mixins.py +9 -10
- lino/modlib/comments/models.py +4 -4
- lino/modlib/comments/ui.py +11 -2
- lino/modlib/dupable/models.py +10 -14
- lino/modlib/gfks/mixins.py +8 -1
- lino/modlib/jinja/choicelists.py +3 -3
- lino/modlib/jinja/renderer.py +2 -0
- lino/modlib/languages/fixtures/all_languages.py +4 -6
- lino/modlib/linod/mixins.py +3 -2
- lino/modlib/memo/mixins.py +17 -215
- lino/modlib/memo/models.py +41 -12
- lino/modlib/memo/parser.py +7 -3
- lino/modlib/notify/mixins.py +35 -34
- lino/modlib/periods/choicelists.py +46 -0
- lino/modlib/periods/mixins.py +26 -1
- lino/modlib/periods/models.py +17 -45
- lino/modlib/printing/choicelists.py +3 -3
- lino/modlib/publisher/choicelists.py +1 -4
- lino/modlib/search/models.py +17 -11
- lino/modlib/system/fixtures/__init__.py +0 -0
- lino/modlib/system/fixtures/std.py +5 -0
- lino/modlib/system/models.py +3 -2
- lino/modlib/uploads/__init__.py +10 -1
- lino/modlib/uploads/choicelists.py +3 -3
- lino/modlib/uploads/mixins.py +49 -51
- lino/modlib/uploads/models.py +144 -61
- lino/modlib/uploads/ui.py +12 -6
- lino/modlib/uploads/utils.py +113 -0
- lino/modlib/users/mixins.py +9 -6
- lino/modlib/weasyprint/choicelists.py +17 -7
- lino/utils/__init__.py +6 -0
- lino/utils/choosers.py +21 -8
- lino/utils/html.py +1 -1
- lino/utils/instantiator.py +9 -0
- lino/utils/media.py +2 -3
- lino/utils/soup.py +311 -0
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/METADATA +2 -2
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/RECORD +72 -67
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/WHEEL +1 -1
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/COPYING +0 -0
lino/modlib/uploads/models.py
CHANGED
@@ -11,9 +11,13 @@ from django.db import models
|
|
11
11
|
from django.conf import settings
|
12
12
|
from django.core.exceptions import ValidationError
|
13
13
|
from django.utils.text import format_lazy
|
14
|
-
from django.utils.html import format_html
|
14
|
+
from django.utils.html import format_html, mark_safe
|
15
15
|
from django.utils.translation import pgettext_lazy as pgettext
|
16
16
|
|
17
|
+
from rstgen.sphinxconf.sigal_image import parse_image_spec
|
18
|
+
# from rstgen.sphinxconf.sigal_image import Standard, Thumb, Tiny, Wide, Solo, Duo, Trio
|
19
|
+
# SMALL_FORMATS = (Thumb, Tiny, Duo, Trio)
|
20
|
+
|
17
21
|
from lino.utils.html import E, join_elems
|
18
22
|
from lino.api import dd, rt, _
|
19
23
|
from lino.modlib.gfks.mixins import Controllable
|
@@ -29,6 +33,7 @@ from lino.modlib.publisher.mixins import Publishable
|
|
29
33
|
from .actions import CameraStream
|
30
34
|
from .choicelists import Shortcuts, UploadAreas
|
31
35
|
from .mixins import UploadBase
|
36
|
+
from .utils import previewer, UploadMediaFile
|
32
37
|
|
33
38
|
from . import VOLUMES_ROOT
|
34
39
|
|
@@ -36,6 +41,7 @@ from . import VOLUMES_ROOT
|
|
36
41
|
class Volume(Referrable):
|
37
42
|
|
38
43
|
class Meta:
|
44
|
+
abstract = dd.is_abstract_model(__name__, "Volume")
|
39
45
|
app_label = "uploads"
|
40
46
|
verbose_name = _("Library volume")
|
41
47
|
verbose_name_plural = _("Library volumes")
|
@@ -50,10 +56,8 @@ class Volume(Referrable):
|
|
50
56
|
return self.ref or self.root_dir
|
51
57
|
|
52
58
|
def full_clean(self, *args, **kw):
|
53
|
-
# if self.ref == "uploads":
|
54
|
-
# raise ValidationError("Invalid reference for a volume.")
|
55
59
|
super().full_clean(*args, **kw)
|
56
|
-
pth =
|
60
|
+
pth = dd.plugins.uploads.get_volumes_root() / self.ref
|
57
61
|
if pth.exists():
|
58
62
|
if pth.resolve().absolute() != Path(self.root_dir).resolve().absolute():
|
59
63
|
raise ValidationError(
|
@@ -74,8 +78,9 @@ class Volume(Referrable):
|
|
74
78
|
|
75
79
|
|
76
80
|
class UploadType(BabelNamed):
|
77
|
-
class Meta
|
81
|
+
class Meta:
|
78
82
|
abstract = dd.is_abstract_model(__name__, "UploadType")
|
83
|
+
app_label = "uploads"
|
79
84
|
verbose_name = _("Upload type")
|
80
85
|
verbose_name_plural = _("Upload types")
|
81
86
|
|
@@ -105,7 +110,8 @@ class UploadType(BabelNamed):
|
|
105
110
|
|
106
111
|
class Upload(UploadBase, UserAuthored, Controllable, Publishable):
|
107
112
|
|
108
|
-
class Meta
|
113
|
+
class Meta:
|
114
|
+
app_label = "uploads"
|
109
115
|
abstract = dd.is_abstract_model(__name__, "Upload")
|
110
116
|
verbose_name = _("Upload file")
|
111
117
|
verbose_name_plural = _("Upload files")
|
@@ -127,41 +133,52 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
|
|
127
133
|
elif self.file:
|
128
134
|
s = filename_leaf(self.file.name)
|
129
135
|
elif self.library_file:
|
130
|
-
s =
|
136
|
+
s = filename_leaf(self.library_file)
|
137
|
+
# s = "{}:{}".format(self.volume.ref, self.library_file)
|
131
138
|
else:
|
132
139
|
s = str(self.id)
|
133
140
|
if self.type:
|
134
141
|
s = str(self.type) + " " + s
|
135
142
|
return s
|
136
143
|
|
137
|
-
def
|
138
|
-
if
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
else:
|
143
|
-
cmd += "]"
|
144
|
-
return cmd
|
144
|
+
def get_file_path(self):
|
145
|
+
if self.file and self.file.name:
|
146
|
+
return self.file.name
|
147
|
+
elif self.library_file and self.volume_id and self.volume.ref:
|
148
|
+
return VOLUMES_ROOT + "/" + self.volume.ref + "/" + self.library_file
|
145
149
|
return None
|
146
150
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
151
|
+
def get_media_file(self):
|
152
|
+
url = self.get_file_path()
|
153
|
+
if url is not None:
|
154
|
+
return UploadMediaFile(url)
|
155
|
+
|
156
|
+
def get_create_comment_text(self, ar):
|
157
|
+
mf = self.get_media_file()
|
158
|
+
if mf is None:
|
159
|
+
return super().get_create_comment_text(ar)
|
160
|
+
return _("Uploaded an {obj}.").format(obj=mf.get_mimetype_description())
|
161
|
+
# or mf.get_image_url() is None:
|
162
|
+
# return _("Uploaded {obj}. [{obj.memo_command} {obj.id}].").format(obj=self)
|
163
|
+
|
164
|
+
def get_memo_command(self, ar=None):
|
165
|
+
if dd.is_installed("memo"):
|
166
|
+
return f"[{self.memo_command} {self.pk} {self}]"
|
167
|
+
# cmd = f"[{self.memo_command} {self.pk}"
|
168
|
+
# if self.description:
|
169
|
+
# cmd += " " + self.description + "]"
|
170
|
+
# else:
|
171
|
+
# cmd += "]"
|
172
|
+
# return cmd
|
154
173
|
return None
|
155
174
|
|
156
175
|
def get_real_file_size(self):
|
157
176
|
if self.file:
|
158
177
|
return self.file.size
|
159
178
|
if self.volume_id and self.library_file:
|
160
|
-
pth =
|
161
|
-
|
162
|
-
|
163
|
-
# pth = os.path.join(settings.MEDIA_ROOT, self.volume.ref, self.library_file)
|
164
|
-
return os.path.getsize(pth)
|
179
|
+
pth = dd.plugins.uploads.get_volumes_root() / self.volume.ref / self.library_file
|
180
|
+
return pth.stat().st_size
|
181
|
+
# return os.path.getsize(pth)
|
165
182
|
|
166
183
|
def disabled_fields(self, ar):
|
167
184
|
df = super().disabled_fields(ar)
|
@@ -184,16 +201,22 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
|
|
184
201
|
|
185
202
|
@dd.chooser()
|
186
203
|
def type_choices(self, upload_area):
|
187
|
-
|
188
|
-
# logger.info("20140430 type_choices %s", upload_area)
|
204
|
+
UploadType = rt.models.uploads.UploadType
|
189
205
|
if upload_area is None:
|
190
|
-
return
|
191
|
-
return
|
206
|
+
return UploadType.objects.all()
|
207
|
+
return UploadType.objects.filter(upload_area=upload_area)
|
192
208
|
|
193
209
|
def full_clean(self, *args, **kw):
|
194
210
|
super().full_clean(*args, **kw)
|
195
211
|
if self.type is not None:
|
196
212
|
self.upload_area = self.type.upload_area
|
213
|
+
for i in self.check_previews(True):
|
214
|
+
pass
|
215
|
+
|
216
|
+
def check_previews(self, fix):
|
217
|
+
p = rt.models.uploads.previewer
|
218
|
+
for i in p.check_preview(self, fix):
|
219
|
+
yield i
|
197
220
|
|
198
221
|
def get_gallery_item(self, ar):
|
199
222
|
d = super().get_gallery_item(ar)
|
@@ -205,40 +228,45 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
|
|
205
228
|
|
206
229
|
@dd.htmlbox()
|
207
230
|
def preview(self, ar):
|
208
|
-
|
209
|
-
if
|
231
|
+
mf = self.get_media_file()
|
232
|
+
if mf is None:
|
210
233
|
txt = _("No preview available")
|
211
234
|
return '<p style="text-align: center;padding: 2em;">({})</p>'.format(txt)
|
212
|
-
return '<img src="{}" style="max-width: 100%; max-height: 20em">'.format(
|
235
|
+
return '<img src="{}" style="max-width: 100%; max-height: 20em">'.format(mf.get_image_url())
|
213
236
|
|
214
237
|
@dd.htmlbox(_("Thumbnail"))
|
215
238
|
def thumbnail(self, ar):
|
216
239
|
# url = settings.SITE.build_media_url(self.file.name)
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
def thumbnail_medium(self, ar):
|
222
|
-
# url = settings.SITE.build_media_url(self.file.name)
|
223
|
-
url = self.get_file_url()
|
224
|
-
return '<img src="{}" style="width: 30ch;">'.format(url)
|
225
|
-
|
226
|
-
@dd.htmlbox(_("Thumbnail Large"))
|
227
|
-
def thumbnail_large(self, ar):
|
228
|
-
# url = settings.SITE.build_media_url(self.file.name)
|
229
|
-
url = self.get_file_url()
|
230
|
-
return '<img src="{}" style="width: 70ch;">'.format(url)
|
240
|
+
mf = self.get_media_file()
|
241
|
+
if mf is None:
|
242
|
+
return ""
|
243
|
+
return '<img src="{}" style="height: 15ch; max-width: 22.5ch">'.format(mf.get_image_url())
|
231
244
|
|
232
245
|
def as_page(self, ar, **kwargs):
|
233
246
|
yield format_html("<h1>{}</h1>", self)
|
234
|
-
|
235
|
-
|
247
|
+
mf = self.get_media_file()
|
248
|
+
if mf is not None:
|
249
|
+
yield format_html('<img src="{}" style="width: 100%;">', mf.get_image_url())
|
236
250
|
if self.description:
|
237
251
|
yield escape(self.description)
|
238
252
|
if self.source:
|
239
253
|
yield _("Source") + ": "
|
240
254
|
yield ar.obj2htmls(self.source)
|
241
255
|
|
256
|
+
def as_paragraph(self, ar, **kwargs):
|
257
|
+
rv = self.memo2html(ar, "")
|
258
|
+
# rv = ar.obj2htmls(self)
|
259
|
+
# mf = self.get_media_file()
|
260
|
+
# if mf is not None:
|
261
|
+
# src = mf.get_image_url()
|
262
|
+
# if src is not None:
|
263
|
+
# url = mf.get_download_url()
|
264
|
+
# rv += f'<a href="{url}"><img src="{src}" style="width: 30%;"></a>'
|
265
|
+
if self.source:
|
266
|
+
rv += format_html(
|
267
|
+
" ({}: {})", _("Source"), ar.obj2htmls(self.source))
|
268
|
+
return mark_safe(rv)
|
269
|
+
|
242
270
|
# def get_choices_text(self, ar, actor, field):
|
243
271
|
# if self.file:
|
244
272
|
# return str(obj) + " <span style=\"float: right;\">" + obj.thumbnail + "</span>"
|
@@ -246,11 +274,12 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
|
|
246
274
|
|
247
275
|
|
248
276
|
dd.update_field(Upload, "user", verbose_name=_("Uploaded by"))
|
277
|
+
dd.update_field(Upload, "owner", verbose_name=_("Attached to"))
|
249
278
|
|
250
279
|
|
251
280
|
class UploadChecker(Checker):
|
252
281
|
verbose_name = _("Check metadata of upload files")
|
253
|
-
model = Upload
|
282
|
+
model = "uploads.Upload"
|
254
283
|
|
255
284
|
def get_checkdata_problems(self, obj, fix=False):
|
256
285
|
if obj.file:
|
@@ -267,6 +296,8 @@ class UploadChecker(Checker):
|
|
267
296
|
tpl = "Stored file size {} differs from real file size {}"
|
268
297
|
yield (False, format_lazy(tpl, obj.file_size, file_size))
|
269
298
|
|
299
|
+
for i in obj.check_previews(fix):
|
300
|
+
yield i
|
270
301
|
|
271
302
|
UploadChecker.activate()
|
272
303
|
|
@@ -304,7 +335,6 @@ class UploadsFolderChecker(Checker):
|
|
304
335
|
UploadsFolderChecker.activate()
|
305
336
|
|
306
337
|
|
307
|
-
|
308
338
|
@dd.receiver(dd.pre_analyze)
|
309
339
|
def before_analyze(sender, **kwargs):
|
310
340
|
# This is the successor for `quick_upload_buttons`.
|
@@ -313,6 +343,8 @@ def before_analyze(sender, **kwargs):
|
|
313
343
|
UploadType = sender.models.uploads.UploadType
|
314
344
|
Shortcuts = sender.models.uploads.Shortcuts
|
315
345
|
|
346
|
+
# raise Exception(f"20241112 {UploadType}")
|
347
|
+
|
316
348
|
for i in Shortcuts.items():
|
317
349
|
|
318
350
|
def f(obj, ar):
|
@@ -341,16 +373,17 @@ def before_analyze(sender, **kwargs):
|
|
341
373
|
elif n == 1:
|
342
374
|
after_show = ar.get_status()
|
343
375
|
obj = sar.data_iterator[0]
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
376
|
+
if (mf := obj.get_media_file()) is not None:
|
377
|
+
items.append(
|
378
|
+
sar.renderer.href_button(
|
379
|
+
mf.get_download_url(),
|
380
|
+
_("show"),
|
381
|
+
target="_blank",
|
382
|
+
icon_name="page_go",
|
383
|
+
style="vertical-align:-30%;",
|
384
|
+
title=_("Open the uploaded file in a new browser window"),
|
385
|
+
)
|
352
386
|
)
|
353
|
-
)
|
354
387
|
after_show.update(record_id=obj.pk)
|
355
388
|
items.append(
|
356
389
|
sar.window_action_button(
|
@@ -381,4 +414,54 @@ def before_analyze(sender, **kwargs):
|
|
381
414
|
# logger.info("Installed upload shortcut field %s.%s",
|
382
415
|
# i.model_spec, i.name)
|
383
416
|
|
417
|
+
|
384
418
|
from .ui import *
|
419
|
+
|
420
|
+
# raise Exception("20241112")
|
421
|
+
|
422
|
+
|
423
|
+
@dd.receiver(dd.post_startup)
|
424
|
+
def setup_memo_commands(sender=None, **kwargs):
|
425
|
+
# Adds another memo command for Upload
|
426
|
+
# See :doc:`/specs/memo`
|
427
|
+
|
428
|
+
if not sender.is_installed('memo'):
|
429
|
+
return
|
430
|
+
|
431
|
+
def file2html(self, ar, text, **ctx):
|
432
|
+
"""
|
433
|
+
Insert an image tag of the specified upload file.
|
434
|
+
"""
|
435
|
+
mf = self.get_media_file()
|
436
|
+
if mf is None:
|
437
|
+
return format_html("<em>{}</em>", text or str(self))
|
438
|
+
ctx.update(src=mf.get_download_url())
|
439
|
+
ctx.update(href=ar.renderer.obj2url(ar, self))
|
440
|
+
if not mf.is_image():
|
441
|
+
if not text:
|
442
|
+
text = str(self)
|
443
|
+
ctx.update(text=text)
|
444
|
+
tpl = '(<a href="{src}" target="_blank">{text}</a>'
|
445
|
+
tpl += '| <a href="{href}">Detail</a>)'
|
446
|
+
return format_html(tpl, **ctx)
|
447
|
+
|
448
|
+
fmt = parse_image_spec(text, **ctx)
|
449
|
+
# TODO: When an image is inserted with format "wide", we should not use
|
450
|
+
# the thumbnail but the original file. But for a PDF file we must always
|
451
|
+
# use the img_src because the download_url is not an image.
|
452
|
+
fmt.context.update(src=mf.get_image_url())
|
453
|
+
# if isinstance(fmt, SMALL_FORMATS):
|
454
|
+
# fmt.context.update(src=img_src)
|
455
|
+
# else:
|
456
|
+
# print(f"20241116 {fmt} {fmt.context}")
|
457
|
+
|
458
|
+
if not fmt.context["caption"]:
|
459
|
+
fmt.context["caption"] = self.description or str(self)
|
460
|
+
|
461
|
+
rv = format_html(
|
462
|
+
'<a href="{href}" target="_blank"><img src="{src}"'
|
463
|
+
+ ' style="{style}" title="{caption}"/></a>', **fmt.context)
|
464
|
+
return rv
|
465
|
+
|
466
|
+
mp = sender.plugins.memo.parser
|
467
|
+
mp.register_django_model('file', rt.models.uploads.Upload, rnd=file2html)
|
lino/modlib/uploads/ui.py
CHANGED
@@ -66,10 +66,11 @@ class UploadDetail(dd.DetailLayout):
|
|
66
66
|
"""
|
67
67
|
|
68
68
|
left = """
|
69
|
-
file
|
69
|
+
file
|
70
70
|
volume:10 library_file:40
|
71
|
-
|
72
|
-
|
71
|
+
user owner
|
72
|
+
upload_area type
|
73
|
+
description
|
73
74
|
source
|
74
75
|
"""
|
75
76
|
|
@@ -82,6 +83,11 @@ class Uploads(dd.Table):
|
|
82
83
|
required_roles = dd.login_required(UploadsReader)
|
83
84
|
column_names = "file type user owner description id *"
|
84
85
|
order_by = ["-id"]
|
86
|
+
default_display_modes = {
|
87
|
+
70: constants.DISPLAY_MODE_LIST,
|
88
|
+
None: constants.DISPLAY_MODE_GALLERY
|
89
|
+
}
|
90
|
+
# extra_display_modes = {constants.DISPLAY_MODE_LIST, constants.DISPLAY_MODE_GALLERY}
|
85
91
|
|
86
92
|
detail_layout = "uploads.UploadDetail"
|
87
93
|
|
@@ -197,10 +203,10 @@ class AreaUploads(Uploads):
|
|
197
203
|
# icon_name='application_form',
|
198
204
|
title=_("Edit metadata of the uploaded file."),
|
199
205
|
)
|
200
|
-
|
201
|
-
if
|
206
|
+
mf = m.get_media_file()
|
207
|
+
if mf:
|
202
208
|
show = ar.renderer.href_button(
|
203
|
-
|
209
|
+
mf.get_download_url(),
|
204
210
|
# u"\u21A7", # DOWNWARDS ARROW FROM BAR (↧)
|
205
211
|
# u"\u21E8",
|
206
212
|
"\u21f2", # SOUTH EAST ARROW TO CORNER (⇲)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
2
|
+
# Copyright 2010-2024 Rumma & Ko Ltd
|
3
|
+
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
|
+
|
5
|
+
from os.path import splitext
|
6
|
+
from django.conf import settings
|
7
|
+
from django.utils.text import format_lazy
|
8
|
+
from lino.api import dd, rt, _
|
9
|
+
from lino.utils import needs_update
|
10
|
+
|
11
|
+
if (with_thumbnails := dd.get_plugin_setting('uploads', 'with_thumbnails', False)):
|
12
|
+
from PIL import Image # pip install Pillow
|
13
|
+
import pymupdf # pip install PyMuPDF
|
14
|
+
|
15
|
+
class UploadMediaFile:
|
16
|
+
|
17
|
+
def __init__(self, url):
|
18
|
+
self.url = url
|
19
|
+
assert url is not None
|
20
|
+
root, suffix = splitext(url)
|
21
|
+
self.suffix = suffix.lower()
|
22
|
+
|
23
|
+
def get_image_name(self):
|
24
|
+
if self.suffix not in previewer.PREVIEW_SUFFIXES:
|
25
|
+
return None
|
26
|
+
if previewer.base_dir is None:
|
27
|
+
if self.suffix == ".pdf":
|
28
|
+
return None
|
29
|
+
return self.url
|
30
|
+
url = self.url
|
31
|
+
if self.suffix == ".pdf":
|
32
|
+
url += ".png"
|
33
|
+
return previewer.base_dir + "/" + url
|
34
|
+
|
35
|
+
def is_image(self):
|
36
|
+
# whether this can be rendered in an <img> tag
|
37
|
+
if self.get_image_name() is None:
|
38
|
+
return False
|
39
|
+
return self.suffix in previewer.PREVIEW_SUFFIXES
|
40
|
+
|
41
|
+
def get_mimetype_description(self):
|
42
|
+
if self.suffix == ".pdf":
|
43
|
+
return _("PDF file")
|
44
|
+
if self.get_image_name():
|
45
|
+
return _("picture")
|
46
|
+
return _("media file")
|
47
|
+
|
48
|
+
def get_image_url(self):
|
49
|
+
url = self.get_image_name()
|
50
|
+
if url is not None:
|
51
|
+
return settings.SITE.build_media_url(url)
|
52
|
+
|
53
|
+
def get_download_url(self):
|
54
|
+
return settings.SITE.build_media_url(self.url)
|
55
|
+
|
56
|
+
|
57
|
+
class Previewer:
|
58
|
+
# The bare media previewer. It doesn't do any real work.
|
59
|
+
base_dir = None
|
60
|
+
max_width = None
|
61
|
+
PREVIEW_SUFFIXES = {'.png', '.jpg'}
|
62
|
+
|
63
|
+
def check_preview(self, obj, fix=False):
|
64
|
+
return []
|
65
|
+
|
66
|
+
|
67
|
+
class FilePreviewer(Previewer):
|
68
|
+
# A media previewer that builds thumbnails in a separate directory tree
|
69
|
+
PREVIEW_SUFFIXES = {'.png', '.jpg', '.pdf'}
|
70
|
+
|
71
|
+
def __init__(self, base_dir=None, max_width=None):
|
72
|
+
self.base_dir = base_dir
|
73
|
+
self.max_width = max_width
|
74
|
+
super().__init__()
|
75
|
+
|
76
|
+
def check_preview(self, obj, fix=False):
|
77
|
+
mf = obj.get_media_file()
|
78
|
+
if mf is None:
|
79
|
+
return
|
80
|
+
if (dst := mf.get_image_name()) is None:
|
81
|
+
return
|
82
|
+
if dst == mf.url:
|
83
|
+
raise Exception("20241113 should never happen")
|
84
|
+
return
|
85
|
+
src = settings.SITE.media_root / mf.url
|
86
|
+
dst = settings.SITE.media_root / dst
|
87
|
+
|
88
|
+
if needs_update(src, dst):
|
89
|
+
yield (True, format_lazy(_("Must build thumbnail for {}"), mf.url))
|
90
|
+
if fix:
|
91
|
+
if src.suffix.lower() == ".pdf":
|
92
|
+
doc = pymupdf.open(src)
|
93
|
+
page = doc.load_page(0)
|
94
|
+
pixmap = page.get_pixmap(dpi=120)
|
95
|
+
pixmap.save(dst)
|
96
|
+
return
|
97
|
+
with Image.open(src) as im:
|
98
|
+
im.thumbnail((self.max_width, self.max_width))
|
99
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
100
|
+
im.save(dst)
|
101
|
+
|
102
|
+
if with_thumbnails:
|
103
|
+
previewer = FilePreviewer("thumbs", 720)
|
104
|
+
else:
|
105
|
+
previewer = Previewer()
|
106
|
+
|
107
|
+
# full = Previewer()
|
108
|
+
# if with_thumbnails:
|
109
|
+
# small = FilePreviewer("thumbs", 720)
|
110
|
+
# else:
|
111
|
+
# small = full
|
112
|
+
|
113
|
+
# Lino currently offers only two previewers, "full" and "small"
|
lino/modlib/users/mixins.py
CHANGED
@@ -159,13 +159,16 @@ class My(dbtables.Table):
|
|
159
159
|
|
160
160
|
@classmethod
|
161
161
|
def get_actor_label(self):
|
162
|
+
if self._label is not None:
|
163
|
+
return self._label
|
162
164
|
if self.model is None:
|
163
|
-
return self.
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
)
|
165
|
+
return self.__name__
|
166
|
+
return format_lazy(_("My {}"), self.model._meta.verbose_name_plural)
|
167
|
+
|
168
|
+
@classmethod
|
169
|
+
def setup_request(cls, ar):
|
170
|
+
super().setup_request(ar)
|
171
|
+
ar.obvious_fields.add("user")
|
169
172
|
|
170
173
|
@classmethod
|
171
174
|
def param_defaults(self, ar, **kw):
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2016-
|
2
|
+
# Copyright 2016-2024 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
5
|
import os
|
6
|
+
from pathlib import Path
|
6
7
|
from copy import copy
|
7
8
|
|
8
9
|
try:
|
@@ -10,11 +11,20 @@ try:
|
|
10
11
|
except ImportError:
|
11
12
|
HTML = None
|
12
13
|
|
14
|
+
try:
|
15
|
+
import bulma
|
16
|
+
from weasyprint import CSS
|
17
|
+
BULMA_CSS = Path(bulma.__file__).parent / "static/bulma/css/style.min.css"
|
18
|
+
assert BULMA_CSS.exists()
|
19
|
+
except ImportError:
|
20
|
+
BULMA_CSS = None
|
21
|
+
|
22
|
+
|
23
|
+
|
13
24
|
from django.conf import settings
|
14
25
|
from django.utils import translation
|
15
26
|
|
16
27
|
from lino.api import dd
|
17
|
-
|
18
28
|
from lino.modlib.jinja.choicelists import JinjaBuildMethod
|
19
29
|
from lino.modlib.printing.choicelists import BuildMethods
|
20
30
|
|
@@ -29,17 +39,17 @@ class WeasyHtmlBuildMethod(WeasyBuildMethod):
|
|
29
39
|
target_ext = ".html"
|
30
40
|
name = "weasy2html"
|
31
41
|
|
32
|
-
def html2file(self, html, filename):
|
33
|
-
open(filename, "w").write(html)
|
34
|
-
|
35
42
|
|
36
43
|
class WeasyPdfBuildMethod(WeasyBuildMethod):
|
37
44
|
target_ext = ".pdf"
|
38
45
|
name = "weasy2pdf"
|
39
46
|
|
40
|
-
def html2file(self, html, filename):
|
47
|
+
def html2file(self, html, filename, context):
|
41
48
|
pdf = HTML(string=html)
|
42
|
-
|
49
|
+
if BULMA_CSS and context.get('use_bulma_css', False):
|
50
|
+
pdf.write_pdf(filename, stylesheets=[CSS(filename=BULMA_CSS)])
|
51
|
+
else:
|
52
|
+
pdf.write_pdf(filename)
|
43
53
|
|
44
54
|
|
45
55
|
add = BuildMethods.add_item_instance
|
lino/utils/__init__.py
CHANGED
@@ -98,6 +98,12 @@ def buildurl(root, *args, **kw):
|
|
98
98
|
return url
|
99
99
|
|
100
100
|
|
101
|
+
def needs_update(src, dest):
|
102
|
+
if dest.exists() and dest.stat().st_mtime >= src.stat().st_mtime:
|
103
|
+
return False
|
104
|
+
return True
|
105
|
+
|
106
|
+
|
101
107
|
class AttrDict(dict):
|
102
108
|
"""
|
103
109
|
Dictionary-like helper object.
|
lino/utils/choosers.py
CHANGED
@@ -108,6 +108,7 @@ class ChoiceConverter(Converter):
|
|
108
108
|
|
109
109
|
def convert(self, **kw):
|
110
110
|
value = kw.get(self.field.name)
|
111
|
+
# print(f"20241203 convert {self.field.name}")
|
111
112
|
|
112
113
|
if value is not None:
|
113
114
|
if not isinstance(value, self.field.choicelist.item_class):
|
@@ -190,7 +191,16 @@ class ManyToManyConverter(LookupConverter):
|
|
190
191
|
|
191
192
|
def make_converter(f, lookup_fields={}):
|
192
193
|
from lino.core.gfks import GenericForeignKey
|
194
|
+
from lino.core.fields import VirtualField
|
193
195
|
|
196
|
+
# if f.name == 'rating_type':
|
197
|
+
# print(f"20241203 {f.__class__}")
|
198
|
+
|
199
|
+
# selector = f
|
200
|
+
|
201
|
+
if isinstance(f, VirtualField):
|
202
|
+
# print(f"20241203 {f.name} {f.return_type.name}")
|
203
|
+
f = f.return_type
|
194
204
|
if isinstance(f, models.ForeignKey):
|
195
205
|
return ForeignKeyConverter(f, lookup_fields.get(f.name, "pk"))
|
196
206
|
if isinstance(f, GenericForeignKey):
|
@@ -206,8 +216,7 @@ def make_converter(f, lookup_fields={}):
|
|
206
216
|
from lino.core import choicelists
|
207
217
|
|
208
218
|
if isinstance(f, choicelists.ChoiceListField):
|
209
|
-
#
|
210
|
-
# print "20131012 b", f
|
219
|
+
# print(f"20241203c {f.__class__}")
|
211
220
|
return ChoiceConverter(f)
|
212
221
|
|
213
222
|
|
@@ -244,18 +253,22 @@ class Chooser(FieldChooser):
|
|
244
253
|
from lino.core.gfks import is_foreignkey
|
245
254
|
from lino.core.choicelists import ChoiceListField
|
246
255
|
|
247
|
-
|
256
|
+
selector = field
|
257
|
+
if isinstance(field, fields.VirtualField):
|
258
|
+
selector = field.return_type
|
259
|
+
|
260
|
+
if isinstance(selector, ChoiceListField):
|
248
261
|
self.simple_values = getattr(meth, "simple_values", False)
|
249
262
|
self.instance_values = getattr(meth, "instance_values", True)
|
250
263
|
self.force_selection = getattr(
|
251
264
|
meth, "force_selection", self.force_selection
|
252
265
|
)
|
253
|
-
elif is_foreignkey(
|
254
|
-
pass
|
255
|
-
elif isinstance(field, fields.VirtualField) and isinstance(
|
256
|
-
field.return_type, models.ForeignKey
|
257
|
-
):
|
266
|
+
elif is_foreignkey(selector):
|
258
267
|
pass
|
268
|
+
# elif isinstance(field, fields.VirtualField) and isinstance(
|
269
|
+
# field.return_type, models.ForeignKey
|
270
|
+
# ):
|
271
|
+
# pass
|
259
272
|
else:
|
260
273
|
self.simple_values = getattr(meth, "simple_values", False)
|
261
274
|
self.instance_values = getattr(meth, "instance_values", False)
|
lino/utils/html.py
CHANGED
@@ -11,7 +11,7 @@ from etgen.html import E, to_rst, fromstring, iselement, join_elems, forcetext,
|
|
11
11
|
|
12
12
|
# from etgen.html import tostring as et_tostring
|
13
13
|
from html2text import HTML2Text
|
14
|
-
from django.utils.html import SafeString, mark_safe, escape
|
14
|
+
from django.utils.html import SafeString, mark_safe, escape, format_html
|
15
15
|
# from lino.utils import tostring
|
16
16
|
|
17
17
|
SAFE_EMPTY = mark_safe("")
|
lino/utils/instantiator.py
CHANGED
@@ -210,6 +210,15 @@ def create_and_get(model, **kw):
|
|
210
210
|
o = create(model, **kw)
|
211
211
|
return model.objects.get(pk=o.pk)
|
212
212
|
|
213
|
+
def make_if_needed(model, **values):
|
214
|
+
qs = model.objects.filter(**values)
|
215
|
+
if qs.count() == 1:
|
216
|
+
pass # ok, nothing to do
|
217
|
+
elif qs.count() == 0:
|
218
|
+
return model(**values)
|
219
|
+
else:
|
220
|
+
raise Exception(f"Multiple {model._meta.verbose_name_plural} for {values}")
|
221
|
+
|
213
222
|
|
214
223
|
def _test():
|
215
224
|
import doctest
|