lino 25.8.2__py3-none-any.whl → 25.9.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 (143) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +0 -1
  3. lino/config/unused/403.html +1 -1
  4. lino/config/unused/404.html +1 -1
  5. lino/config/unused/500.html +1 -1
  6. lino/core/__init__.py +0 -1
  7. lino/core/actions.py +2 -2
  8. lino/core/actors.py +10 -2
  9. lino/core/elems.py +1 -1
  10. lino/core/fields.py +4 -1
  11. lino/core/kernel.py +5 -1
  12. lino/core/model.py +2 -11
  13. lino/core/renderer.py +2 -2
  14. lino/core/requests.py +12 -12
  15. lino/core/site.py +5 -82
  16. lino/core/store.py +3 -1
  17. lino/core/urls.py +1 -1
  18. lino/core/user_types.py +1 -10
  19. lino/help_texts.py +6 -6
  20. lino/management/commands/initdb.py +0 -3
  21. lino/modlib/__init__.py +0 -1
  22. lino/modlib/bootstrap5/README.txt +2 -0
  23. lino/modlib/bootstrap5/__init__.py +69 -0
  24. lino/modlib/{bootstrap3/config/bootstrap3 → bootstrap5/config/bootstrap5}/base.html +9 -4
  25. lino/modlib/{bootstrap3/config/bootstrap3 → bootstrap5/config/bootstrap5}/detail.html +1 -1
  26. lino/modlib/{bootstrap3/config/bootstrap3 → bootstrap5/config/bootstrap5}/index.html +1 -1
  27. lino/modlib/{bootstrap3/config/bootstrap3 → bootstrap5/config/bootstrap5}/table.html +1 -1
  28. lino/modlib/bootstrap5/models.py +30 -0
  29. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.css +4085 -0
  30. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.css.map +1 -0
  31. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.min.css +6 -0
  32. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.min.css.map +1 -0
  33. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.rtl.css +4084 -0
  34. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.rtl.css.map +1 -0
  35. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.rtl.min.css +6 -0
  36. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-grid.rtl.min.css.map +1 -0
  37. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.css +597 -0
  38. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.css.map +1 -0
  39. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.min.css +6 -0
  40. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.min.css.map +1 -0
  41. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.rtl.css +594 -0
  42. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.rtl.css.map +1 -0
  43. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.rtl.min.css +6 -0
  44. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-reboot.rtl.min.css.map +1 -0
  45. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.css +5406 -0
  46. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.css.map +1 -0
  47. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.min.css +6 -0
  48. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.min.css.map +1 -0
  49. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.rtl.css +5397 -0
  50. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.rtl.css.map +1 -0
  51. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.rtl.min.css +6 -0
  52. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap-utilities.rtl.min.css.map +1 -0
  53. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.css +12043 -0
  54. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.css.map +1 -0
  55. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.min.css +6 -0
  56. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.min.css.map +1 -0
  57. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.rtl.css +12016 -0
  58. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.rtl.css.map +1 -0
  59. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.rtl.min.css +6 -0
  60. lino/modlib/bootstrap5/static/bootstrap-5.3.7/css/bootstrap.rtl.min.css.map +1 -0
  61. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.bundle.js +6315 -0
  62. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.bundle.js.map +1 -0
  63. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.bundle.min.js +7 -0
  64. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.bundle.min.js.map +1 -0
  65. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.esm.js +4450 -0
  66. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.esm.js.map +1 -0
  67. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.esm.min.js +7 -0
  68. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.esm.min.js.map +1 -0
  69. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.js +4497 -0
  70. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.js.map +1 -0
  71. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.min.js +7 -0
  72. lino/modlib/bootstrap5/static/bootstrap-5.3.7/js/bootstrap.min.js.map +1 -0
  73. lino/modlib/{bootstrap3 → bootstrap5}/views.py +12 -117
  74. lino/modlib/checkdata/choicelists.py +1 -1
  75. lino/modlib/comments/fixtures/demo2.py +1 -0
  76. lino/modlib/comments/mixins.py +1 -8
  77. lino/modlib/comments/models.py +2 -0
  78. lino/modlib/comments/ui.py +7 -7
  79. lino/modlib/extjs/__init__.py +2 -4
  80. lino/modlib/extjs/config/extjs/index.html +1 -1
  81. lino/modlib/extjs/ext_renderer.py +1 -7
  82. lino/modlib/extjs/views.py +2 -0
  83. lino/modlib/help/models.py +1 -12
  84. lino/modlib/jinja/renderer.py +1 -1
  85. lino/modlib/linod/mixins.py +3 -2
  86. lino/modlib/memo/__init__.py +11 -11
  87. lino/modlib/memo/mixins.py +38 -21
  88. lino/modlib/memo/models.py +10 -7
  89. lino/modlib/memo/parser.py +3 -1
  90. lino/modlib/notify/models.py +6 -9
  91. lino/modlib/odata/views.py +7 -7
  92. lino/modlib/publisher/__init__.py +15 -3
  93. lino/modlib/publisher/choicelists.py +8 -94
  94. lino/modlib/publisher/config/publisher/page.pub.html +82 -19
  95. lino/modlib/publisher/fixtures/std.py +14 -1
  96. lino/modlib/publisher/fixtures/synodalworld.py +3 -1
  97. lino/modlib/publisher/mixins.py +59 -77
  98. lino/modlib/publisher/models.py +109 -204
  99. lino/modlib/publisher/renderer.py +31 -11
  100. lino/modlib/publisher/ui.py +46 -98
  101. lino/modlib/publisher/views.py +61 -11
  102. lino/modlib/system/models.py +3 -2
  103. lino/modlib/uploads/__init__.py +1 -0
  104. lino/modlib/uploads/mixins.py +2 -2
  105. lino/modlib/uploads/models.py +55 -21
  106. lino/modlib/uploads/ui.py +1 -0
  107. lino/modlib/uploads/utils.py +2 -2
  108. lino/modlib/users/__init__.py +2 -3
  109. lino/modlib/users/actions.py +12 -17
  110. lino/modlib/users/fixtures/abc.py +20 -0
  111. lino/modlib/users/mixins.py +6 -6
  112. lino/modlib/users/models.py +37 -36
  113. lino/modlib/weasyprint/__init__.py +25 -14
  114. lino/modlib/weasyprint/choicelists.py +6 -0
  115. lino/modlib/weasyprint/config/weasyprint/base.weasy.html +43 -27
  116. lino/utils/diag.py +5 -3
  117. lino/utils/html.py +103 -0
  118. lino/utils/mldbc/mixins.py +2 -2
  119. lino/utils/soup.py +16 -8
  120. {lino-25.8.2.dist-info → lino-25.9.0.dist-info}/METADATA +1 -1
  121. {lino-25.8.2.dist-info → lino-25.9.0.dist-info}/RECORD +135 -95
  122. lino/modlib/bootstrap3/README.txt +0 -2
  123. lino/modlib/bootstrap3/__init__.py +0 -73
  124. lino/modlib/bootstrap3/models.py +0 -30
  125. lino/modlib/bootstrap3/static/bootstrap-3.3.4/css/bootstrap.css +0 -6584
  126. lino/modlib/bootstrap3/static/bootstrap-3.3.4/css/bootstrap.css.map +0 -1
  127. lino/modlib/bootstrap3/static/bootstrap-3.3.4/css/bootstrap.min.css +0 -5
  128. lino/modlib/bootstrap3/static/bootstrap-3.3.4/js/bootstrap.js +0 -2317
  129. lino/modlib/bootstrap3/static/bootstrap-3.3.4/js/bootstrap.min.js +0 -7
  130. /lino/modlib/{bootstrap3 → bootstrap5}/renderer.py +0 -0
  131. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/css/bootstrap-theme.css +0 -0
  132. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/css/bootstrap-theme.css.map +0 -0
  133. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/css/bootstrap-theme.min.css +0 -0
  134. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/fonts/glyphicons-halflings-regular.eot +0 -0
  135. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/fonts/glyphicons-halflings-regular.svg +0 -0
  136. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/fonts/glyphicons-halflings-regular.ttf +0 -0
  137. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/fonts/glyphicons-halflings-regular.woff +0 -0
  138. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/fonts/glyphicons-halflings-regular.woff2 +0 -0
  139. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/js/bootstrap_lino.js +0 -0
  140. /lino/modlib/{bootstrap3/static/bootstrap-3.3.4 → bootstrap5/static/bootstrap-5.3.7}/js/npm.js +0 -0
  141. {lino-25.8.2.dist-info → lino-25.9.0.dist-info}/WHEEL +0 -0
  142. {lino-25.8.2.dist-info → lino-25.9.0.dist-info}/licenses/AUTHORS.rst +0 -0
  143. {lino-25.8.2.dist-info → lino-25.9.0.dist-info}/licenses/COPYING +0 -0
