lino 25.8.3__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.
- lino/__init__.py +1 -1
- lino/api/dd.py +0 -1
- lino/core/__init__.py +0 -1
- lino/core/actions.py +1 -1
- lino/core/actors.py +8 -0
- lino/core/elems.py +1 -1
- lino/core/fields.py +4 -1
- lino/core/model.py +2 -11
- lino/core/requests.py +8 -7
- lino/core/site.py +0 -79
- lino/core/user_types.py +1 -10
- lino/help_texts.py +1 -5
- lino/management/commands/initdb.py +0 -3
- lino/modlib/__init__.py +0 -1
- lino/modlib/bootstrap5/README.txt +1 -1
- lino/modlib/bootstrap5/__init__.py +34 -38
- lino/modlib/bootstrap5/config/bootstrap5/base.html +4 -0
- lino/modlib/bootstrap5/models.py +23 -23
- lino/modlib/bootstrap5/views.py +2 -107
- lino/modlib/checkdata/choicelists.py +1 -1
- lino/modlib/comments/fixtures/demo2.py +1 -0
- lino/modlib/comments/ui.py +7 -7
- lino/modlib/extjs/__init__.py +2 -4
- lino/modlib/extjs/config/extjs/index.html +1 -1
- lino/modlib/extjs/ext_renderer.py +1 -7
- lino/modlib/extjs/views.py +2 -0
- lino/modlib/help/models.py +1 -12
- lino/modlib/linod/mixins.py +3 -2
- lino/modlib/memo/__init__.py +10 -9
- lino/modlib/memo/mixins.py +38 -21
- lino/modlib/memo/models.py +10 -7
- lino/modlib/memo/parser.py +3 -1
- lino/modlib/notify/models.py +6 -9
- lino/modlib/publisher/__init__.py +12 -7
- lino/modlib/publisher/choicelists.py +9 -64
- lino/modlib/publisher/config/publisher/page.pub.html +73 -7
- lino/modlib/publisher/fixtures/std.py +14 -1
- lino/modlib/publisher/mixins.py +41 -12
- lino/modlib/publisher/models.py +74 -75
- lino/modlib/publisher/renderer.py +28 -12
- lino/modlib/publisher/ui.py +35 -35
- lino/modlib/publisher/views.py +59 -24
- lino/modlib/system/models.py +3 -2
- lino/modlib/uploads/__init__.py +1 -0
- lino/modlib/uploads/mixins.py +2 -2
- lino/modlib/uploads/models.py +55 -21
- lino/modlib/uploads/ui.py +1 -0
- lino/modlib/uploads/utils.py +2 -2
- lino/modlib/users/__init__.py +2 -3
- lino/modlib/users/actions.py +12 -17
- lino/modlib/users/models.py +37 -36
- lino/modlib/weasyprint/choicelists.py +6 -0
- lino/utils/diag.py +5 -3
- lino/utils/html.py +103 -0
- lino/utils/mldbc/mixins.py +2 -2
- lino/utils/soup.py +16 -8
- {lino-25.8.3.dist-info → lino-25.9.0.dist-info}/METADATA +1 -1
- {lino-25.8.3.dist-info → lino-25.9.0.dist-info}/RECORD +61 -61
- {lino-25.8.3.dist-info → lino-25.9.0.dist-info}/WHEEL +0 -0
- {lino-25.8.3.dist-info → lino-25.9.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.8.3.dist-info → lino-25.9.0.dist-info}/licenses/COPYING +0 -0
lino/modlib/publisher/ui.py
CHANGED
@@ -9,30 +9,30 @@ from django.db import models
|
|
9
9
|
|
10
10
|
from lino.api import dd, rt, _
|
11
11
|
from lino.utils.html import E
|
12
|
+
from lino.utils.soup import MORE_MARKER
|
12
13
|
from lino.core import constants
|
13
14
|
# from lino.core.renderer import add_user_language
|
14
15
|
from lino.modlib.office.roles import OfficeUser
|
15
16
|
|
16
|
-
from .choicelists import
|
17
|
+
from .choicelists import SpecialPages
|
17
18
|
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
""
|
23
|
-
|
24
|
-
|
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"
|
25
26
|
|
26
27
|
|
27
28
|
class PageDetail(dd.DetailLayout):
|
28
|
-
main = "general first_panel more"
|
29
|
+
# main = "general first_panel more"
|
30
|
+
main = f"general first_panel {VARTABS} more"
|
29
31
|
|
30
32
|
first_panel = dd.Panel(
|
31
33
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
label=_("Preview"),
|
35
|
-
)
|
34
|
+
treeview_panel:20 preview:60
|
35
|
+
""", label=_("Preview"))
|
36
36
|
|
37
37
|
general = dd.Panel(
|
38
38
|
"""
|
@@ -42,16 +42,15 @@ class PageDetail(dd.DetailLayout):
|
|
42
42
|
required_roles=dd.login_required(OfficeUser),
|
43
43
|
)
|
44
44
|
|
45
|
-
more = dd.Panel(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
)
|
45
|
+
# more = dd.Panel(
|
46
|
+
# VARTABS,
|
47
|
+
# label=_("Discussion"),
|
48
|
+
# required_roles=dd.login_required(OfficeUser),
|
49
|
+
# )
|
50
50
|
|
51
51
|
content_panel = """
|
52
52
|
title id
|
53
53
|
body
|
54
|
-
publisher.PagesByParent
|
55
54
|
"""
|
56
55
|
|
57
56
|
# right_panel = """
|
@@ -62,23 +61,27 @@ class PageDetail(dd.DetailLayout):
|
|
62
61
|
# """
|
63
62
|
|
64
63
|
right_panel = """
|
65
|
-
|
66
|
-
|
64
|
+
parent seqno
|
65
|
+
publisher.PagesByParent
|
66
|
+
"""
|
67
|
+
|
68
|
+
more = dd.Panel("""
|
69
|
+
publisher_tree language
|
67
70
|
child_node_depth main_image
|
68
|
-
special_page filler
|
69
|
-
publishing_state
|
71
|
+
special_page #filler
|
72
|
+
publishing_state
|
70
73
|
publisher.TranslationsByPage
|
71
|
-
"""
|
74
|
+
""", label=_("More"))
|
72
75
|
|
73
76
|
|
74
77
|
class Pages(dd.Table):
|
75
78
|
model = "publisher.Page"
|
76
|
-
column_names = "
|
79
|
+
column_names = "title publisher_tree id *"
|
77
80
|
detail_layout = "publisher.PageDetail"
|
78
81
|
insert_layout = """
|
79
82
|
title
|
80
|
-
|
81
|
-
#
|
83
|
+
publisher_tree
|
84
|
+
language #filler
|
82
85
|
"""
|
83
86
|
default_display_modes = {None: constants.DISPLAY_MODE_LIST}
|
84
87
|
|
@@ -101,7 +104,7 @@ class PagesByParent(Pages):
|
|
101
104
|
class TranslationsByPage(Pages):
|
102
105
|
master_key = "translated_from"
|
103
106
|
label = _("Translations")
|
104
|
-
column_names = "
|
107
|
+
column_names = "title language id *"
|
105
108
|
default_display_modes = {None: constants.DISPLAY_MODE_SUMMARY}
|
106
109
|
|
107
110
|
@classmethod
|
@@ -110,16 +113,13 @@ class TranslationsByPage(Pages):
|
|
110
113
|
return E.span("({}) ".format(obj.language), obj.as_summary_item(ar, text, **kwargs))
|
111
114
|
|
112
115
|
|
113
|
-
class
|
114
|
-
|
115
|
-
|
116
|
-
column_names = "ref title language"
|
117
|
-
# default_display_modes = {None: constants.DISPLAY_MODE_LIST}
|
118
|
-
|
116
|
+
class Trees(dd.Table):
|
117
|
+
model = "publisher.Tree"
|
118
|
+
column_names = "ref root_page group private id *"
|
119
119
|
|
120
|
-
filler = PageFillers.add_item(RootPages)
|
121
120
|
|
122
121
|
SpecialPages.add_item(
|
123
|
-
"pages", filler=filler,
|
122
|
+
"pages", # filler=filler,
|
123
|
+
body=_("List of root pages.") + MORE_MARKER + " [show publisher.Trees]",
|
124
124
|
title=_("Root pages"),
|
125
125
|
parent='home')
|
lino/modlib/publisher/views.py
CHANGED
@@ -1,13 +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
|
10
11
|
from lino.core.requests import BaseRequest
|
12
|
+
from lino.core.views import json_response
|
11
13
|
|
12
14
|
|
13
15
|
class Element(View):
|
@@ -24,50 +26,83 @@ class Element(View):
|
|
24
26
|
|
25
27
|
# kw = dict(actor=self.publisher_model.get_default_table(),
|
26
28
|
# request=request, renderer=rnd, permalink_uris=True)
|
27
|
-
|
29
|
+
kw = dict(renderer=rnd, request=request)
|
28
30
|
# kw = dict(renderer=rnd, permalink_uris=True)
|
29
31
|
# if rnd.front_end.media_name == 'react':
|
30
32
|
# kw.update(hash_router=True)
|
31
33
|
|
32
|
-
|
34
|
+
kw.update(selected_pks=[pk])
|
33
35
|
#
|
34
|
-
# try:
|
35
|
-
# ar = self.table_class.create_request(request=request, **kw)
|
36
|
-
# except ObjectDoesNotExist as e:
|
37
|
-
# # print("20240911", e)
|
38
|
-
# return http.HttpResponseNotFound(f"No row #{pk} in {self.table_class} ({e})")
|
39
|
-
# if len(ar.selected_rows) == 0:
|
40
|
-
# # print(f"20241003 Oops {ar} has no rows")
|
41
|
-
# return http.HttpResponseNotFound(f"20241003 No row #{pk} in {self.table_class}")
|
42
|
-
# obj = ar.selected_rows[0]
|
43
|
-
|
44
|
-
m = self.table_class.model
|
45
36
|
try:
|
46
|
-
|
47
|
-
except
|
48
|
-
|
49
|
-
|
37
|
+
ar = self.table_class.create_request(**kw)
|
38
|
+
except ObjectDoesNotExist as e:
|
39
|
+
# print("20240911", e)
|
40
|
+
return http.HttpResponseNotFound(
|
41
|
+
f"No row #{pk} in {self.table_class} ({e})")
|
42
|
+
if len(ar.selected_rows) == 0:
|
43
|
+
# print(f"20241003 Oops {ar} has no rows")
|
44
|
+
return http.HttpResponseNotFound(
|
45
|
+
f"20241003 No row #{pk} in {self.table_class}")
|
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])
|
50
54
|
# ar = BaseRequest(renderer=rnd, request=request)
|
51
55
|
return obj.get_publisher_response(ar)
|
52
56
|
|
53
57
|
|
54
58
|
class Index(View):
|
55
|
-
|
59
|
+
|
60
|
+
ref = 'index'
|
61
|
+
|
62
|
+
def get(self, request):
|
63
|
+
Tree = settings.SITE.models.publisher.Tree
|
56
64
|
dv = settings.SITE.models.publisher.Pages
|
57
65
|
if len(settings.SITE.languages) == 1:
|
58
66
|
# language = settings.SITE.languages[0].django_code
|
59
67
|
language = translation.get_language()
|
60
68
|
else:
|
61
69
|
language = request.LANGUAGE_CODE
|
62
|
-
if ref == '':
|
63
|
-
ref = 'index'
|
64
70
|
try:
|
65
|
-
|
66
|
-
except
|
67
|
-
return http.HttpResponseNotFound(f"No
|
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}")
|
68
84
|
|
69
85
|
# print("20231025", index_node)
|
70
86
|
rnd = settings.SITE.plugins.publisher.renderer
|
71
87
|
ar = dv.create_request(request=request, renderer=rnd,
|
72
88
|
selected_rows=[obj])
|
73
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', '/'))
|
lino/modlib/system/models.py
CHANGED
@@ -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(
|
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 =
|
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()
|
lino/modlib/uploads/__init__.py
CHANGED
lino/modlib/uploads/mixins.py
CHANGED
@@ -134,7 +134,7 @@ class UploadController(dd.Model):
|
|
134
134
|
|
135
135
|
|
136
136
|
class GalleryViewable(dd.Model):
|
137
|
-
class Meta
|
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
|
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
|
lino/modlib/uploads/models.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
+ '
|
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
|
-
|
488
|
-
|
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
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
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
lino/modlib/uploads/utils.py
CHANGED
@@ -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
|
lino/modlib/users/__init__.py
CHANGED
@@ -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
|
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)
|
lino/modlib/users/actions.py
CHANGED
@@ -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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
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')
|
lino/modlib/users/models.py
CHANGED
@@ -476,43 +476,44 @@ if dd.plugins.users.allow_online_registration:
|
|
476
476
|
About.create_account = CreateAccount()
|
477
477
|
|
478
478
|
|
479
|
-
|
480
|
-
def setup_memo_commands(sender=None, **kwargs):
|
481
|
-
# See :doc:`/specs/memo`
|
482
|
-
|
483
|
-
if not sender.is_installed("memo"):
|
484
|
-
return
|
485
|
-
|
486
|
-
mp = sender.plugins.memo.parser
|
487
|
-
mp.add_suggester(
|
488
|
-
"@",
|
489
|
-
sender.models.users.User.objects.filter(username__isnull=False).order_by(
|
490
|
-
"username"
|
491
|
-
),
|
492
|
-
"username",
|
493
|
-
)
|
479
|
+
if dd.is_installed("memo"):
|
494
480
|
|
481
|
+
@dd.receiver(dd.post_startup)
|
482
|
+
def setup_memo_commands(sender=None, **kwargs):
|
483
|
+
# See :doc:`/specs/memo`
|
495
484
|
|
496
|
-
|
497
|
-
|
498
|
-
if not me.is_verified():
|
499
|
-
# sar = rt.models.users.Me.create_request(parent=ar)
|
500
|
-
sar = ar
|
501
|
-
if me.email:
|
502
|
-
# verify_me =
|
503
|
-
msg = format_html(
|
504
|
-
_("Your email address ({email}) is not verified, "
|
505
|
-
"please check your mailbox and {verify} or {resend}."),
|
506
|
-
email=me.email,
|
507
|
-
verify=tostring(sar.instance_action_button(
|
508
|
-
me.verify_me, _("verify now"))),
|
509
|
-
resend=tostring(sar.instance_action_button(
|
510
|
-
me.send_welcome_email, _("re-send our welcome email"))))
|
511
|
-
else:
|
512
|
-
msg = format_html(
|
513
|
-
_("You have no email address, please {edit}."),
|
514
|
-
edit=tostring(sar.obj2html(me, _("edit your user settings"))))
|
515
|
-
yield mark_safe(msg)
|
485
|
+
# if not sender.is_installed("memo"):
|
486
|
+
# return
|
516
487
|
|
488
|
+
mp = sender.plugins.memo.parser
|
489
|
+
mp.add_suggester(
|
490
|
+
"@",
|
491
|
+
sender.models.users.User.objects.filter(
|
492
|
+
username__isnull=False).order_by("username"),
|
493
|
+
"username")
|
494
|
+
|
495
|
+
|
496
|
+
if dd.plugins.users.allow_online_registration:
|
517
497
|
|
518
|
-
|
498
|
+
def welcome_messages(ar):
|
499
|
+
me = ar.get_user()
|
500
|
+
if not me.is_verified():
|
501
|
+
# sar = rt.models.users.Me.create_request(parent=ar)
|
502
|
+
sar = ar
|
503
|
+
if me.email:
|
504
|
+
# verify_me =
|
505
|
+
msg = format_html(
|
506
|
+
_("Your email address ({email}) is not verified, "
|
507
|
+
"please check your mailbox and {verify} or {resend}."),
|
508
|
+
email=me.email,
|
509
|
+
verify=tostring(sar.instance_action_button(
|
510
|
+
me.verify_me, _("verify now"))),
|
511
|
+
resend=tostring(sar.instance_action_button(
|
512
|
+
me.send_welcome_email, _("re-send our welcome email"))))
|
513
|
+
else:
|
514
|
+
msg = format_html(
|
515
|
+
_("You have no email address, please {edit}."),
|
516
|
+
edit=tostring(sar.obj2html(me, _("edit your user settings"))))
|
517
|
+
yield mark_safe(msg)
|
518
|
+
|
519
|
+
dd.add_welcome_handler(welcome_messages)
|
@@ -14,6 +14,12 @@ except ImportError:
|
|
14
14
|
BULMA_CSS = None
|
15
15
|
|
16
16
|
if dd.plugins.weasyprint.with_bulma:
|
17
|
+
|
18
|
+
# Bulma causes weayprint to issue many warnings, more than 2000 during one
|
19
|
+
# tested doc. So we deactivate them:
|
20
|
+
from weasyprint.logger import LOGGER, logging
|
21
|
+
LOGGER.setLevel(logging.ERROR)
|
22
|
+
|
17
23
|
try:
|
18
24
|
from pathlib import Path
|
19
25
|
import bulma
|
lino/utils/diag.py
CHANGED
@@ -144,14 +144,14 @@ class Analyzer(object):
|
|
144
144
|
|
145
145
|
return rstgen.ul(items)
|
146
146
|
|
147
|
-
def
|
147
|
+
def show_db_structure(self, sort_fields=False, sort_models=True, verbose_names=False):
|
148
148
|
"""Show a bullet list of all models and their fields.
|
149
149
|
|
150
150
|
Both the list of models and the fields of each model can optionally be
|
151
151
|
sorted alphabetically. The models are sorted by default, the fields not.
|
152
152
|
|
153
153
|
"""
|
154
|
-
self.analyze()
|
154
|
+
# self.analyze()
|
155
155
|
items = []
|
156
156
|
for model in get_models():
|
157
157
|
# names = []
|
@@ -170,7 +170,9 @@ class Analyzer(object):
|
|
170
170
|
|
171
171
|
if sort_models:
|
172
172
|
items = sorted(items)
|
173
|
-
|
173
|
+
print(rstgen.ul(items))
|
174
|
+
|
175
|
+
show_database_structure = show_db_structure
|
174
176
|
|
175
177
|
def show_fields(self, model, field_names=None, languages=None):
|
176
178
|
model = dd.resolve_model(model)
|