lino 24.11.0__py3-none-any.whl → 24.11.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 (54) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actions.py +2 -4
  3. lino/core/actors.py +2 -2
  4. lino/core/dbtables.py +12 -12
  5. lino/core/fields.py +1 -0
  6. lino/core/inject.py +9 -2
  7. lino/core/kernel.py +7 -6
  8. lino/core/model.py +24 -27
  9. lino/core/renderer.py +2 -2
  10. lino/core/requests.py +21 -45
  11. lino/core/site.py +4 -45
  12. lino/core/store.py +4 -4
  13. lino/core/utils.py +5 -2
  14. lino/help_texts.py +4 -3
  15. lino/locale/bn/LC_MESSAGES/django.po +1210 -907
  16. lino/locale/de/LC_MESSAGES/django.po +1760 -1375
  17. lino/locale/django.pot +1136 -906
  18. lino/locale/es/LC_MESSAGES/django.po +1709 -1347
  19. lino/locale/et/LC_MESSAGES/django.po +1206 -906
  20. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  21. lino/locale/fr/LC_MESSAGES/django.po +1193 -923
  22. lino/locale/nl/LC_MESSAGES/django.po +1247 -942
  23. lino/locale/pt_BR/LC_MESSAGES/django.po +1190 -903
  24. lino/locale/zh_Hant/LC_MESSAGES/django.po +1190 -903
  25. lino/mixins/periods.py +6 -4
  26. lino/modlib/comments/choicelists.py +1 -1
  27. lino/modlib/comments/fixtures/demo2.py +4 -1
  28. lino/modlib/comments/mixins.py +9 -10
  29. lino/modlib/comments/models.py +4 -4
  30. lino/modlib/comments/ui.py +5 -0
  31. lino/modlib/linod/mixins.py +3 -2
  32. lino/modlib/memo/mixins.py +10 -208
  33. lino/modlib/notify/mixins.py +33 -32
  34. lino/modlib/periods/mixins.py +0 -1
  35. lino/modlib/printing/choicelists.py +3 -3
  36. lino/modlib/search/models.py +17 -11
  37. lino/modlib/system/fixtures/__init__.py +0 -0
  38. lino/modlib/system/fixtures/std.py +5 -0
  39. lino/modlib/system/models.py +3 -2
  40. lino/modlib/uploads/__init__.py +10 -1
  41. lino/modlib/uploads/choicelists.py +3 -3
  42. lino/modlib/uploads/mixins.py +30 -32
  43. lino/modlib/uploads/models.py +89 -56
  44. lino/modlib/uploads/ui.py +12 -6
  45. lino/modlib/uploads/utils.py +107 -0
  46. lino/utils/__init__.py +6 -0
  47. lino/utils/html.py +1 -1
  48. lino/utils/media.py +2 -3
  49. lino/utils/soup.py +311 -0
  50. {lino-24.11.0.dist-info → lino-24.11.1.dist-info}/METADATA +1 -3
  51. {lino-24.11.0.dist-info → lino-24.11.1.dist-info}/RECORD +54 -50
  52. {lino-24.11.0.dist-info → lino-24.11.1.dist-info}/WHEEL +1 -1
  53. {lino-24.11.0.dist-info → lino-24.11.1.dist-info}/licenses/AUTHORS.rst +0 -0
  54. {lino-24.11.0.dist-info → lino-24.11.1.dist-info}/licenses/COPYING +0 -0
@@ -46,14 +46,14 @@ class BuildMethod(Choice):
46
46
  names, self.__class__.__name__, names, **kwargs
47
47
  )
48
48
 
49
- def get_target(self, action, elem):
50
- "used by `get_target_name`"
49
+ def get_target(self, action, obj):
50
+ # Used by get_target_name()
51
51
  # assert self.name is not None
52
52
  return MediaFile(
53
53
  self.use_webdav,
54
54
  self.cache_name,
55
55
  self.value,
56
- elem.filename_root() + self.target_ext,
56
+ obj.filename_root() + self.target_ext,
57
57
  )
58
58
 
59
59
  def get_target_name(self, action, elem):