@@ -1,105 +1,38 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2012-2024 Rumma & Ko Ltd
2
+ # Copyright 2012-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
- from html import escape
6
5
  from django.db import models
7
- from django.http import HttpResponseRedirect
8
- from django.conf import settings
9
- from django.utils import translation
10
- from django.utils.translation import pgettext_lazy
11
6
 
12
7
  # from django.utils.translation import get_language
13
- from django.utils.html import mark_safe
14
- from django.utils.html import format_html
8
+ # from django.utils.html import format_html
15
9
 
16
10
  from lino.api import dd, rt, _
17
11
  from lino.utils.html import E
12
+ from lino.utils.soup import MORE_MARKER
18
13
  from lino.core import constants
19
14
  # from lino.core.renderer import add_user_language
20
-
21
- from lino.utils.mldbc.fields import LanguageField
22
- from lino import mixins
23
- from lino.mixins import Hierarchical, Sequenced, Referrable
24
15
  from lino.modlib.office.roles import OfficeUser
25
- from lino.modlib.publisher.mixins import Publishable
26
-
27
- # from lino.modlib.publisher.choicelists import PublisherViews
28
- from lino.modlib.memo.mixins import Previewable
29
- # from .utils import render_node
30
-
31
- # class NodeDetail(dd.DetailLayout):
32
- # main = "first_panel general more"
33
- #
34
- # first_panel = dd.Panel("""
35
- # treeview_panel:20 preview:60
36
- # """, label=_("Preview"))
37
- #
38
- # general = dd.Panel("""
39
- # content_panel:60 right_panel:20
40
- # """, label=_("General"), required_roles=dd.login_required(OfficeUser))
41
- #
42
- # more = dd.Panel("""
43
- # # topics.TagsByOwner:20 add_interest
44
- # comments.CommentsByRFC:20
45
- # """, label=_("More"), required_roles=dd.login_required(OfficeUser))
46
- #
47
- # content_panel = """
48
- # title id
49
- # body
50
- # publisher.PagesByParent
51
- # """
52
- #
53
- # right_panel = """
54
- # parent seqno
55
- # child_node_depth
56
- # page_type
57
- # filler
58
- # """
59
- #
60
- #
61
- # class Nodes(dd.Table):
62
- # model = 'pages.Node'
63
- # column_names = "title page_type id *"
64
- # order_by = ["id"]
65
- # detail_layout = 'pages.NodeDetail'
66
- # insert_layout = """
67
- # title
68
- # page_type filler
69
- # """
70
- # display_mode = ((None, constants.DISPLAY_MODE_STORY),)
71
- #
72
- #
73
-
74
- # class Translations(dd.Table):
75
- # model = 'pages.Translation'
76
- #
77
- # class TranslationsByParent(Translations):
78
- # master_key = 'parent'
79
- # label = _("Translated to...")
80
- #
81
- # class TranslationsByChild(Translations):
82
- # master_key = 'child'
83
- # label = _("Translated from...")
84
-
85
-
86
- if dd.is_installed("comments") and dd.is_installed("topics"):
87
- DISCUSSION_PANEL = """
88
- topics.TagsByOwner:20 comments.CommentsByRFC:60
89
- """
90
- else:
91
- DISCUSSION_PANEL = ""
16
+
17
+ from .choicelists import SpecialPages
18
+
19
+
20
+ VARTABS = ""
21
+
22
+ if dd.is_installed("topics"):
23
+ VARTABS += " topics.TagsByOwner:20"
24
+ if dd.is_installed("comments"):
25
+ VARTABS += " comments.CommentsByRFC:60"
92
26
 
