lino 24.11.1__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.
Files changed (51) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actors.py +33 -12
  3. lino/core/fields.py +26 -18
  4. lino/core/kernel.py +4 -6
  5. lino/core/model.py +5 -16
  6. lino/core/renderer.py +1 -2
  7. lino/core/requests.py +70 -47
  8. lino/core/site.py +1 -1
  9. lino/core/tables.py +0 -16
  10. lino/help_texts.py +4 -3
  11. lino/locale/bn/LC_MESSAGES/django.po +58 -45
  12. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  13. lino/locale/de/LC_MESSAGES/django.po +79 -108
  14. lino/locale/django.pot +55 -44
  15. lino/locale/es/LC_MESSAGES/django.po +56 -44
  16. lino/locale/et/LC_MESSAGES/django.po +58 -45
  17. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  18. lino/locale/fr/LC_MESSAGES/django.po +60 -48
  19. lino/locale/nl/LC_MESSAGES/django.po +59 -45
  20. lino/locale/pt_BR/LC_MESSAGES/django.po +55 -44
  21. lino/locale/zh_Hant/LC_MESSAGES/django.po +55 -44
  22. lino/mixins/dupable.py +8 -7
  23. lino/mixins/periods.py +4 -4
  24. lino/modlib/checkdata/__init__.py +1 -1
  25. lino/modlib/comments/ui.py +7 -3
  26. lino/modlib/dupable/models.py +10 -14
  27. lino/modlib/gfks/mixins.py +8 -1
  28. lino/modlib/jinja/choicelists.py +3 -3
  29. lino/modlib/jinja/renderer.py +2 -0
  30. lino/modlib/languages/fixtures/all_languages.py +4 -6
  31. lino/modlib/memo/mixins.py +7 -7
  32. lino/modlib/memo/models.py +41 -12
  33. lino/modlib/memo/parser.py +7 -3
  34. lino/modlib/notify/mixins.py +8 -8
  35. lino/modlib/periods/choicelists.py +46 -0
  36. lino/modlib/periods/mixins.py +26 -0
  37. lino/modlib/periods/models.py +17 -45
  38. lino/modlib/publisher/choicelists.py +1 -4
  39. lino/modlib/uploads/mixins.py +37 -37
  40. lino/modlib/uploads/models.py +68 -18
  41. lino/modlib/uploads/utils.py +6 -0
  42. lino/modlib/users/mixins.py +9 -6
  43. lino/modlib/weasyprint/choicelists.py +17 -7
  44. lino/utils/choosers.py +21 -8
  45. lino/utils/instantiator.py +9 -0
  46. lino/utils/soup.py +5 -5
  47. {lino-24.11.1.dist-info → lino-25.1.0.dist-info}/METADATA +4 -2
  48. {lino-24.11.1.dist-info → lino-25.1.0.dist-info}/RECORD +51 -50
  49. {lino-24.11.1.dist-info → lino-25.1.0.dist-info}/WHEEL +1 -1
  50. {lino-24.11.1.dist-info → lino-25.1.0.dist-info}/licenses/AUTHORS.rst +0 -0
  51. {lino-24.11.1.dist-info → lino-25.1.0.dist-info}/licenses/COPYING +0 -0
@@ -159,7 +159,7 @@ class Parser:
159
159
  )
160
160
  self.commands[cmdname] = func
161
161
 