@@ -1,9 +1,8 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2021-2023 Rumma & Ko Ltd
2
+ # Copyright 2021-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import re
6
- from html import escape
7
6
  from lxml import etree
8
7
  from django.conf import settings
9
8
 
@@ -12,7 +11,7 @@ from lino.core import constants
12
11
  from lino.core.utils import get_models
13
12
  from lino.core.site import has_elasticsearch, has_haystack
14
13
 
15
- from lino.utils.html import E
14
+ from lino.utils.html import E, escape, mark_safe, format_html
16
15
 
17
16
  from .roles import SiteSearcher
18
17
 
@@ -23,15 +22,17 @@ class SiteSearchBase(dd.VirtualTable):
23
22
  required_roles = dd.login_required(SiteSearcher)
24
23
  label = _("Search")
25
24
  column_names = "description matches"
25
+ # column_names = "search_overview matches"
26
26
  private_apps = frozenset(["sessions", "contenttypes", "users"])
27
27
 
28
- default_display_modes = {None: constants.DISPLAY_MODE_STORY}
28
+ default_display_modes = {None: constants.DISPLAY_MODE_LIST}
29
+ # default_display_modes = {None: constants.DISPLAY_MODE_STORY}
29
30
 
30
31
  card_layout = """description"""
31
- list_layout = """
32
- search_overview
33
- matches
34
- """
32
+ # list_layout = """
33
+ # search_overview
34
+ # matches
35
+ # """
35
36
 
36
37
  # _site_search_tables = []
37
38
  # @classmethod
@@ -76,6 +77,11 @@ class SiteSearchBase(dd.VirtualTable):
76
77
  return False
77
78
  return True
78
79
 
80
+ @classmethod
81
+ def row_as_paragraph(cls, ar, obj):
82
+ text = "{} #{}".format(obj._meta.verbose_name, obj.pk)
83
+ return format_html("{} : {}", ar.obj2htmls(obj, text), obj.as_paragraph(ar))
84
+
79
85
  @dd.displayfield(_("Description"))
80
86
  def description(self, obj, ar):
81
87
  elems = []
@@ -110,7 +116,8 @@ class SiteSearchBase(dd.VirtualTable):
110
116
  s = matches.get(de, None)
111
117
  if s is None:
112
118
  s = str(de.value_from_object(obj))
113
- s = escape(s, quote=False)
119
+ # s = escape(s, quote=False)
120
+ s = escape(s)
114
121
  r, count = re.subn(w, bold, s, flags=re.IGNORECASE)
115
122
  if count:
116
123
  matches[de] = r
@@ -132,11 +139,10 @@ class SiteSearchBase(dd.VirtualTable):
132
139
  raise Exception("{} : {}".format(e, s))
133
140
  # return etree.fromstring(', '.join(chunks))
134
141
  # return E.raw(', '.join(chunks))
135
- return s
142
+ return mark_safe(s)
136
143
 
137
144
 
138
145
  class SiteSearch(SiteSearchBase):
139
- default_display_modes = {None: constants.DISPLAY_MODE_LIST}
140
146
 
141
147
  @classmethod
142
148
  def get_data_rows(cls, ar):
File without changes
@@ -0,0 +1,5 @@
1
+ from django.conf import settings
2
+
3
+ def objects():
4
+ settings.SITE.site_config.update()
5
+ return []
@@ -138,7 +138,7 @@ class SiteConfig(dd.Model):
138
138
  # print("20180502 Created SiteConfig {}".format(
139
139
  # obj2str(self._site_config, True)))
140
140
  # 20120725
141
- # polls_tutorial menu selection `Config --> Site Parameters`
141
+ # polls_tutorial menu selection `Config --> Site configuration`
142
142
  # said "SiteConfig 1 does not exist"
143
143
  # cannot save the instance here because the db table possibly doesn't yet exit.
144
144
  # ~ self._site_config.save()
@@ -146,7 +146,7 @@ class SiteConfig(dd.Model):
146
146
  return cls._site_config
147
147
 
148
148
  def __str__(self):
149
- return force_str(_("Site Parameters"))
149
+ return force_str(_("Site configuration"))
150
150
 