93
27
 
94
28
  class PageDetail(dd.DetailLayout):
95
- main = "general first_panel more"
29
+ # main = "general first_panel more"
30
+ main = f"general first_panel {VARTABS} more"
96
31
 
97
32
  first_panel = dd.Panel(
98
33
  """
99
- treeview_panel:20 preview:60
100
- """,
101
- label=_("Preview"),
102
- )
34
+ treeview_panel:20 preview:60
35
+ """, label=_("Preview"))
103
36
 
104
37
  general = dd.Panel(
105
38
  """
@@ -109,16 +42,15 @@ class PageDetail(dd.DetailLayout):
109
42
  required_roles=dd.login_required(OfficeUser),
110
43
  )
111
44
 
112
- more = dd.Panel(
113
- DISCUSSION_PANEL,
114
- label=_("Discussion"),
115
- required_roles=dd.login_required(OfficeUser),
116
- )
45
+ # more = dd.Panel(
46
+ # VARTABS,
47
+ # label=_("Discussion"),
48
+ # required_roles=dd.login_required(OfficeUser),
49
+ # )
117
50
 
118
51
  content_panel = """
119
52
  title id
120
53
  body
121
- publisher.PagesByParent
122
54
  """
123
55
 
124
56
  # right_panel = """
@@ -129,23 +61,27 @@ class PageDetail(dd.DetailLayout):
129
61
  # """
