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.
Files changed (72) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actions.py +2 -4
  3. lino/core/actors.py +35 -14
  4. lino/core/dbtables.py +12 -12
  5. lino/core/fields.py +27 -18
  6. lino/core/inject.py +9 -2
  7. lino/core/kernel.py +11 -12
  8. lino/core/model.py +25 -39
  9. lino/core/renderer.py +3 -4
  10. lino/core/requests.py +91 -92
  11. lino/core/site.py +5 -46
  12. lino/core/store.py +4 -4
  13. lino/core/tables.py +0 -16
  14. lino/core/utils.py +5 -2
  15. lino/help_texts.py +7 -5
  16. lino/locale/bn/LC_MESSAGES/django.po +1225 -909
  17. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  18. lino/locale/de/LC_MESSAGES/django.po +1748 -1392
  19. lino/locale/django.pot +1150 -909
  20. lino/locale/es/LC_MESSAGES/django.po +1721 -1347
  21. lino/locale/et/LC_MESSAGES/django.po +1267 -954
  22. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  23. lino/locale/fr/LC_MESSAGES/django.po +1207 -925
  24. lino/locale/nl/LC_MESSAGES/django.po +1261 -942
  25. lino/locale/pt_BR/LC_MESSAGES/django.po +1204 -906
  26. lino/locale/zh_Hant/LC_MESSAGES/django.po +1204 -906
  27. lino/mixins/dupable.py +8 -7
  28. lino/mixins/periods.py +10 -8
  29. lino/modlib/checkdata/__init__.py +1 -1
  30. lino/modlib/comments/choicelists.py +1 -1
  31. lino/modlib/comments/fixtures/demo2.py +4 -1
  32. lino/modlib/comments/mixins.py +9 -10
  33. lino/modlib/comments/models.py +4 -4
  34. lino/modlib/comments/ui.py +11 -2
  35. lino/modlib/dupable/models.py +10 -14
  36. lino/modlib/gfks/mixins.py +8 -1
  37. lino/modlib/jinja/choicelists.py +3 -3
  38. lino/modlib/jinja/renderer.py +2 -0
  39. lino/modlib/languages/fixtures/all_languages.py +4 -6
  40. lino/modlib/linod/mixins.py +3 -2
  41. lino/modlib/memo/mixins.py +17 -215
  42. lino/modlib/memo/models.py +41 -12
  43. lino/modlib/memo/parser.py +7 -3
  44. lino/modlib/notify/mixins.py +35 -34
  45. lino/modlib/periods/choicelists.py +46 -0
  46. lino/modlib/periods/mixins.py +26 -1
  47. lino/modlib/periods/models.py +17 -45
  48. lino/modlib/printing/choicelists.py +3 -3
  49. lino/modlib/publisher/choicelists.py +1 -4
  50. lino/modlib/search/models.py +17 -11
  51. lino/modlib/system/fixtures/__init__.py +0 -0
  52. lino/modlib/system/fixtures/std.py +5 -0
  53. lino/modlib/system/models.py +3 -2
  54. lino/modlib/uploads/__init__.py +10 -1
  55. lino/modlib/uploads/choicelists.py +3 -3
  56. lino/modlib/uploads/mixins.py +49 -51
  57. lino/modlib/uploads/models.py +144 -61
  58. lino/modlib/uploads/ui.py +12 -6
  59. lino/modlib/uploads/utils.py +113 -0
  60. lino/modlib/users/mixins.py +9 -6
  61. lino/modlib/weasyprint/choicelists.py +17 -7
  62. lino/utils/__init__.py +6 -0
  63. lino/utils/choosers.py +21 -8
  64. lino/utils/html.py +1 -1
  65. lino/utils/instantiator.py +9 -0
  66. lino/utils/media.py +2 -3
  67. lino/utils/soup.py +311 -0
  68. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/METADATA +2 -2
  69. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/RECORD +72 -67
  70. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/WHEEL +1 -1
  71. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/AUTHORS.rst +0 -0
  72. {lino-24.11.0.dist-info → lino-25.1.0.dist-info}/licenses/COPYING +0 -0
@@ -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
 
@@ -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):
@@ -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
 
@@ -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,44 +193,41 @@ 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
-
200
- 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)
209
- ctx.update(href=ar.renderer.obj2url(ar, self))
210
- if url.endswith(".pdf"):
211
- if not text:
212
- text = str(self)
213
- 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)
217
-
218
- fmt = parse_image_spec(text, **ctx)
219
- if not fmt.context["caption"]:
220
- fmt.context["caption"] = self.description or str(self)
221
- # if text:
222
- # ctx.update(caption=text)
223
-
224
- rv = (
225
- '<a href="{href}" target="_blank"/><img src="{src}"'
226
- + ' style="{style}" title="{caption}"/></a>'
227
- ).format(**fmt.context)
228
- # if ar.renderer.front_end.media_name == 'react':
229
- # return ('<figure class="lino-memo-image"><img src="{src}" ' +
230
- # 'style="{style}" title="{caption}"/><figcaption' +
231
- # ' style="text-align: center;">{caption}</figcaption>' +
232
- # '</figure>').format(**kwargs)
233
-
234
- # print("20230325", rv)
235
- return rv
199
+ return E.a(text, href=mf.get_download_url(), target="_blank")
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