162
- def register_django_model(self, name, model, cmd=None):
162
+ def register_django_model(self, name, model, cmd=None, rnd=None):
163
163
  """
164
164
  Register the given string `name` as command for referring to
165
165
  database rows of the given Django database model `model`.
@@ -172,6 +172,8 @@ class Parser:
172
172
  # if rnd is None:
173
173
  # def rnd(obj):
174
174
  # return "[{} {}] ({})".format(name, obj.id, title(obj))
175
+ if rnd is None:
176
+ rnd = model.memo2html
175
177
  if cmd is None:
176
178
 
177
179
  def cmd(ar, s, cmdname, mentions, context):
@@ -200,7 +202,8 @@ class Parser:
200
202
  # caption = obj.get_memo_title()
201
203
  # txt = "#{0}".format(obj.id)
202
204
  # kw.update(title=title(obj))
203
- return obj.memo2html(ar, text)
205
+ # return obj.memo2html(ar, text)
206
+ return rnd(obj, ar, text)
204
207
  # e = ar.obj2html(obj, txt, **kw)
205
208
  # # return str(ar)
206
209
  # return etree.tostring(e)
@@ -210,7 +213,8 @@ class Parser:
210
213
  # pass
211
214
 
212
215
  cmd._for_model = model
213
- cmd.__doc__ = """
216
+ if cmd.__doc__ is None:
217
+ cmd.__doc__ = rnd.__doc__ or """
214
218
  Insert a reference to the specified {}.
215
219
 
216
220
  The first argument is mandatory and specifies the primary key.
@@ -1,7 +1,7 @@
1
- # Copyright 2016-2022 Rumma & Ko Ltd
1
+ # Copyright 2016-2024 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
3
 
4
- from lino.utils.html import E, tostring
4
+ from lino.utils.html import E, tostring, format_html, mark_safe
5
5
  from lino.api import dd, rt, _
6
6
 
7
7
  # PUBLIC_GROUP = "all_users_channel"
@@ -27,21 +27,21 @@ class ChangeNotifier(dd.Model):
27
27
  def get_change_body(self, ar, cw):
28
28
  ctx = dict(user=ar.user, what=ar.obj2htmls(self))
29
29
  if cw is None:
30
- html = _("{user} created {what}").format(**ctx)
30
+ html = format_html(_("{user} created {what}"), **ctx)
31
31
  html += self.get_change_info(ar, cw)
32
- html = "<p>{}</p>.".format(html)
32
+ html = format_html("<p>{}</p>.", html)
33
33
  else:
34
34
  items = list(cw.get_updates_html(["_user_cache"]))
35
35
  if len(items) == 0:
36
36
  return
37
- html = _("{user} modified {what}").format(**ctx)
38
- html = "<p>{}:</p>".format(html)
37
+ txt = format_html(_("{user} modified {what}"), **ctx)
38
+ html = format_html("<p>{}:</p>", txt)
39
39
  html += tostring(E.ul(*items))
40
40
  html += self.get_change_info(ar, cw)
41
- return "<div>{}</div>".format(html)
41
+ return format_html("<div>{}</div>", html)
42
42
 
43
43
  def get_change_info(self, ar, cw):
44
- return ""
44
+ return mark_safe("")
45
45
 
46
46
  if dd.is_installed("notify"):
47
47
 
@@ -0,0 +1,46 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2008-2024 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+ from lino.api import dd
8
+ from lino.utils import ONE_DAY
9
+
10
+
11
+ class PeriodType(dd.Choice):
12
+ ref_template = None
13
+
14
+ def __init__(self, value, text, duration, ref_template):
15
+ super().__init__(value, text, value)
16
+ self.ref_template = ref_template
17
+ self.duration = duration
18
+
19
+ class PeriodTypes(dd.ChoiceList):
20
+ item_class = PeriodType
21
+ verbose_name = _("Period type")
22
+ verbose_name_plural = _("Period types")
23
+ column_names = "value text duration ref_template"
24
+
25
+ @dd.displayfield(_("Duration"))
26
+ def duration(cls, p, ar):
27
+ return str(p.duration)
28
+
29
+ @dd.displayfield(_("Template for reference"))
30
+ def ref_template(cls, p, ar):
31
+ return p.ref_template
32
+
33
+ add = PeriodTypes.add_item
34
+ # value/names, text, duration, ref_template
35
+ add("month", _("Month"), 1, "{month:0>2}")
36
+ add("quarter", _("Quarter"), 3, "Q{period}")
37
+ add("trimester", _("Trimester"), 4, "T{period}")
38
+ add("semester", _("Semester"), 6, "S{period}")
39
+
40
+
41
+ class PeriodStates(dd.Workflow):
42
+ pass
43
+
44
+ add = PeriodStates.add_item
45
+ add('10', _("Open"), 'open')
46
+ add('20', _("Closed"), 'closed')
@@ -2,14 +2,40 @@
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 datetime
5
6
  from django.db import models
6
7
  from django.utils.translation import gettext_lazy as _
7
8
 
8
9
  from lino.api import dd, rt
9
10
  from lino import mixins
10
11
  from lino.mixins import Referrable
12
+ from lino.utils import ONE_DAY
11
13
 
12
14
  from lino.modlib.office.roles import OfficeStaff
15
+ from lino.modlib.system.choicelists import DurationUnits
16
+
17
+
18
+ def get_range_for_date(date):
19
+ """
20
+ Return the default start and end date of the period to create for the given
21
+ date.
22
+ """
23
+ pt = dd.plugins.periods.period_type
24
+ month = date.month
25
+ year = date.year
26
+ month -= dd.plugins.periods.start_month
27
+ if month < 0:
28
+ month += 12
29
+ year -= 1
30
+ period = int(month / pt.duration)
31
+ month = dd.plugins.periods.start_month + period * pt.duration
32
+ if month > 12:
33
+ month -= 12
34
+ year += 1
35
+ sd = datetime.date(year, month, 1)
36
+ # ed = sd.replace(month=sd.month + pt.duration + 1, 1) - ONE_DAY
37
+ ed = DurationUnits.months.add_duration(sd, pt.duration) - ONE_DAY
38
+ return (sd, ed)
13
39
 
14
40
 
15
41
  class PeriodRange(dd.Model):
@@ -8,52 +8,17 @@ from django.utils.translation import gettext_lazy as _
8
8
 
9
9
  from lino.api import dd
10
10
  from lino import mixins
11
- from lino.utils import last_day_of_month, ONE_DAY
11
+ from lino.utils import ONE_DAY
12
12
  from lino.mixins.periods import DateRange
13
13
  from lino.mixins import Referrable
14
14
 
15
15
  from lino.modlib.office.roles import OfficeStaff
16
+ from .mixins import get_range_for_date
17
+ from .choicelists import PeriodTypes, PeriodStates
16
18
 
17
19
  NEXT_YEAR_SEP = "/"
18
20
  YEAR_PERIOD_SEP = "-"
19
21
 
20
- class PeriodType(dd.Choice):
21
- ref_template = None
22
- ref_template = None
23
-
24
- def __init__(self, value, text, duration, ref_template):
25
- super().__init__(value, text, value)
26
- self.ref_template = ref_template
27
- self.duration = duration
28
-
29
- class PeriodTypes(dd.ChoiceList):
30
- item_class = PeriodType
31
- verbose_name = _("Period type")
32
- verbose_name_plural = _("Period types")
33
- column_names = "value text duration ref_template"
34
-
35
- @dd.displayfield(_("Duration"))
36
- def duration(cls, p, ar):
37
- return str(p.duration)
38
-
39
- @dd.displayfield(_("Template for reference"))
40
- def ref_template(cls, p, ar):
41
- return p.ref_template
42
-
43
- add = PeriodTypes.add_item
44
- # value/names, text, duration, ref_template
45
- add("month", _("Month"), 1, "{month:0>2}")
46
- add("quarter", _("Quarter"), 3, "Q{period}")
47
- add("trimester", _("Trimester"), 4, "T{period}")
48
- add("semester", _("Semester"), 6, "S{period}")
49
-
50
-
51
- class PeriodStates(dd.Workflow):
52
- pass
53
-
54
- add = PeriodStates.add_item
55
- add('10', _("Open"), 'open')
56
- add('20', _("Closed"), 'closed')
57
22
 
58
23
 
59
24
  class StoredYear(DateRange, Referrable):
@@ -120,7 +85,7 @@ class StoredPeriod(DateRange, Referrable):
120
85
  preferred_foreignkey_width = 10
121
86
 
122
87
  state = PeriodStates.field(default='open')
123
- year = dd.ForeignKey('periods.StoredYear', blank=True, null=True)
88
+ year = dd.ForeignKey('periods.StoredYear', blank=True, null=True, related_name="periods")
124
89
  remark = models.CharField(_("Remark"), max_length=250, blank=True)
125
90
 
126
91
  @classmethod
@@ -197,10 +162,8 @@ class StoredPeriod(DateRange, Referrable):
197
162
  ref = date2ref(date)
198
163
  obj = cls.get_by_ref(ref, None)
199
164
  if obj is None:
200
- obj = cls(
201
- ref=ref,
202
- start_date=date.replace(day=1),
203
- end_date=last_day_of_month(date))
165
+ sd, ed = get_range_for_date(date)
166
+ obj = cls(ref=ref, start_date=sd, end_date=ed)
204
167
  obj.full_clean()
205
168
  obj.save()
206
169
  return obj
@@ -208,7 +171,7 @@ class StoredPeriod(DateRange, Referrable):
208
171
  def full_clean(self, *args, **kwargs):
209
172
  if self.start_date is None:
210
173
  self.start_date = dd.today().replace(day=1)
211
- if not self.year:
174
+ if not self.year_id:
212
175
  self.year = StoredYear.get_or_create_from_date(self.start_date)
213
176
  super().full_clean(*args, **kwargs)
214
177
 
@@ -218,10 +181,19 @@ class StoredPeriod(DateRange, Referrable):
218
181
  # "{0} {1} (#{0})".format(self.pk, self.year)
219
182
  return self.ref
220
183
 
184
+ # def get_str_words(self, ar):
185
+ # # if ar.is_obvious_field("year"):
186
+ # if self.year.covers_date(dd.today()):
187
+ # # yield self.nickname
188
+ # yield f"{dd.plugins.periods.period_name} {self.nickname}"
189
+ # else:
190
+ # yield str(self)
191
+
221
192
  @property
222
193
  def nickname(self):
223
194
  if self.year.covers_date(dd.today()):
224
- if len(parts := self.ref.split(YEAR_PERIOD_SEP)) == 2:
195
+ if self.ref and len(parts := self.ref.split(YEAR_PERIOD_SEP)) == 2:
196
+ # return "{} {}".format(dd.plugins.periods.period_name, parts[1])
225
197
  return parts[1]
226
198
  return self.ref
227
199
 
@@ -51,12 +51,9 @@ class PublisherBuildMethod(JinjaBuildMethod):
51
51
  )
52
52
  context = elem.get_printable_context(ar)
53
53
  html = tpl.render(context)
54
- self.html2file(html, filename)
54
+ self.html2file(html, filename, context)
55
55
  return os.path.getmtime(filename)
56
56
 
57
- def html2file(self, html, filename):
58
- raise NotImplementedError()
59
-
60
57
 
61
58
  BuildMethods.add_item_instance(PublisherBuildMethod())
62
59
 
@@ -7,10 +7,10 @@ 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
+ # 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
+ #
14
14
  from django.db import models
15
15
  from django.conf import settings
16
16
  from django.core.files.storage import default_storage
@@ -198,36 +198,36 @@ class UploadBase(Commentable, GalleryViewable):
198
198
  return text
199
199
  return E.a(text, href=mf.get_download_url(), target="_blank")
200
200
 
201
- def memo2html(self, ar, text, **ctx):
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())
206
- ctx.update(href=ar.renderer.obj2url(ar, self))
207
- small_url = mf.get_image_url()
208
- if small_url is None or small_url == mf.url: # non-previewable mimetype
209
- if not text:
210
- text = str(self)
211
- ctx.update(text=text)
212
- tpl = '(<a href="{src}" target="_blank">{text}</a>'
213
- tpl += '| <a href="{href}">detail</a>)'
214
- return format_html(tpl, **ctx)
215
-
216
- fmt = parse_image_spec(text, **ctx)
217
- if isinstance(fmt, SMALL_FORMATS):
218
- fmt.context.update(src=small_url)
219
-
220
- if not fmt.context["caption"]:
221
- fmt.context["caption"] = self.description or str(self)
222
-
223
- rv = format_html(
224
- '<a href="{href}" target="_blank"><img src="{src}"'
225
- + ' style="{style}" title="{caption}"/></a>', **fmt.context)
226
- # if ar.renderer.front_end.media_name == 'react':
227
- # return ('<figure class="lino-memo-image"><img src="{src}" ' +
228
- # 'style="{style}" title="{caption}"/><figcaption' +
229
- # ' style="text-align: center;">{caption}</figcaption>' +
230
- # '</figure>').format(**kwargs)
231
-
232
- # print("20230325", rv)
233
- return rv
201
+ # def memo2html(self, ar, text, **ctx):
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())
206
+ # ctx.update(href=ar.renderer.obj2url(ar, self))
207
+ # small_url = mf.get_image_url()
208
+ # if small_url is None or small_url == mf.url: # non-previewable mimetype
209
+ # if not text:
210
+ # text = str(self)
211
+ # ctx.update(text=text)
212
+ # tpl = '(<a href="{src}" target="_blank">{text}</a>'
213
+ # tpl += '| <a href="{href}">detail</a>)'
214
+ # return format_html(tpl, **ctx)
215
+ #
216
+ # fmt = parse_image_spec(text, **ctx)
217
+ # if isinstance(fmt, SMALL_FORMATS):
218
+ # fmt.context.update(src=small_url)
219
+ #
220
+ # if not fmt.context["caption"]:
221
+ # fmt.context["caption"] = self.description or str(self)
222
+ #
223
+ # rv = format_html(
224
+ # '<a href="{href}" target="_blank"><img src="{src}"'
225
+ # + ' style="{style}" title="{caption}"/></a>', **fmt.context)
226
+ # # if ar.renderer.front_end.media_name == 'react':
227
+ # # return ('<figure class="lino-memo-image"><img src="{src}" ' +
228
+ # # 'style="{style}" title="{caption}"/><figcaption' +
229
+ # # ' style="text-align: center;">{caption}</figcaption>' +
230
+ # # '</figure>').format(**kwargs)
231
+ #
232
+ # # print("20230325", rv)
233
+ # return rv
@@ -14,6 +14,10 @@ from django.utils.text import format_lazy
14
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
@@ -112,7 +116,7 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
112
116
  verbose_name = _("Upload file")
113
117
  verbose_name_plural = _("Upload files")
114
118
 
115
- memo_command = "file"
119
+ memo_command = "upload"
116
120
 
117
121
  upload_area = UploadAreas.field(default="general")
118
122
  type = dd.ForeignKey("uploads.UploadType", blank=True, null=True)
@@ -129,7 +133,8 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
129
133
  elif self.file:
130
134
  s = filename_leaf(self.file.name)
131
135
  elif self.library_file:
132
- s = "{}:{}".format(self.volume.ref, self.library_file)
136
+ s = filename_leaf(self.library_file)
137
+ # s = "{}:{}".format(self.volume.ref, self.library_file)
133
138
  else:
134
139
  s = str(self.id)
135
140
  if self.type:
@@ -158,12 +163,13 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
158
163
 
159
164
  def get_memo_command(self, ar=None):
160
165
  if dd.is_installed("memo"):
161
- cmd = f"[{self.memo_command} {self.pk}"
162
- if self.description:
163
- cmd += " " + self.description + "]"
164
- else:
165
- cmd += "]"
166
- return cmd
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
167
173
  return None
168
174
 
169
175
  def get_real_file_size(self):
@@ -180,16 +186,6 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
180
186
  df.add("camera_stream")
181
187
  return df
182
188
 
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
-
193
189
  @dd.displayfield(_("Description"))
194
190
  def description_link(self, ar):
195
191
  s = str(self)
@@ -214,6 +210,13 @@ class Upload(UploadBase, UserAuthored, Controllable, Publishable):
214
210
  super().full_clean(*args, **kw)
215
211
  if self.type is not None:
216
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
217
220
 
218
221
  def get_gallery_item(self, ar):
219
222
  d = super().get_gallery_item(ar)
@@ -415,3 +418,50 @@ def before_analyze(sender, **kwargs):
415
418
  from .ui import *
416
419
 
417
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)
@@ -32,6 +32,12 @@ class UploadMediaFile:
32
32
  url += ".png"
33
33
  return previewer.base_dir + "/" + url
34
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
+
35
41
  def get_mimetype_description(self):
36
42
  if self.suffix == ".pdf":
37
43
  return _("PDF file")
@@ -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._label or self.__name__
164
- # return self._label or \
165
- # _("My %s") % self.model._meta.verbose_name_plural
166
- return self._label or format_lazy(
167
- _("My {}"), self.model._meta.verbose_name_plural
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-2020 Rumma & Ko Ltd
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
- pdf.write_pdf(filename)
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/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
- # if f.name == 'p_book':
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
- if isinstance(field, ChoiceListField):
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(field):
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)
@@ -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