130
62
 
131
63
  right_panel = """
132
- ref language
133
64
  parent seqno
65
+ publisher.PagesByParent
66
+ """
67
+
68
+ more = dd.Panel("""
69
+ publisher_tree language
134
70
  child_node_depth main_image
135
- #page_type filler
136
- publishing_state special_page
71
+ special_page #filler
72
+ publishing_state
137
73
  publisher.TranslationsByPage
138
- """
74
+ """, label=_("More"))
139
75
 
140
76
 
141
77
  class Pages(dd.Table):
142
78
  model = "publisher.Page"
143
- column_names = "ref title #page_type id *"
79
+ column_names = "title publisher_tree id *"
144
80
  detail_layout = "publisher.PageDetail"
145
81
  insert_layout = """
146
82
  title
147
- ref
148
- #page_type filler
83
+ publisher_tree
84
+ language #filler
149
85
  """
150
86
  default_display_modes = {None: constants.DISPLAY_MODE_LIST}
151
87
 
@@ -168,10 +104,22 @@ class PagesByParent(Pages):
168
104
  class TranslationsByPage(Pages):
169
105
  master_key = "translated_from"
170
106
  label = _("Translations")
171
- column_names = "ref title language id *"
107
+ column_names = "title language id *"
172
108
  default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
173
109
 
174
110
  @classmethod
175
111
  def row_as_summary(cls, ar, obj, text=None, **kwargs):
176
112
  # return format_html("({}) {}", obj.language, obj.as_summary_row(ar, **kwargs))
177
113
  return E.span("({}) ".format(obj.language), obj.as_summary_item(ar, text, **kwargs))
114
+
115
+
116
+ class Trees(dd.Table):
117
+ model = "publisher.Tree"
118
+ column_names = "ref root_page group private id *"
119
+
120
+
121
+ SpecialPages.add_item(
122
+ "pages", # filler=filler,
123
+ body=_("List of root pages.") + MORE_MARKER + " [show publisher.Trees]",
124
+ title=_("Root pages"),
125
+ parent='home')
@@ -1,12 +1,15 @@
1
1
  # -*- coding: UTF-8 -*-
2
2
  # Copyright 2020-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
-
5
4
  from django import http
6
5
  from django.conf import settings
7
6
  from django.core.exceptions import ObjectDoesNotExist
7
+ from django.shortcuts import redirect
8
8
  from django.utils import translation
9
9
  from django.views.generic import View
10
+ from lino.core import auth
11
+ from lino.core.requests import BaseRequest
12
+ from lino.core.views import json_response
10
13
 
11
14
 
12
15
  class Element(View):
@@ -23,36 +26,83 @@ class Element(View):
23
26
 
24
27
  # kw = dict(actor=self.publisher_model.get_default_table(),
25
28
  # request=request, renderer=rnd, permalink_uris=True)
26
- kw = dict(renderer=rnd)
29
+ kw = dict(renderer=rnd, request=request)
27
30
  # kw = dict(renderer=rnd, permalink_uris=True)
28
31
  # if rnd.front_end.media_name == 'react':
29
32
  # kw.update(hash_router=True)
30
33
 
31
34
  kw.update(selected_pks=[pk])
32
-
35
+ #
33
36
  try:
34
- ar = self.table_class.create_request(request=request, **kw)
37
+ ar = self.table_class.create_request(**kw)
35
38
  except ObjectDoesNotExist as e:
36
39
  # print("20240911", e)
37
- return http.HttpResponseNotFound(f"No row #{pk} in {self.table_class} ({e})")
40
+ return http.HttpResponseNotFound(
41
+ f"No row #{pk} in {self.table_class} ({e})")
38
42
  if len(ar.selected_rows) == 0:
39
43
  # print(f"20241003 Oops {ar} has no rows")
40
- return http.HttpResponseNotFound(f"20241003 No row #{pk} in {self.table_class}")
44
+ return http.HttpResponseNotFound(
45
+ f"20241003 No row #{pk} in {self.table_class}")
41
46
  obj = ar.selected_rows[0]
47
+
48
+ # m = self.table_class.model
49
+ # try:
50
+ # obj = m.objects.get(pk=pk)
51
+ # except m.DoesNotExist as e:
52
+ # return http.HttpResponseNotFound(f"No row #{pk} in {m} ({e})")
53
+ # ar = BaseRequest(renderer=rnd, request=request, selected_rows=[obj])
54
+ # ar = BaseRequest(renderer=rnd, request=request)
42
55
  return obj.get_publisher_response(ar)
43
56
 
44
57
 
45
58
  class Index(View):
46
- def get(self, request, pk=1):
47
- rnd = settings.SITE.plugins.publisher.renderer
59
+
60
+ ref = 'index'
61
+
62
+ def get(self, request):
63
+ Tree = settings.SITE.models.publisher.Tree
48
64
  dv = settings.SITE.models.publisher.Pages
49
65
  if len(settings.SITE.languages) == 1:
50
66
  # language = settings.SITE.languages[0].django_code
51
67
  language = translation.get_language()
52
68
  else:
53
69
  language = request.LANGUAGE_CODE
54
- index_node = dv.model.objects.get(ref="index", language=language)
70
+ try:
71
+ tree = Tree.objects.get(ref=self.ref)
72
+ except Tree.DoesNotExist:
73
+ return http.HttpResponseNotFound(f"No tree for {self.ref}")
74
+ obj = tree.get_root_page(language)
75
+ # print(20250829, obj)
76
+ if obj is None:
77
+ return http.HttpResponseNotFound(
78
+ f"No root page for {self.ref} in {language}")
79
+ # try:
80
+ # obj = dv.model.objects.get(
81
+ # parent=None, publisher_tree=tree)
82
+ # except dv.model.DoesNotExist:
83
+ # return http.HttpResponseNotFound(f"No row {ref} in {dv.model}")
84
+
55
85
  # print("20231025", index_node)
86
+ rnd = settings.SITE.plugins.publisher.renderer
56
87
  ar = dv.create_request(request=request, renderer=rnd,
57
- selected_rows=[index_node])
58
- return index_node.get_publisher_response(ar)
88
+ selected_rows=[obj])
89
+ return obj.get_publisher_response(ar)
90
+
91
+
92
+ class Login(View):
93
+ def post(self, request):
94
+ username = request.POST.get("username")
95
+ password = request.POST.get("password")
96
+
97
+ user = auth.authenticate(request, username=username, password=password)
98
+ if user is None:
99
+ return json_response({"success": False})
100
+ else:
101
+ auth.login(request, user)
102
+ return json_response({"success": True})
103
+
104
+
105
+ class Logout(View):
106
+ def get(self, request):
107
+ auth.logout(request)
108
+ return redirect(request.META.get('HTTP_REFERER', '/'))
@@ -321,16 +321,17 @@ class BleachChecker(Checker):
321
321
  model = dd.Model
322
322
 
323
323
  def get_checkable_models(self):
324
- for m in super(BleachChecker, self).get_checkable_models():
324
+ for m in super().get_checkable_models():
325
325
  if len(m._bleached_fields):
326
326
  yield m
327
327
 
328
328
  def get_checkdata_problems(self, ar, obj, fix=False):
329
329
  t = tuple(obj.fields_to_bleach(save=False))
330
330
  if len(t):
331
- fldnames = ", ".join([f.name for f, old, new in t])
331
+ fldnames = tuple([f.name for f, old, new in t])
332
332
  yield (True, _("Fields {} have unbleached content.").format(fldnames))
333
333
  if fix:
334
+ # obj.before_ui_save(ar, None)
334
335
  obj.before_ui_save(None, None)
335
336
  obj.full_clean()
336
337
  obj.save()
@@ -15,6 +15,7 @@ class Plugin(ad.Plugin):
15
15
 
16
16
  verbose_name = _("Uploads")
17
17
  menu_group = "office"
18
+ # needs_plugins = ['lino.modlib.checkdata']
18
19
 
19
20
  remove_orphaned_files = False
20
21
  """
@@ -134,7 +134,7 @@ class UploadController(dd.Model):
134
134
 
135
135
 
136
136
  class GalleryViewable(dd.Model):
137
- class Meta(object):
137
+ class Meta:
138
138
  abstract = True
139
139
 
140
140
  def get_gallery_item(self, ar):
@@ -155,7 +155,7 @@ class UploadBase(Commentable, GalleryViewable):
155
155
 
156
156
  def handle_uploaded_files(self, request, file=None):
157
157
  # ~ from django.core.files.base import ContentFile
158
- if not file and not "file" in request.FILES:
158
+ if not file and "file" not in request.FILES:
159
159
  dd.logger.debug("No 'file' has been submitted.")
160
160
  return
161
161
  uf = file or request.FILES["file"] # an UploadedFile instance
@@ -24,7 +24,7 @@ from lino.modlib.gfks.mixins import Controllable
24
24
  from lino.modlib.users.mixins import UserAuthored
25
25
 
26
26
  from lino.mixins import Referrable
27
- from lino.utils.soup import register_sanitizer
27
+ from lino.utils.soup import register_sanitizer, DATA_UPLOAD_ID
28
28
  from lino.utils.mldbc.mixins import BabelNamed
29
29
  from lino.modlib.checkdata.choicelists import Checker
30
30
  from lino.modlib.publisher.mixins import Publishable
@@ -309,6 +309,9 @@ UploadChecker.activate()
309
309
  class UploadsFolderChecker(Checker):
310
310
  verbose_name = _("Find orphaned files in uploads folder")
311
311
 
312
+ # It is no problem to have multiple upload entries pointing to a same file
313
+ # on the file system
314
+
312
315
  def get_checkdata_problems(self, ar, obj, fix=False):
313
316
  assert obj is None # this is an unbound checker
314
317
  Upload = rt.models.uploads.Upload
@@ -321,8 +324,7 @@ class UploadsFolderChecker(Checker):
321
324
  continue
322
325
  rel_filename = str(filename)[start:]
323
326
  qs = Upload.objects.filter(file=rel_filename)
324
- n = qs.count()
325
- if n == 0:
327
+ if not qs.exists():
326
328
  msg = format_lazy(
327
329
  _("File {} has no upload entry."), rel_filename)
328
330
  # print(msg)
@@ -339,9 +341,6 @@ class UploadsFolderChecker(Checker):
339
341
  # else:
340
342
  # print("{} has {} entries.".format(filename, n))
341
343
  # elif n > 1:
342
- # msg = _("Multiple upload entries for {} ").format(filename)
343
- # yield (False, msg)
344
- # This is no problem. A same file should be linkable to diffeerent controlers.
345
344
 
346
345
 
347
346
  UploadsFolderChecker.activate()
@@ -454,46 +453,81 @@ def setup_memo_commands(sender=None, **kwargs):
454
453
  ctx.update(href=ar.obj2url(self))
455
454
  if not mf.is_image():
456
455
  if not text:
457
- text = str(self)
456
+ text = self.description or str(self)
458
457
  ctx.update(text=text)
459
458
  tpl = '(<a href="{src}" target="_blank">{text}</a>'
460
459
  tpl += '| <a href="{href}">Detail</a>)'
461
460
  return format_html(tpl, **ctx)
462
461
 
463
- fmt = parse_image_spec(text, **ctx)
462
+ # fmt = parse_image_spec(text, **ctx)
464
463
  # TODO: When an image is inserted with format "wide", we should not use
465
464
  # the thumbnail but the original file. But for a PDF file we must always
466
465
  # use the img_src because the download_url is not an image.
467
- fmt.context.update(src=mf.get_image_url())
466
+ ctx.update(src=mf.get_image_url())
467
+ # fmt.context.update(src=mf.get_image_url())
468
468
  # if isinstance(fmt, SMALL_FORMATS):
469
469
  # fmt.context.update(src=img_src)
470
470
  # else:
471
471
  # print(f"20241116 {fmt} {fmt.context}")
472
472
 
473
- if not fmt.context["caption"]:
474
- fmt.context["caption"] = self.description or str(self)
473
+ # if not fmt.context["caption"]:
474
+ # fmt.context["caption"] = self.description or str(self)
475
+
476
+ if not text:
477
+ text = ""
478
+
479
+ if 'title="' not in text:
480
+ text += f' title="{self.description or str(self)}"'
481
+
482
+ if 'style="' not in text:
483
+ # text += ' style="max-height: 20ex; width: auto;"'
484
+ text += ' style="max-width:100%;height:auto;"'
485
+
486
+ ctx.update(properties=mark_safe(text))
475
487
 
488
+ # rv = format_html(
489
+ # '<a href="{href}" target="_blank"><img src="{src}"'
490
+ # + ' style="{style}" title="{caption}"/></a>', **fmt.context)
476
491
  rv = format_html(
477
492
  '<a href="{href}" target="_blank"><img src="{src}"'
478
- + ' style="{style}" title="{caption}"/></a>', **fmt.context)
493
+ + ' {properties}/></a>', **ctx)
479
494
  return rv
