lino 25.6.1__py3-none-any.whl → 25.7.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.
- lino/__init__.py +1 -1
- lino/api/dd.py +1 -0
- lino/api/doctest.py +21 -0
- lino/core/actions.py +80 -25
- lino/core/actors.py +54 -27
- lino/core/boundaction.py +16 -0
- lino/core/choicelists.py +7 -7
- lino/core/constants.py +3 -0
- lino/core/dashboard.py +4 -2
- lino/core/dbtables.py +2 -2
- lino/core/elems.py +38 -13
- lino/core/fields.py +20 -11
- lino/core/kernel.py +8 -0
- lino/core/layouts.py +6 -2
- lino/core/menus.py +3 -6
- lino/core/model.py +5 -4
- lino/core/renderer.py +20 -9
- lino/core/requests.py +8 -7
- lino/core/signals.py +1 -0
- lino/core/site.py +48 -28
- lino/core/store.py +4 -2
- lino/core/tables.py +23 -10
- lino/core/utils.py +4 -1
- lino/core/workflows.py +2 -1
- lino/help_texts.py +1 -2
- lino/management/commands/prep.py +2 -2
- lino/management/commands/show.py +8 -10
- lino/mixins/__init__.py +14 -13
- lino/mixins/periods.py +2 -0
- lino/mixins/sequenced.py +1 -1
- lino/modlib/about/models.py +4 -3
- lino/modlib/checkdata/__init__.py +42 -36
- lino/modlib/checkdata/choicelists.py +9 -1
- lino/modlib/checkdata/fixtures/checkdata.py +4 -2
- lino/modlib/checkdata/management/commands/checkdata.py +3 -3
- lino/modlib/checkdata/models.py +9 -2
- lino/modlib/comments/models.py +4 -3
- lino/modlib/extjs/ext_renderer.py +4 -4
- lino/modlib/extjs/views.py +8 -2
- lino/modlib/gfks/fields.py +1 -1
- lino/modlib/help/__init__.py +3 -3
- lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
- lino/modlib/help/fixtures/demo2.py +6 -1
- lino/modlib/help/management/commands/makehelp.py +4 -1
- lino/modlib/help/models.py +4 -1
- lino/modlib/help/utils.py +12 -6
- lino/modlib/linod/choicelists.py +57 -4
- lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
- lino/modlib/linod/management/commands/linod.py +0 -13
- lino/modlib/linod/mixins.py +8 -0
- lino/modlib/linod/models.py +29 -30
- lino/modlib/memo/__init__.py +7 -7
- lino/modlib/memo/management/__init__,py +0 -0
- lino/modlib/memo/management/commands/__init__.py +0 -0
- lino/modlib/memo/management/commands/removeurls.py +67 -0
- lino/modlib/memo/mixins.py +1 -9
- lino/modlib/memo/parser.py +1 -1
- lino/modlib/notify/config/notify/summary.eml +5 -2
- lino/modlib/notify/fixtures/demo2.py +5 -6
- lino/modlib/notify/models.py +9 -10
- lino/modlib/periods/__init__.py +11 -8
- lino/modlib/periods/choicelists.py +16 -10
- lino/modlib/periods/models.py +45 -45
- lino/modlib/publisher/renderer.py +2 -5
- lino/modlib/summaries/fixtures/checksummaries.py +4 -2
- lino/modlib/system/models.py +17 -18
- lino/modlib/uploads/fixtures/demo.py +9 -3
- lino/modlib/uploads/mixins.py +5 -2
- lino/modlib/uploads/models.py +15 -9
- lino/modlib/uploads/utils.py +4 -1
- lino/modlib/users/__init__.py +59 -18
- lino/modlib/users/actions.py +24 -20
- lino/modlib/users/fixtures/demo_users.py +2 -35
- lino/modlib/users/mixins.py +3 -4
- lino/modlib/users/models.py +53 -13
- lino/modlib/users/ui.py +30 -16
- lino/modlib/users/utils.py +5 -6
- lino/projects/std/settings.py +1 -1
- lino/sphinxcontrib/logo/templates/footer.html +1 -0
- lino/utils/ajax.py +1 -1
- lino/utils/cycler.py +5 -0
- lino/utils/dbhash.py +4 -9
- lino/utils/dpy.py +2 -2
- lino/utils/format_date.py +4 -3
- lino/utils/html.py +13 -5
- lino/utils/jsgen.py +3 -2
- lino/utils/quantities.py +8 -0
- lino/utils/soup.py +75 -106
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/METADATA +1 -1
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/RECORD +93 -90
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/WHEEL +0 -0
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/COPYING +0 -0
lino/modlib/help/__init__.py
CHANGED
@@ -66,10 +66,10 @@ class Plugin(ad.Plugin):
|
|
66
66
|
# site.install_help_text(ba.action, ba.action.__class__)
|
67
67
|
if a.model is not None:
|
68
68
|
self.htl.install_help_text(
|
69
|
-
ba
|
69
|
+
ba, a.model, ba.action.action_name
|
70
70
|
)
|
71
|
-
self.htl.install_help_text(ba
|
72
|
-
self.htl.install_help_text(ba.
|
71
|
+
self.htl.install_help_text(ba, a, ba.action.action_name)
|
72
|
+
self.htl.install_help_text(ba.__class__)
|
73
73
|
# htl.install_help_text(
|
74
74
|
# ba.action, ba.action.__class__,
|
75
75
|
# attrname=ba.action.action_name)
|
@@ -18,9 +18,9 @@ from lino.sphinxcontrib import configure ; configure(globals())
|
|
18
18
|
project = "{{settings.SITE.title}}"
|
19
19
|
html_title = "{{settings.SITE.title}}"
|
20
20
|
|
21
|
-
{% if settings.SITE.
|
21
|
+
{% if settings.SITE.plugins.contacts.site_owner %}
|
22
22
|
import datetime
|
23
|
-
copyright = "{} {{settings.SITE.
|
23
|
+
copyright = "{} {{settings.SITE.plugins.contacts.site_owner}}".format(
|
24
24
|
datetime.date.today())
|
25
25
|
{% endif %}
|
26
26
|
|
@@ -14,10 +14,15 @@ if dd.get_plugin_setting("help", "use_contacts"):
|
|
14
14
|
return help.SiteContact(site_contact_type=type, company=company, **kwargs)
|
15
15
|
|
16
16
|
def objects():
|
17
|
-
yield site_contact("owner", settings.SITE.
|
17
|
+
yield site_contact("owner", settings.SITE.plugins.contacts.site_owner)
|
18
18
|
yield site_contact("serveradmin", contacts.Company.objects.get(pk=PS+6))
|
19
19
|
yield site_contact(
|
20
20
|
"hotline",
|
21
21
|
contact_person=contacts.Person.objects.get(pk=PS+13),
|
22
22
|
**dd.babelkw("remark", _("Mon and Fri from 11:30 to 12:00")),
|
23
23
|
)
|
24
|
+
|
25
|
+
else:
|
26
|
+
|
27
|
+
def objects():
|
28
|
+
return []
|
@@ -410,7 +410,10 @@ class Command(GeneratingCommand):
|
|
410
410
|
|
411
411
|
def action2par(self, a):
|
412
412
|
if isinstance(a, BoundAction):
|
413
|
-
a = a.action
|
413
|
+
# a = a.action
|
414
|
+
return shortpar(a.action.action_name, a.action.label,
|
415
|
+
a.get_help_text() or _("See {}.").format(
|
416
|
+
self.refto(a.action)))
|
414
417
|
return shortpar(a.action_name, a.label, self.get_help_text_from_field(a))
|
415
418
|
|
416
419
|
def collect_refs(self, cls, role):
|
lino/modlib/help/models.py
CHANGED
@@ -20,7 +20,10 @@ class OpenHelpWindow(dd.Action):
|
|
20
20
|
# this doesn't look nice. But adding spaces breaks a series of doctests, so
|
21
21
|
# I undid that change:
|
22
22
|
# button_text = " ? "
|
23
|
-
button_text = "?"
|
23
|
+
# button_text = "?"
|
24
|
+
# button_text = "🛈"
|
25
|
+
button_text = "ⓘ" # 24d8
|
26
|
+
# button_text = "🯄" # 1fbc4
|
24
27
|
select_rows = False
|
25
28
|
help_text = _("Open Help Window")
|
26
29
|
show_in_plain = True
|
lino/modlib/help/utils.py
CHANGED
@@ -79,8 +79,10 @@ class HelpTextsLoader:
|
|
79
79
|
# if m in self.unhelpful_classes:
|
80
80
|
# continue
|
81
81
|
k, txt = self.get_help_text_for_class(m, attrname)
|
82
|
-
# if attrname == "
|
83
|
-
#
|
82
|
+
# if attrname == "update_guests":
|
83
|
+
# # if str(cls) == "cal.Events":
|
84
|
+
# # if k == "lino_xl.lib.cal.Events.update_guests":
|
85
|
+
# print(f"20250622 {hash(fld)} {cls} {k} {fld.help_text} {txt}")
|
84
86
|
if txt is None:
|
85
87
|
if debug:
|
86
88
|
print(
|
@@ -102,11 +104,15 @@ class HelpTextsLoader:
|
|
102
104
|
cls, attrname, txt, k, fld
|
103
105
|
)
|
104
106
|
)
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
107
|
+
fld.help_text = txt
|
108
|
+
# fld._found = True
|
109
|
+
# try:
|
110
|
+
# fld.help_text = txt
|
111
|
+
# except AttributeError as e:
|
112
|
+
# raise AttributeError("20240329 {} {}".format(fld, e))
|
109
113
|
fld._lino_help_ref = k # for makehelp
|
114
|
+
# if attrname == "update_guests":
|
115
|
+
# print(f"20250622 {fld} {cls} {k} {txt}")
|
110
116
|
return
|
111
117
|
if debug:
|
112
118
|
print("20170824 {}.{} : no help_text".format(cls, attrname))
|
lino/modlib/linod/choicelists.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2023-
|
2
|
+
# Copyright 2023-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
# See https://dev.lino-framework.org/plugins/linod.html
|
5
5
|
|
6
6
|
import logging
|
7
7
|
from typing import Callable
|
8
|
-
from
|
8
|
+
from asgiref.sync import sync_to_async
|
9
|
+
from django.conf import settings
|
10
|
+
from lino.api import dd, rt, _
|
9
11
|
from lino.core.roles import SiteStaff
|
10
12
|
|
11
13
|
|
@@ -13,14 +15,39 @@ class Procedure(dd.Choice):
|
|
13
15
|
func: Callable
|
14
16
|
kwargs: dict
|
15
17
|
class_name: str
|
18
|
+
run_after: Callable | None
|
19
|
+
during_initdb: bool
|
16
20
|
|
17
|
-
def __init__(
|
21
|
+
def __init__(
|
22
|
+
self, func, class_name=None, run_after=None, during_initdb=True, **kwargs
|
23
|
+
):
|
18
24
|
name = func.__name__
|
19
25
|
super().__init__(name, name, name)
|
20
26
|
self.func = func
|
21
27
|
self.class_name = class_name
|
28
|
+
self.run_after = run_after
|
29
|
+
self.during_initdb = during_initdb
|
22
30
|
self.kwargs = kwargs
|
23
31
|
|
32
|
+
async def install_if_needed(self, ar):
|
33
|
+
if self.class_name == "linod.SystemTask":
|
34
|
+
SystemTask = rt.models.linod.SystemTask
|
35
|
+
if not await SystemTask.objects.filter(procedure=self).aexists():
|
36
|
+
await ar.adebug("Create background task for %r", self)
|
37
|
+
# Without a value for start_date and end_date, Django would
|
38
|
+
# dd.today(), which does a database lookup to find
|
39
|
+
# SiteConfig.simulate_today, which would raise a "cannot call
|
40
|
+
# this from an async context" exception.
|
41
|
+
# self.kwargs.update(start_date=None, end_date=None)
|
42
|
+
self.kwargs.setdefault('start_date', await settings.SITE.atoday())
|
43
|
+
# self.kwargs.setdefault('end_date', None)
|
44
|
+
obj = SystemTask(procedure=self, **self.kwargs)
|
45
|
+
# every_unit=proc.every_unit, every=proc.every_value)
|
46
|
+
if obj.every_unit == "secondly":
|
47
|
+
obj.log_level = "WARNING"
|
48
|
+
await sync_to_async(obj.full_clean)()
|
49
|
+
await obj.asave()
|
50
|
+
|
24
51
|
def run(self, ar):
|
25
52
|
return self.func(ar)
|
26
53
|
|
@@ -36,7 +63,33 @@ class Procedures(dd.ChoiceList):
|
|
36
63
|
column_names = "value name text class_name kwargs"
|
37
64
|
required_roles = dd.login_required(SiteStaff)
|
38
65
|
|
39
|
-
task_classes = []
|
66
|
+
# task_classes = []
|
67
|
+
|
68
|
+
# @classmethod
|
69
|
+
# def sort_for_runner(cls):
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def add_item(cls, *args, **kwargs):
|
73
|
+
rv = super().add_item(*args, **kwargs)
|
74
|
+
newchoices = []
|
75
|
+
collected_funcs = set()
|
76
|
+
todo = cls.choices
|
77
|
+
while len(todo):
|
78
|
+
later = []
|
79
|
+
for p_text in todo:
|
80
|
+
p, text = p_text
|
81
|
+
if p.run_after is None or p.run_after in collected_funcs:
|
82
|
+
newchoices.append(p_text)
|
83
|
+
collected_funcs.add(p.name)
|
84
|
+
else:
|
85
|
+
later.append(p_text)
|
86
|
+
if len(later) == len(todo):
|
87
|
+
# raise Exception(f"No {p.run_after} in {collected_funcs}")
|
88
|
+
newchoices += todo
|
89
|
+
break
|
90
|
+
todo = later
|
91
|
+
cls.choices = newchoices
|
92
|
+
return rv
|
40
93
|
|
41
94
|
@classmethod
|
42
95
|
def task_classes(cls):
|
@@ -2,7 +2,7 @@
|
|
2
2
|
# Copyright 2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
from asgiref.sync import async_to_sync
|
5
|
-
from lino.api import rt
|
5
|
+
from lino.api import dd, rt
|
6
6
|
from lino.modlib.linod.mixins import start_task_runner
|
7
7
|
|
8
8
|
# import logging
|
@@ -12,19 +12,9 @@ from lino.modlib.linod.mixins import start_task_runner
|
|
12
12
|
# if l.name == name:
|
13
13
|
# return l
|
14
14
|
|
15
|
-
def objects():
|
16
|
-
raise Exception("""
|
17
|
-
|
18
|
-
This fixture isn't used at the moment. I wrote it because I thought it
|
19
|
-
would be nice to run the system task runner automatically when ``pm prep``
|
20
|
-
in order to cover the sync_ibanity system task. But (1) this would require
|
21
|
-
me to integrate also the ``checkdata`` and ``checksummaries`` fixtures into
|
22
|
-
it (otherwise they would run again as a system task) and (2) we don't want
|
23
|
-
to start `sync_ibanity` automatically on GitLab because it can't work
|
24
|
-
without credentials.
|
25
15
|
|
26
|
-
|
27
|
-
ar = rt.login(
|
16
|
+
def objects():
|
17
|
+
ar = rt.login(dd.plugins.users.demo_username)
|
28
18
|
# logger.setLevel(logging.DEBUG)
|
29
19
|
# getHandlerByName('console').setLevel(logging.DEBUG)
|
30
20
|
# ar.debug("Coucou")
|
@@ -2,7 +2,6 @@
|
|
2
2
|
# Copyright 2022-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
|
-
# import time
|
6
5
|
import asyncio
|
7
6
|
from django.conf import settings
|
8
7
|
from django.core.management import BaseCommand, call_command
|
@@ -17,18 +16,6 @@ if dd.plugins.linod.use_channels:
|
|
17
16
|
|
18
17
|
|
19
18
|
class Command(BaseCommand):
|
20
|
-
def add_arguments(self, parser):
|
21
|
-
parser.add_argument(
|
22
|
-
"--force",
|
23
|
-
help="Force starts the runworker process even if a log_socket_file exists."
|
24
|
-
" Use only in production server.",
|
25
|
-
action="store_true",
|
26
|
-
default=False,
|
27
|
-
)
|
28
|
-
# parser.add_argument("--skip-system-tasks",
|
29
|
-
# help="Skips the system tasks coroutine",
|
30
|
-
# action="store_true",
|
31
|
-
# default=False)
|
32
19
|
|
33
20
|
def handle(self, *args, **options):
|
34
21
|
|
lino/modlib/linod/mixins.py
CHANGED
@@ -217,6 +217,11 @@ async def start_task_runner(ar=None, max_count=None):
|
|
217
217
|
# ar.info("Start task runner using %s...", ar.logger)
|
218
218
|
# await sync_to_async(tasks.setup)()
|
219
219
|
# print("20240109", ar.logger.handlers)
|
220
|
+
|
221
|
+
# Procedures.sort_for_runner()
|
222
|
+
for proc in Procedures.get_list_items():
|
223
|
+
await proc.install_if_needed(ar)
|
224
|
+
|
220
225
|
count = 0
|
221
226
|
while True:
|
222
227
|
await ar.adebug("Start next task runner loop.")
|
@@ -241,6 +246,9 @@ async def start_task_runner(ar=None, max_count=None):
|
|
241
246
|
for self in tasks:
|
242
247
|
# print("20240424d")
|
243
248
|
# raise Warning("20231230")
|
249
|
+
if max_count is not None and not self.procedure.during_initdb:
|
250
|
+
# print(f"20250621 skip {self} during initdb")
|
251
|
+
continue
|
244
252
|
if self.last_end_time is None and self.last_start_time is not None:
|
245
253
|
run_duration = now - self.last_start_time
|
246
254
|
if run_duration > timedelta(hours=2):
|
lino/modlib/linod/models.py
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2023-
|
2
|
+
# Copyright 2023-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
# See https://dev.lino-framework.org/plugins/linod.html
|
5
5
|
|
6
|
-
from lino.api import dd,
|
6
|
+
from lino.api import dd, _
|
7
7
|
from lino.core.roles import SiteStaff
|
8
8
|
from lino import logger
|
9
|
-
from lino.
|
10
|
-
from lino.modlib.checkdata.choicelists import Checker
|
9
|
+
# from lino.modlib.checkdata.choicelists import Checker
|
11
10
|
from .choicelists import Procedures, LogLevels
|
12
11
|
|
13
12
|
# if dd.plugins.linod.use_channels:
|
@@ -47,29 +46,29 @@ class SystemTasks(dd.Table):
|
|
47
46
|
"""
|
48
47
|
|
49
48
|
|
50
|
-
class SystemTaskChecker(Checker):
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
SystemTaskChecker.activate()
|
49
|
+
# class SystemTaskChecker(Checker):
|
50
|
+
# """
|
51
|
+
# Checks for procedures that do not yet have a background task.
|
52
|
+
# """
|
53
|
+
#
|
54
|
+
# verbose_name = _("Check for missing system tasks")
|
55
|
+
# model = None
|
56
|
+
#
|
57
|
+
# def get_checkdata_problems(self, ar, obj, fix=False):
|
58
|
+
# for proc in Procedures.get_list_items():
|
59
|
+
# if proc.class_name == "linod.SystemTask":
|
60
|
+
# if SystemTask.objects.filter(procedure=proc).count() == 0:
|
61
|
+
# msg = _("No {} for {}").format(
|
62
|
+
# SystemTask._meta.verbose_name, proc)
|
63
|
+
# yield (True, msg)
|
64
|
+
# if fix:
|
65
|
+
# logger.debug("Create background task for %r", proc)
|
66
|
+
# jr = SystemTask(procedure=proc, **proc.kwargs)
|
67
|
+
# # every_unit=proc.every_unit, every=proc.every_value)
|
68
|
+
# if jr.every_unit == "secondly":
|
69
|
+
# jr.log_level = "WARNING"
|
70
|
+
# jr.full_clean()
|
71
|
+
# jr.save()
|
72
|
+
#
|
73
|
+
#
|
74
|
+
# SystemTaskChecker.activate()
|
lino/modlib/memo/__init__.py
CHANGED
@@ -68,13 +68,13 @@ class Plugin(ad.Plugin):
|
|
68
68
|
def on_plugins_loaded(self, site):
|
69
69
|
self.parser = Parser()
|
70
70
|
|
71
|
-
def url2html(ar, s, cmdname, mentions, context):
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
self.parser.register_command("url", url2html)
|
71
|
+
# def url2html(ar, s, cmdname, mentions, context):
|
72
|
+
# url, text = split_name_rest(s)
|
73
|
+
# if text is None:
|
74
|
+
# text = url
|
75
|
+
# return '<a href="%s" target="_blank">%s</a>' % (url, text)
|
76
|
+
#
|
77
|
+
# self.parser.register_command("url", url2html)
|
78
78
|
|
79
79
|
# 20240920 I disabled the "py" memo command because I don't know anybody
|
80
80
|
# who used it (except myself a few times for testing it) and because it
|
File without changes
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
2
|
+
# Copyright 2025 Rumma & Ko Ltd
|
3
|
+
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
|
+
"""
|
5
|
+
Convert [url] memo commands in the text fields of this database into <a href>
|
6
|
+
tags.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from click import confirm
|
10
|
+
from django.core.management.base import BaseCommand
|
11
|
+
from django.conf import settings
|
12
|
+
from lino.core.utils import full_model_name as fmn
|
13
|
+
from lino.modlib.memo.parser import Parser, split_name_rest
|
14
|
+
from lino.api import dd, rt
|
15
|
+
|
16
|
+
parser = Parser()
|
17
|
+
|
18
|
+
|
19
|
+
def url2html(ar, s, cmdname, mentions, context):
|
20
|
+
url, text = split_name_rest(s)
|
21
|
+
if text is None:
|
22
|
+
text = url
|
23
|
+
return '<a href="%s" target="_blank">%s</a>' % (url, text)
|
24
|
+
|
25
|
+
|
26
|
+
parser.register_command("url", url2html)
|
27
|
+
|
28
|
+
|
29
|
+
class Command(BaseCommand):
|
30
|
+
help = __doc__
|
31
|
+
|
32
|
+
def add_arguments(self, parser):
|
33
|
+
super().add_arguments(parser)
|
34
|
+
(
|
35
|
+
parser.add_argument(
|
36
|
+
"-b", "--batch", "--noinput",
|
37
|
+
action="store_false",
|
38
|
+
dest="interactive",
|
39
|
+
default=True,
|
40
|
+
help="Do not prompt for input of any kind.",
|
41
|
+
),
|
42
|
+
)
|
43
|
+
|
44
|
+
def handle(self, *args, **options):
|
45
|
+
|
46
|
+
batch = not options.get("interactive")
|
47
|
+
|
48
|
+
settings.SITE.startup()
|
49
|
+
|
50
|
+
for m in rt.models_by_base(dd.Model):
|
51
|
+
if len(m._bleached_fields) == 0:
|
52
|
+
continue
|
53
|
+
print(f"Search for [url] memo commands in {fmn(m)}...")
|
54
|
+
for obj in m.objects.all():
|
55
|
+
for f in m._bleached_fields:
|
56
|
+
if getattr(f, "format") != "plain":
|
57
|
+
old = getattr(obj, f.name)
|
58
|
+
if old is None:
|
59
|
+
continue
|
60
|
+
new = parser.parse(old)
|
61
|
+
if new != old:
|
62
|
+
print(f"- {obj} {f} :")
|
63
|
+
print(f"- {old} -> {new}")
|
64
|
+
if batch or confirm("Update this ?", default=True):
|
65
|
+
setattr(obj, f.name, new)
|
66
|
+
obj.full_clean()
|
67
|
+
obj.save()
|
lino/modlib/memo/mixins.py
CHANGED
@@ -12,7 +12,6 @@ except ImportError:
|
|
12
12
|
|
13
13
|
from django.conf import settings
|
14
14
|
from django.utils import translation
|
15
|
-
from django.utils.text import Truncator
|
16
15
|
from django.utils.html import format_html
|
17
16
|
|
18
17
|
from lino.core.gfks import gfk2lookup
|
@@ -25,13 +24,6 @@ from lino.modlib.checkdata.choicelists import Checker
|
|
25
24
|
from lino.api import rt, dd, _
|
26
25
|
|
27
26
|
|
28
|
-
def django_truncate_comment(html_str):
|
29
|
-
# works, but we don't use it because (...)
|
30
|
-
return Truncator(html_str).chars(
|
31
|
-
settings.SITE.plugins.memo.short_preview_length, html=True
|
32
|
-
)
|
33
|
-
|
34
|
-
|
35
27
|
MARKDOWNCFG = dict(
|
36
28
|
extensions=["toc"], extension_configs=dict(toc=dict(toc_depth=3, permalink=True))
|
37
29
|
)
|
@@ -178,7 +170,7 @@ class BasePreviewable(dd.Model):
|
|
178
170
|
)
|
179
171
|
short = truncate_comment(full, self.get_preview_length())
|
180
172
|
if not full.startswith("<"):
|
181
|
-
if
|
173
|
+
if settings.SITE.plugins.memo.use_markup:
|
182
174
|
full = markdown.markdown(full, **MARKDOWNCFG)
|
183
175
|
return (short, full)
|
184
176
|
|
lino/modlib/memo/parser.py
CHANGED
@@ -102,7 +102,7 @@ class Suggester:
|
|
102
102
|
return (
|
103
103
|
r"javascript:window.App.runAction({"
|
104
104
|
+ f"'actorId': '{self.formatter(da.actor)}', "
|
105
|
-
+ "'
|
105
|
+
+ f"'action_full_name': '{da.action.full_name()}', "
|
106
106
|
+ "'rp': null, "
|
107
107
|
+ r"'status': {"
|
108
108
|
+ f"'record_id': {self.formatter(obj.pk)}"
|
@@ -1,7 +1,9 @@
|
|
1
1
|
<html><head><base href="{{settings.SITE.server_url}}" target="_blank"></head><body>
|
2
2
|
{% if len(messages) == 1 %}
|
3
3
|
{% set msg = messages[0] %}
|
4
|
-
|
4
|
+
|
5
|
+
{% set created = user.dt_astimezone(msg.created) %}
|
6
|
+
({{ created.strftime("%d/%m/%Y %H:%M %Z") }})
|
5
7
|
{#{ar.parse_memo(msg.body)}#}
|
6
8
|
{{msg.body}}
|
7
9
|
{% else %}
|
@@ -9,7 +11,8 @@
|
|
9
11
|
{{_("You have {} unseen notifications").format(len(messages))}}
|
10
12
|
{% for msg in messages %}
|
11
13
|
<div>
|
12
|
-
|
14
|
+
{% set created = user.dt_astimezone(msg.created) %}
|
15
|
+
<H3>{{ created.strftime("%d/%m/%Y %H:%M %Z") }}</H3>
|
13
16
|
{#{ar.parse_memo(msg.body)}#}
|
14
17
|
{{msg.body}}
|
15
18
|
</div>
|
@@ -25,13 +25,12 @@ def objects():
|
|
25
25
|
mt = rt.models.notify.MessageTypes.system
|
26
26
|
|
27
27
|
rt.models.notify.Message.emit_broadcast_notification(
|
28
|
-
"The database has been initialized.", message_type=mt,
|
29
|
-
|
30
|
-
# return []
|
28
|
+
"The database has been initialized.", message_type=mt,
|
29
|
+
created=now, sent=now)
|
31
30
|
for u in rt.models.users.User.objects.exclude(first_name="").order_by('username'):
|
32
31
|
with translation.override(u.language):
|
33
|
-
|
32
|
+
rt.models.notify.Message.create_message(
|
34
33
|
u, subject=_("Welcome on board, {}.").format(u.first_name),
|
35
|
-
mail_mode=u.mail_mode, created=now, message_type=mt,
|
36
|
-
sent=now)
|
34
|
+
mail_mode=u.mail_mode, created=now, message_type=mt, sent=now)
|
37
35
|
# print("20240710", u, u.language)
|
36
|
+
return []
|
lino/modlib/notify/models.py
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
# import json
|
6
6
|
from io import StringIO
|
7
7
|
from lxml import etree
|
8
|
-
from datetime import timedelta
|
8
|
+
from datetime import timedelta, UTC
|
9
9
|
|
10
10
|
from django.db import models
|
11
11
|
from django.conf import settings
|
@@ -156,14 +156,13 @@ class Message(UserAuthored, Controllable, Created):
|
|
156
156
|
body=msg,
|
157
157
|
subject="Broadcast message",
|
158
158
|
message_type=message_type,
|
159
|
-
**msgdata
|
160
|
-
)
|
159
|
+
**msgdata)
|
161
160
|
|
162
161
|
@classmethod
|
163
162
|
def emit_notification(cls, ar, owner, message_type, msg_func, recipients, **kwargs):
|
164
163
|
# kwargs can be used to set `reply_to`
|
165
164
|
# ATM the action_url is computed using the permissions of the sending
|
166
|
-
# user. That
|
165
|
+
# user. That might not be exactly what we want (the url should depend on
|
167
166
|
# the recipient). But until now we cannot imagine any case where this
|
168
167
|
# would become important.
|
169
168
|
push_options = {}
|
@@ -176,8 +175,7 @@ class Message(UserAuthored, Controllable, Created):
|
|
176
175
|
if ba is not None:
|
177
176
|
push_options.update(
|
178
177
|
action_url=ar.renderer.get_detail_url(
|
179
|
-
ar, ba.actor, owner.pk)
|
180
|
-
)
|
178
|
+
ar, ba.actor, owner.pk))
|
181
179
|
# push_options.update(action_url=ar.renderer.obj2url(ar, owner))
|
182
180
|
me = ar.get_user()
|
183
181
|
others = set()
|
@@ -229,8 +227,7 @@ class Message(UserAuthored, Controllable, Created):
|
|
229
227
|
subject=settings.SITE.title,
|
230
228
|
# body=settings.SITE.plugins.memo.parser.parse(obj.body),
|
231
229
|
body=html2text(obj.body),
|
232
|
-
created=obj.created
|
233
|
-
)
|
230
|
+
created=obj.created)
|
234
231
|
send_notification(user, **push_options)
|
235
232
|
|
236
233
|
mark_all_seen = MarkAllSeen()
|
@@ -246,8 +243,10 @@ class Message(UserAuthored, Controllable, Created):
|
|
246
243
|
qs = qs.exclude(user__email="")
|
247
244
|
qs = qs.filter(mail_mode=mm).order_by("user")
|
248
245
|
if qs.count() == 0:
|
246
|
+
# print("20250621 No email notifications to send for %s.", mm)
|
249
247
|
ar.logger.debug("No email notifications to send for %s.", mm)
|
250
248
|
return
|
249
|
+
# print("20250621 Some email notifications to send for %s.", mm, qs)
|
251
250
|
mail_users = settings.SITE.user_model.get_active_users().exclude(email="")
|
252
251
|
# from lino.core.renderer import MailRenderer
|
253
252
|
# ar = rt.login(renderer=MailRenderer())
|
@@ -468,13 +467,13 @@ class MyMessages(My, Messages):
|
|
468
467
|
# h)
|
469
468
|
|
470
469
|
|
471
|
-
@schedule_often(every=10)
|
470
|
+
@schedule_often(every=10, during_initdb=False)
|
472
471
|
def send_pending_emails_often(ar):
|
473
472
|
# print("20231021 send_pending_emails_often()", ar.logger)
|
474
473
|
rt.models.notify.Message.send_summary_emails(ar, MailModes.often)
|
475
474
|
|
476
475
|
|
477
|
-
@schedule_daily()
|
476
|
+
@schedule_daily(during_initdb=False)
|
478
477
|
def send_pending_emails_daily(ar):
|
479
478
|
rt.models.notify.Message.send_summary_emails(ar, MailModes.daily)
|
480
479
|
|
lino/modlib/periods/__init__.py
CHANGED
@@ -16,16 +16,19 @@ class Plugin(ad.Plugin):
|
|
16
16
|
fix_y2k = False
|
17
17
|
short_ref = False
|
18
18
|
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
def pre_site_startup(self, site):
|
20
|
+
if isinstance(self.period_type, str):
|
21
|
+
self.period_type = site.models.periods.PeriodTypes.get_by_name(
|
22
|
+
self.period_type)
|
23
|
+
super().pre_site_startup(site)
|
24
24
|
|
25
25
|
def before_analyze(self):
|
26
26
|
if self.fix_y2k and self.start_month != 1:
|
27
27
|
raise Exception("When fix_y2k is set, start_month must be 1")
|
28
|
-
if isinstance(self.period_type, str):
|
29
|
-
self.period_type = self.site.models.periods.PeriodTypes.get_by_name(
|
30
|
-
self.period_type)
|
31
28
|
super().before_analyze()
|
29
|
+
|
30
|
+
def setup_config_menu(self, site, user_type, m, ar=None):
|
31
|
+
p = self.get_menu_group()
|
32
|
+
m = m.add_menu(p.app_label, p.verbose_name)
|
33
|
+
m.add_action("periods.StoredYears")
|
34
|
+
m.add_action("periods.StoredPeriods")
|