151
151
  def update(self, **kw):
152
152
  """
@@ -238,6 +238,7 @@ class SiteConfigs(dd.Table):
238
238
  detail_layout = dd.DetailLayout(
239
239
  """
240
240
  default_build_method
241
+ simulate_today
241
242
  # lino.ModelsBySite
242
243
  """,
243
244
  window_size=(60, "auto"),
@@ -9,7 +9,7 @@ from os.path import join
9
9
  from lino import ad, _
10
10
  from lino.modlib.memo.parser import split_name_rest
11
11
 
12
- UPLOADS_ROOT = "uploads"
12
+ UPLOADS_ROOT = 'uploads'
13
13
  VOLUMES_ROOT = 'volumes'
14
14
 
15
15
  class Plugin(ad.Plugin):
@@ -25,6 +25,10 @@ class Plugin(ad.Plugin):
25
25
 
26
26
  """
27
27
 
28
+ with_thumbnails = False
29
+ """Whether to use PIL, the Python Imaging Library.
30
+ """
31
+
28
32
  # def on_ui_init(self, kernel):
29
33
  # super().on_ui_init(kernel)
30
34
  # self.site.makedirs_if_missing(self.get_uploads_root())
@@ -75,3 +79,8 @@ class Plugin(ad.Plugin):
75
79
 
76
80
  # site.makedirs_if_missing(self.get_uploads_root())
77
81
  # site.makedirs_if_missing(self.get_volumes_root())
82
+
83
+ def get_requirements(self, site):
84
+ if self.with_thumbnails:
85
+ yield "Pillow"
86
+ yield "PyMuPDF"
@@ -1,9 +1,9 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2010-2020 Rumma & Ko Ltd
2
+ # Copyright 2010-2024 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from lino.modlib.office.roles import OfficeStaff
6
5
  from lino.api import dd, rt, _
6
+ from lino.modlib.office.roles import OfficeStaff
7
7
 
8
8
 
9
9
  class Shortcut(dd.Choice):
@@ -17,7 +17,7 @@ class Shortcut(dd.Choice):
17
17
  self.target = target
18
18
  self.model_spec = model_spec
19
19
  value = model_spec + "." + name
20
- super(Shortcut, self).__init__(value, verbose_name, name)
20
+ super().__init__(value, verbose_name, name)
21
21
 
22
22
  def get_uploads(self, **kw):
23
23
  """Return a queryset with the uploads of this shortcut."""
@@ -7,19 +7,24 @@ import shutil
7
7
  import uuid
8
8
  from pathlib import Path
9
9
 
10
+ from rstgen.sphinxconf.sigal_image import parse_image_spec
11
+ from rstgen.sphinxconf.sigal_image import Standard, Thumb, Tiny, Wide, Solo, Duo, Trio
12
+ SMALL_FORMATS = (Thumb, Tiny, Duo, Trio)
13
+
10
14
  from django.db import models
11
15
  from django.conf import settings
12
16
  from django.core.files.storage import default_storage
13
17
  from django.core.exceptions import ValidationError, FieldError
14
18
  from django.template.defaultfilters import filesizeformat
19
+ from django.utils.html import format_html
15
20
 
16
- from rstgen.sphinxconf.sigal_image import parse_image_spec
17
21
  from lino.core import constants
18
22
  from lino.utils import DATE_TO_DIR_TPL
23
+ from lino.utils import needs_update
19
24
  from lino.utils.html import E
20
25
  from lino.api import dd, rt, _
21
26
  from lino.modlib.uploads import UPLOADS_ROOT
22
- from lino.modlib.memo.mixins import MemoReferrable
27
+ from lino.modlib.comments.mixins import Commentable
23
28
 
24
29
  # from lino.mixins.sequenced import Sequenced
25
30
  # from lino.modlib.gfks.mixins import Controllable
@@ -39,12 +44,6 @@ def safe_filename(name):
39
44
  return name
40
45
 
41
46
 
42
- def needs_update(src, dest):
43
- if dest.exists() and dest.stat().st_mtime >= src.stat().st_mtime:
44
- return False
45
- return True
46
-
47
-
48
47
  def make_uploaded_file(filename, src=None, upload_date=None):