480
495
 
481
496
  mp = sender.plugins.memo.parser
482
497
  mp.register_django_model('file', rt.models.uploads.Upload, rnd=file2html)
483
498
 
484
499
 
485
- def on_sanitize(soup, save=False, ar=None):
500
+ def on_sanitize(soup, save=False, ar=None, mentions=None):
486
501
  # raise Exception(f"20250301")
487
- for tag in soup.find_all():
488
- tag_name = tag.name.lower()
489
- if tag_name == "img" and ar is not None and save:
502
+ if save:
503
+ for tag in soup.find_all('img'):
490
504
  if (src := tag.get('src')) and src.startswith("data:image"):
491
505
  file = base64_to_image(src)
492
- upload = rt.models.uploads.Upload(file=file, user=ar.get_user())
493
- sar = upload.get_default_table().create_request(parent=ar)
494
- upload.save_new_instance(sar)
495
- rt.models.checkdata.fix_instance(ar, upload)
496
- tag.replace_with(f'[file {upload.pk}]')
506
+ user = ar.get_user() if ar else dd.plugins.users.get_demo_user()
507
+ obj = rt.models.uploads.Upload(file=file, user=user)
508
+ tag["src"] = obj.get_media_file().get_image_url()
509
+ if ar:
510
+ sar = obj.get_default_table().create_request(parent=ar)
511
+ obj.save_new_instance(sar) # create comment or notify message
512
+ else:
513
+ obj.save()
514
+ rt.models.checkdata.fix_instance(ar, obj) # create thumbnail
515
+ # style = ''
516
+ # if (s := tag.get("style")):
517
+ # # if not s.strip().endswith(";"):
518
+ # # s += ";"
519
+ # style += s
520
+ # if (w := tag.get("width")) is not None:
521
+ # style += f" width: {w}px;"
522
+ # else:
523
+ # # 20ex comes from the default value of Format.height in rstgen.sphinxconf.sigal_image
524
+ # style += " height: 20ex;"
525
+ # tag.replace_with(f'[file {upload.pk} style="{style}"]')
526
+ tag[DATA_UPLOAD_ID] = obj.id
527
+ if mentions is not None:
528
+ for tag in soup.find_all('img'):
529
+ if (upload_id := tag.get(DATA_UPLOAD_ID)):
530
+ mentions.add(rt.models.uploads.Upload(id=upload_id))
497
531
 
