lino 24.11.1__py3-none-any.whl → 25.1.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 (60) hide show
  1. lino/__init__.py +1 -1
  2. lino/core/actors.py +33 -12
  3. lino/core/dbtables.py +0 -4
  4. lino/core/fields.py +26 -18
  5. lino/core/kernel.py +4 -6
  6. lino/core/model.py +5 -16
  7. lino/core/renderer.py +1 -7
  8. lino/core/requests.py +70 -47
  9. lino/core/site.py +1 -1
  10. lino/core/tables.py +0 -16
  11. lino/help_texts.py +4 -4
  12. lino/locale/bn/LC_MESSAGES/django.po +58 -45
  13. lino/locale/de/LC_MESSAGES/django.mo +0 -0
  14. lino/locale/de/LC_MESSAGES/django.po +79 -108
  15. lino/locale/django.pot +55 -44
  16. lino/locale/es/LC_MESSAGES/django.po +56 -44
  17. lino/locale/et/LC_MESSAGES/django.po +58 -45
  18. lino/locale/fr/LC_MESSAGES/django.mo +0 -0
  19. lino/locale/fr/LC_MESSAGES/django.po +60 -48
  20. lino/locale/nl/LC_MESSAGES/django.po +59 -45
  21. lino/locale/pt_BR/LC_MESSAGES/django.po +55 -44
  22. lino/locale/zh_Hant/LC_MESSAGES/django.po +55 -44
  23. lino/mixins/dupable.py +8 -7
  24. lino/mixins/periods.py +4 -4
  25. lino/modlib/checkdata/__init__.py +1 -1
  26. lino/modlib/comments/mixins.py +45 -44
  27. lino/modlib/comments/models.py +2 -2
  28. lino/modlib/comments/ui.py +9 -3
  29. lino/modlib/dupable/models.py +10 -14
  30. lino/modlib/gfks/mixins.py +8 -1
  31. lino/modlib/jinja/choicelists.py +3 -3
  32. lino/modlib/jinja/renderer.py +2 -0
  33. lino/modlib/languages/fixtures/all_languages.py +4 -6
  34. lino/modlib/memo/mixins.py +7 -7
  35. lino/modlib/memo/models.py +41 -12
  36. lino/modlib/memo/parser.py +7 -3
  37. lino/modlib/notify/mixins.py +8 -8
  38. lino/modlib/periods/choicelists.py +46 -0
  39. lino/modlib/periods/mixins.py +26 -0
  40. lino/modlib/periods/models.py +17 -45
  41. lino/modlib/publisher/choicelists.py +1 -4
  42. lino/modlib/system/__init__.py +0 -3
  43. lino/modlib/system/choicelists.py +0 -13
  44. lino/modlib/system/mixins.py +1 -0
  45. lino/modlib/system/models.py +0 -18
  46. lino/modlib/uploads/mixins.py +37 -37
  47. lino/modlib/uploads/models.py +68 -18
  48. lino/modlib/uploads/utils.py +6 -0
  49. lino/modlib/users/mixins.py +9 -6
  50. lino/modlib/users/models.py +0 -2
  51. lino/modlib/users/ui.py +1 -1
  52. lino/modlib/weasyprint/choicelists.py +17 -7
  53. lino/utils/choosers.py +21 -8
  54. lino/utils/instantiator.py +9 -0
  55. lino/utils/soup.py +5 -5
  56. {lino-24.11.1.dist-info → lino-25.1.1.dist-info}/METADATA +4 -2
  57. {lino-24.11.1.dist-info → lino-25.1.1.dist-info}/RECORD +60 -59
  58. {lino-24.11.1.dist-info → lino-25.1.1.dist-info}/WHEEL +1 -1
  59. {lino-24.11.1.dist-info → lino-25.1.1.dist-info}/licenses/AUTHORS.rst +0 -0
  60. {lino-24.11.1.dist-info → lino-25.1.1.dist-info}/licenses/COPYING +0 -0
@@ -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):
@@ -27,7 +27,6 @@ from .actions import SendWelcomeMail, CreateAccount, ResetPassword, VerifyUser,
27
27
 
28
28
  # from .actions import SignIn
29
29
  from lino.modlib.about.choicelists import TimeZones, DateFormats
30
- from lino.modlib.system.choicelists import DashboardLayouts
31
30
  from lino.modlib.publisher.mixins import Publishable
32
31
 
33
32
  from lino.core.roles import Supervisor
@@ -120,7 +119,6 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
120
119
  time_zone = dd.DummyField()
121
120
 
122
121
  date_format = DateFormats.field(default="default")
123
- dashboard_layout = DashboardLayouts.field(blank=True, null=True)
124
122
 
125
123
  ledger = dd.ForeignKey("ledgers.Ledger", null=True, blank=True)
126
124
 
lino/modlib/users/ui.py CHANGED
@@ -39,7 +39,7 @@ class UserDetail(dd.DetailLayout):
39
39
  username user_type:20
40
40
  first_name last_name
41
41
  email status
42
- time_zone #dashboard_layout
42
+ time_zone
43
43
  """
44
44
  box2 = """
45
45
  id language
@@ -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
lino/utils/soup.py CHANGED
@@ -251,12 +251,12 @@ ALLOWED_ATTRIBUTES["span"] = {
251
251
  "data-value",
252
252
  "contenteditable",
253
253
  }
254
- ALLOWED_ATTRIBUTES["p"] = {"align"}
254
+ ALLOWED_ATTRIBUTES["p"] = {"align", "style"}
255
255
 
256
- def safe_css(attr, css):
257
- if attr == "style":
258
- return re.sub("(width|height):[^;]+;", "", css)
259
- return css
256
+ # def safe_css(attr, css):
257
+ # if attr == "style":
258
+ # return re.sub("(width|height):[^;]+;", "", css)
259
+ # return css
260
260
 
261
261
  def sanitize(old):
262
262
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: lino
3
- Version: 24.11.1
3
+ Version: 25.1.1
4
4
  Summary: A framework for writing desktop-like web applications using Django and ExtJS or React
5
5
  Project-URL: Homepage, https://www.lino-framework.org
6
6
  Project-URL: Repository, https://gitlab.com/lino-framework/lino
@@ -666,6 +666,8 @@ License: GNU AFFERO GENERAL PUBLIC LICENSE
666
666
  if any, to sign a "copyright disclaimer" for the program, if necessary.
667
667
  For more information on this, and how to apply and follow the GNU AGPL, see
668
668
  <https://www.gnu.org/licenses/>.
669
+ License-File: AUTHORS.rst
670
+ License-File: COPYING
669
671
  Keywords: Django,React,customized,framework
670
672
  Classifier: Development Status :: 5 - Production/Stable
671
673
  Classifier: Environment :: Web Environment