49
48
  """
50
49
  Create a dummy file that looks as if a file had really been uploaded.
@@ -130,8 +129,9 @@ class GalleryViewable(dd.Model):
130
129
  return {}
131
130
 
132
131
 
133
- class UploadBase(MemoReferrable, GalleryViewable):
134
- class Meta(object):
132
+ class UploadBase(Commentable, GalleryViewable):
133
+
134
+ class Meta:
135
135
  abstract = True
136
136
 
137
137
  extra_display_modes = {constants.DISPLAY_MODE_GALLERY}
@@ -180,7 +180,8 @@ class UploadBase(MemoReferrable, GalleryViewable):
180
180
  dd.logger.info("Wrote uploaded file %s", ff.path)
181
181
 
182
182
  def get_gallery_item(self, ar):
183
- return dict(image_src=self.get_file_url())
183
+ mf = self.get_media_file()
184
+ return dict(image_src=mf.get_image_url())
184
185
 
185
186
  def full_clean(self, *args, **kw):
186
187
  super().full_clean(*args, **kw)
@@ -192,39 +193,36 @@ class UploadBase(MemoReferrable, GalleryViewable):
192
193
  def get_file_button(self, text=None):
193
194
  if text is None:
194
195
  text = str(self)
195
- url = self.get_file_url()
196
- if url is None:
196
+ mf = self.get_media_file()
197
+ if mf is None:
197
198
  return text
198
- return E.a(text, href=url, target="_blank")
199
+ return E.a(text, href=mf.get_download_url(), target="_blank")
199
200
 
200
201
  def memo2html(self, ar, text, **ctx):
201
- # if not text:
202
- # text = self.description or str(self)
203
- # ctx = dict()
204
- # text = src + "|" + text
205
- url = self.get_file_url()
206
- if url is None:
207
- return ""
208
- ctx.update(src=url)
202
+ mf = self.get_media_file()
203
+ if mf is None:
204
+ return format_html("<em>{}</em>", text or str(self))
205
+ ctx.update(src=mf.get_download_url())
209
206
  ctx.update(href=ar.renderer.obj2url(ar, self))
210
- if url.endswith(".pdf"):
207
+ small_url = mf.get_image_url()
208
+ if small_url is None or small_url == mf.url: # non-previewable mimetype
211
209
  if not text:
212
210
  text = str(self)
213
211
  ctx.update(text=text)
214
- tpl = '(<a href="{src}" target="_blank"/>{text}</a>'
215
- tpl += '| <a href="{href}""/>detail</a>)'
216
- return tpl.format(**ctx)
212
+ tpl = '(<a href="{src}" target="_blank">{text}</a>'
213
+ tpl += '| <a href="{href}">detail</a>)'
214
+ return format_html(tpl, **ctx)
217
215
 
218
216
  fmt = parse_image_spec(text, **ctx)
217
+ if isinstance(fmt, SMALL_FORMATS):
218
+ fmt.context.update(src=small_url)
219
+
219
220
  if not fmt.context["caption"]:
220
221
  fmt.context["caption"] = self.description or str(self)
221
- # if text:
222
- # ctx.update(caption=text)
223
222
 
224
- rv = (
225
- '<a href="{href}" target="_blank"/><img src="{src}"'
226
- + ' style="{style}" title="{caption}"/></a>'
227
- ).format(**fmt.context)
223
+ rv = format_html(
224
+ '<a href="{href}" target="_blank"><img src="{src}"'
225
+ + ' style="{style}" title="{caption}"/></a>', **fmt.context)
228
226
  # if ar.renderer.front_end.media_name == 'react':
229
227
  # return ('<figure class="lino-memo-image"><img src="{src}" ' +
230
228
  # 'style="{style}" title="{caption}"/><figcaption' +
@@ -11,7 +11,7 @@ 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
17
  from lino.utils.html import E, join_elems
@@ -29,6 +29,7 @@ from lino.modlib.publisher.mixins import Publishable
29
29
  from .actions import CameraStream
30
30
  from .choicelists import Shortcuts, UploadAreas
31
31
  from .mixins import UploadBase
32
+ from .utils import previewer, UploadMediaFile
32
33
 
33
34
  from . import VOLUMES_ROOT
34
35
 
@@ -36,6 +37,7 @@ from . import VOLUMES_ROOT
36
37
  class Volume(Referrable):
37
38
 
38
39
  class Meta:
40
+ abstract = dd.is_abstract_model(__name__, "Volume")
39
41
  app_label = "uploads"
40
42
  verbose_name = _("Library volume")
41
43
  verbose_name_plural = _("Library volumes")
@@ -50,10 +52,8 @@ class Volume(Referrable):
50
52
  return self.ref or self.root_dir
51
53
 
52
54
  def full_clean(self, *args, **kw):
53
- # if self.ref == "uploads":
54
- # raise ValidationError("Invalid reference for a volume.")
55
55
  super().full_clean(*args, **kw)
56
- pth = Path(dd.plugins.uploads.get_volumes_root(), self.ref)
56
+ pth = dd.plugins.uploads.get_volumes_root() / self.ref
57
57
  if pth.exists():
58
58
  if pth.resolve().absolute() != Path(self.root_dir).resolve().absolute():
59
59
  raise ValidationError(
@@ -74,8 +74,9 @@ class Volume(Referrable):
74
74
 
75
75
 
76
76
  class UploadType(BabelNamed):
77
- class Meta(object):
77
+ class Meta:
78
78
  abstract = dd.is_abstract_model(__name__, "UploadType")
79
+ app_label = "uploads"
79
80
  verbose_name = _("Upload type")
80
81
  verbose_name_plural = _("Upload types")
81
82
 
@@ -105,12 +106,13 @@ class UploadType(BabelNamed):
105
106
 
106
107
  class Upload(UploadBase, UserAuthored, Controllable, Publishable):
107
108
 
108
- class Meta(object):
109
+ class Meta:
110
+ app_label = "uploads"
109
111
  abstract = dd.is_abstract_model(__name__, "Upload")
110
112
  verbose_name = _("Upload file")
111
113
  verbose_name_plural = _("Upload files")
112
114
 
113
- memo_command = "upload"
115
+ memo_command = "file"
114
116
 
115
117
  upload_area = UploadAreas.field(default="general")
116
118
  type = dd.ForeignKey("uploads.UploadType", blank=True, null=True)
@@ -134,9 +136,29 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
134
136
  s = str(self.type) + " " + s
135
137
  return s
136
138
 
139
+ def get_file_path(self):
140
+ if self.file and self.file.name:
141
+ return self.file.name
142
+ elif self.library_file and self.volume_id and self.volume.ref:
143
+ return VOLUMES_ROOT + "/" + self.volume.ref + "/" + self.library_file
144
+ return None
145
+
146
+ def get_media_file(self):
147
+ url = self.get_file_path()
148
+ if url is not None:
149
+ return UploadMediaFile(url)
150
+
151
+ def get_create_comment_text(self, ar):
152
+ mf = self.get_media_file()
153
+ if mf is None:
154
+ return super().get_create_comment_text(ar)
155
+ return _("Uploaded an {obj}.").format(obj=mf.get_mimetype_description())
156
+ # or mf.get_image_url() is None:
157
+ # return _("Uploaded {obj}. [{obj.memo_command} {obj.id}].").format(obj=self)
158
+
137
159
  def get_memo_command(self, ar=None):
138
160
  if dd.is_installed("memo"):
139
- cmd = f"[upload {self.pk}"
161
+ cmd = f"[{self.memo_command} {self.pk}"
140
162
  if self.description:
141
163
  cmd += " " + self.description + "]"
142
164
  else:
@@ -144,24 +166,13 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
144
166
  return cmd
145
167
  return None
146
168
 
147
- def get_file_url(self):
148
- if self.file.name:
149
- return settings.SITE.build_media_url(self.file.name)
150
- if self.library_file and self.volume_id and self.volume.ref:
151
- return settings.SITE.build_media_url(
152
- VOLUMES_ROOT, self.volume.ref, self.library_file)
153
- # return self.volume.base_url + self.library_file
154
- return None
155
-
156
169
  def get_real_file_size(self):
157
170
  if self.file:
158
171
  return self.file.size
159
172
  if self.volume_id and self.library_file:
160
- pth = os.path.join(
161
- dd.plugins.uploads.get_volumes_root(),
162
- self.volume.ref, self.library_file)
163
- # pth = os.path.join(settings.MEDIA_ROOT, self.volume.ref, self.library_file)
164
- return os.path.getsize(pth)
173
+ pth = dd.plugins.uploads.get_volumes_root() / self.volume.ref / self.library_file
174
+ return pth.stat().st_size
175
+ # return os.path.getsize(pth)
165
176
 
166
177
  def disabled_fields(self, ar):
167
178
  df = super().disabled_fields(ar)
@@ -169,6 +180,16 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
169
180
  df.add("camera_stream")
170
181
  return df
171
182
 
183
+ def full_clean(self):
184
+ super().full_clean()
185
+ for i in self.check_previews(True):
186
+ pass
187
+
188
+ def check_previews(self, fix):
189
+ for p in [rt.models.uploads.previewer]:
190
+ for i in p.check_preview(self, fix):
191
+ yield i
192
+
172
193
  @dd.displayfield(_("Description"))
173
194
  def description_link(self, ar):
174
195
  s = str(self)
@@ -184,11 +205,10 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
184
205
 
185
206
  @dd.chooser()
186
207
  def type_choices(self, upload_area):
187
- M = dd.resolve_model("uploads.UploadType")
188
- # logger.info("20140430 type_choices %s", upload_area)
208
+ UploadType = rt.models.uploads.UploadType
189
209
  if upload_area is None:
190
- return M.objects.all()
191
- return M.objects.filter(upload_area=upload_area)
210
+ return UploadType.objects.all()
211
+ return UploadType.objects.filter(upload_area=upload_area)
192
212
 
193
213
  def full_clean(self, *args, **kw):
194
214
  super().full_clean(*args, **kw)
@@ -205,40 +225,45 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
205
225
 
206
226
  @dd.htmlbox()
207
227
  def preview(self, ar):
208
- url = self.get_file_url()
209
- if url is None or url.endswith(".pdf"):
228
+ mf = self.get_media_file()
229
+ if mf is None:
210
230
  txt = _("No preview available")
211
231
  return '<p style="text-align: center;padding: 2em;">({})</p>'.format(txt)
212
- return '<img src="{}" style="max-width: 100%; max-height: 20em">'.format(url)
232
+ return '<img src="{}" style="max-width: 100%; max-height: 20em">'.format(mf.get_image_url())
213
233
 
214
234
  @dd.htmlbox(_("Thumbnail"))
215
235
  def thumbnail(self, ar):
216
236
  # url = settings.SITE.build_media_url(self.file.name)
217
- url = self.get_file_url()
218
- return '<img src="{}" style="height: 15ch; max-width: 22.5ch">'.format(url)
219
-
220
- @dd.htmlbox(_("Thumbnail Medium"))
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)
237
+ mf = self.get_media_file()
238
+ if mf is None:
239
+ return ""
240
+ return '<img src="{}" style="height: 15ch; max-width: 22.5ch">'.format(mf.get_image_url())
231
241
 
232
242
  def as_page(self, ar, **kwargs):
233
243
  yield format_html("<h1>{}</h1>", self)
234
- url = self.get_file_url()
235
- yield format_html('<img src="{}" style="width: 100%;">', url)
244
+ mf = self.get_media_file()
245
+ if mf is not None:
246
+ yield format_html('<img src="{}" style="width: 100%;">', mf.get_image_url())
236
247
  if self.description:
237
248
  yield escape(self.description)
238
249
  if self.source:
239
250
  yield _("Source") + ": "
240
251
  yield ar.obj2htmls(self.source)
241
252
 
253
+ def as_paragraph(self, ar, **kwargs):
254
+ rv = self.memo2html(ar, "")
255
+ # rv = ar.obj2htmls(self)
256
+ # mf = self.get_media_file()
257
+ # if mf is not None:
258
+ # src = mf.get_image_url()
259
+ # if src is not None:
260
+ # url = mf.get_download_url()
261
+ # rv += f'<a href="{url}"><img src="{src}" style="width: 30%;"></a>'
262
+ if self.source:
263
+ rv += format_html(
264
+ " ({}: {})", _("Source"), ar.obj2htmls(self.source))
265
+ return mark_safe(rv)
266
+
242
267
  # def get_choices_text(self, ar, actor, field):
243
268
  # if self.file:
244
269
  # return str(obj) + "&nbsp;<span style=\"float: right;\">" + obj.thumbnail + "</span>"
@@ -246,11 +271,12 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
246
271
 
247
272
 
248
273
  dd.update_field(Upload, "user", verbose_name=_("Uploaded by"))
274
+ dd.update_field(Upload, "owner", verbose_name=_("Attached to"))
249
275
 
250
276
 
251
277
  class UploadChecker(Checker):
252
278
  verbose_name = _("Check metadata of upload files")
253
- model = Upload
279
+ model = "uploads.Upload"
254
280
 
255
281
  def get_checkdata_problems(self, obj, fix=False):
256
282
  if obj.file:
@@ -267,6 +293,8 @@ class UploadChecker(Checker):
267
293
  tpl = "Stored file size {} differs from real file size {}"
268
294
  yield (False, format_lazy(tpl, obj.file_size, file_size))
269
295
 
296
+ for i in obj.check_previews(fix):
297
+ yield i
270
298
 
271
299
  UploadChecker.activate()
272
300
 
@@ -304,7 +332,6 @@ class UploadsFolderChecker(Checker):
304
332
  UploadsFolderChecker.activate()
305
333
 
306
334
 
307
-
308
335
  @dd.receiver(dd.pre_analyze)
309
336
  def before_analyze(sender, **kwargs):
310
337
  # This is the successor for `quick_upload_buttons`.
@@ -313,6 +340,8 @@ def before_analyze(sender, **kwargs):
313
340
  UploadType = sender.models.uploads.UploadType
314
341
  Shortcuts = sender.models.uploads.Shortcuts
315
342
 
343
+ # raise Exception(f"20241112 {UploadType}")
344
+
316
345
  for i in Shortcuts.items():
317
346
 
318
347
  def f(obj, ar):
@@ -341,16 +370,17 @@ def before_analyze(sender, **kwargs):
341
370
  elif n == 1:
342
371
  after_show = ar.get_status()
343
372
  obj = sar.data_iterator[0]
344
- items.append(
345
- sar.renderer.href_button(
346
- obj.get_file_url(),
347
- _("show"),
348
- target="_blank",
349
- icon_name="page_go",
350
- style="vertical-align:-30%;",
351
- title=_("Open the uploaded file in a new browser window"),
373
+ if (mf := obj.get_media_file()) is not None:
374
+ items.append(
375
+ sar.renderer.href_button(
376
+ mf.get_download_url(),
377
+ _("show"),
378
+ target="_blank",
379
+ icon_name="page_go",
380
+ style="vertical-align:-30%;",
381
+ title=_("Open the uploaded file in a new browser window"),
382
+ )
352
383
  )
353
- )
354
384
  after_show.update(record_id=obj.pk)
355
385
  items.append(
356
386
  sar.window_action_button(
@@ -381,4 +411,7 @@ def before_analyze(sender, **kwargs):
381
411
  # logger.info("Installed upload shortcut field %s.%s",
382
412
  # i.model_spec, i.name)
383
413
 
414
+
384
415
  from .ui import *
416
+
417
+ # raise Exception("20241112")
lino/modlib/uploads/ui.py CHANGED
@@ -66,10 +66,11 @@ class UploadDetail(dd.DetailLayout):
66
66
  """
67
67
 
68
68
  left = """
69
- file user
69
+ file
70
70
  volume:10 library_file:40
71
- upload_area type description
72
- owner
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
- url = m.get_file_url()
201
- if url:
206
+ mf = m.get_media_file()
207
+ if mf:
202
208
  show = ar.renderer.href_button(
203
- url,
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 (⇲)