498
532
 
499
533
  register_sanitizer(on_sanitize)
lino/modlib/uploads/ui.py CHANGED
@@ -72,6 +72,7 @@ class UploadDetail(dd.DetailLayout):
72
72
  upload_area type
73
73
  description
74
74
  source
75
+ memo.MentionsByTarget
75
76
  """
76
77
 
77
78
  window_size = (80, "auto")
@@ -63,7 +63,7 @@ class Previewer:
63
63
  # The bare media previewer. It doesn't do any real work.
64
64
  base_dir = None
65
65
  max_width = None
66
- PREVIEW_SUFFIXES = {'.png', '.jpg'}
66
+ PREVIEW_SUFFIXES = {'.png', '.jpg', '.jpeg'}
67
67
 
68
68
  def check_preview(self, obj, fix=False):
69
69
  return []
@@ -71,7 +71,7 @@ class Previewer:
71
71
 
72
72
  class FilePreviewer(Previewer):
73
73
  # A media previewer that builds thumbnails in a separate directory tree
74
- PREVIEW_SUFFIXES = {'.png', '.jpg', '.pdf'}
74
+ PREVIEW_SUFFIXES = {'.png', '.jpg', '.jpeg', '.pdf'}
75
75
 
76
76
  def __init__(self, base_dir=None, max_width=None):
77
77
  self.base_dir = base_dir
@@ -57,14 +57,13 @@ class Plugin(ad.Plugin):
57
57
 
58
58
  _demo_user = None # the cached User object
59
59
 
60
- def get_demo_user(self, checker, obj):
60
+ def get_demo_user(self):
61
61
  if self.demo_username is None:
62
62
  return None
63
63
  if self._demo_user is None:
64
64
  User = self.site.models.users.User
65
65
  try:
66
- self._demo_user = User.objects.get(
67
- username=self.demo_username)
66
+ self._demo_user = User.objects.get(username=self.demo_username)
68
67
  except User.DoesNotExist:
69
68
  msg = "Invalid username '{0}' in `demo_username` "
70
69
  msg = msg.format(self.demo_username)
@@ -18,6 +18,8 @@ from lino.core.roles import SiteAdmin
18
18
  from lino.core import auth, layouts
19
19
  from lino.core.actions import SubmitInsert
20
20
 
21
+ MSG_TAKEN = _("The username {} is taken. Please choose another one")
22
+
21
23
 
22
24
  def send_welcome_email(ar, obj, recipients):
23
25
  sender = settings.SERVER_EMAIL
@@ -36,14 +38,10 @@ class CheckedSubmitInsert(SubmitInsert):
36
38
 
37
39
  def run_from_ui(self, ar, **kw):
38
40
  obj = ar.create_instance_from_request()
39
- qs = obj.__class__.objects.filter(username=obj.username)
40
- if len(qs) > 0:
41
- msg = _("The username {} is taken. " "Please choose another one").format(
42
- obj.username
43
- )
44
-
45
- ar.error(msg)
46
- return
41
+ if obj.username:
42
+ if obj.__class__.objects.filter(username=obj.username).exist():
43
+ ar.error(MSG_TAKEN.format(obj.username))
44
+ return
47
45
 
48
46
  def ok(ar2):
49
47
  SubmitInsert.run_from_ui(self, ar, **kw)
@@ -96,16 +94,13 @@ class CreateAccount(dd.Action):
96
94
  pv = ar.action_param_values
97
95
  if not pv["username"]:
98
96
  pv["username"] = pv["email"]
99
- qs = User.objects.filter(username=pv["username"])
100
- if len(qs) > 0:
101
- msg = _("The username {} is taken. " "Please choose another one").format(
102
- pv.username
103
- )
104
- ar.error(msg)
105
- # ar.set_response(close_window=False)
106
- return
97
+ if pv["username"]:
98
+ if User.objects.filter(username=pv["username"]).exist():
99
+ ar.error(MSG_TAKEN.format(pv.username))
100
+ # ar.set_response(close_window=False)
101
+ return
107
102
 
108
- validate_password(pv['password'])
103
+ validate_password(pv['password'])
109
104
 
110
105
  ut = UserTypes.get_by_name(dd.plugins.users.user_type_new)
111
106
  # pv.pop('social_auth_links')
@@ -0,0 +1,20 @@
1
+ # -*- coding: UTF-8 -*-
2
+ # Copyright 2017-2025 Rumma & Ko Ltd
3
+ # License: GNU Affero General Public License v3 (see file COPYING for details)
4
+
5
+ from lino.api import dd, rt, _
6
+
7
+
8
+ def objects():
9
+ User = rt.models.users.User
10
+ UserTypes = rt.models.users.UserTypes
11
+
12
+ def user(username, **kwargs):
13
+ kwargs.update(user_type=UserTypes.user, username=username)
14
+ if not dd.plugins.users.with_nickname:
15
+ kwargs.pop('nickname', None)
16
+ return User(**kwargs)
17
+
18
+ yield user("andy", first_name="Andreas", last_name="Anderson", nickname="Andy", email="andy@example.com")
19
+ yield user("bert", first_name="Albert", last_name="Bernstein", nickname="Bert", email="bert@example.com")
20
+ yield user("chloe", first_name="Chloe", last_name="Cleoment", email="chloe@example.com")
@@ -490,18 +490,16 @@ class PrivacyRelevant(dd.Model):
490
490
  qs = super().get_request_queryset(ar, **filter)
491
491
  user = ar.get_user()
492
492
  if user.is_anonymous:
493
- if dd.is_installed('groups'):
494
- qs = qs.filter(group__private=False)
493
+ # if dd.is_installed('groups'):
494
+ # qs = qs.filter(group__private=False)
495
495
  return qs.filter(private=False)
496
- # if user.current_group is not None:
497
- # qs = qs.filter(group=user.current_group)
498
496
  if user.user_type.has_required_roles([SiteAdmin]):
499
497
  return qs
500
498
  flt = Q(private=False)
501
499
  if issubclass(cls, UserAuthored):
502
500
  flt |= Q(user=user)
503
501
  if dd.is_installed('groups'):
504
- flt |= Q(group__private=False)
502
+ # flt |= Q(group__private=False)
505
503
  flt |= Q(group__members__user=user)
506
504
  qs = qs.filter(flt).distinct()
507
505
  return qs
@@ -518,9 +516,11 @@ class PrivacyRelevant(dd.Model):
518
516
  self.private = self.group.private
519
517
 
520
518
  def get_default_group(self):
521
- return None
519
+ return None # dd.plugins.groups.get_default_group()
522
520
 
523
521
  def full_clean(self):
524
522
  if not self.group_id:
525
523
  self.group = self.get_default_group()
524
+ if self.group and self.group.private:
525
+ self.private = True
526
526
  super().full_clean()