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.
Files changed (93) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +1 -0
  3. lino/api/doctest.py +21 -0
  4. lino/core/actions.py +80 -25
  5. lino/core/actors.py +54 -27
  6. lino/core/boundaction.py +16 -0
  7. lino/core/choicelists.py +7 -7
  8. lino/core/constants.py +3 -0
  9. lino/core/dashboard.py +4 -2
  10. lino/core/dbtables.py +2 -2
  11. lino/core/elems.py +38 -13
  12. lino/core/fields.py +20 -11
  13. lino/core/kernel.py +8 -0
  14. lino/core/layouts.py +6 -2
  15. lino/core/menus.py +3 -6
  16. lino/core/model.py +5 -4
  17. lino/core/renderer.py +20 -9
  18. lino/core/requests.py +8 -7
  19. lino/core/signals.py +1 -0
  20. lino/core/site.py +48 -28
  21. lino/core/store.py +4 -2
  22. lino/core/tables.py +23 -10
  23. lino/core/utils.py +4 -1
  24. lino/core/workflows.py +2 -1
  25. lino/help_texts.py +1 -2
  26. lino/management/commands/prep.py +2 -2
  27. lino/management/commands/show.py +8 -10
  28. lino/mixins/__init__.py +14 -13
  29. lino/mixins/periods.py +2 -0
  30. lino/mixins/sequenced.py +1 -1
  31. lino/modlib/about/models.py +4 -3
  32. lino/modlib/checkdata/__init__.py +42 -36
  33. lino/modlib/checkdata/choicelists.py +9 -1
  34. lino/modlib/checkdata/fixtures/checkdata.py +4 -2
  35. lino/modlib/checkdata/management/commands/checkdata.py +3 -3
  36. lino/modlib/checkdata/models.py +9 -2
  37. lino/modlib/comments/models.py +4 -3
  38. lino/modlib/extjs/ext_renderer.py +4 -4
  39. lino/modlib/extjs/views.py +8 -2
  40. lino/modlib/gfks/fields.py +1 -1
  41. lino/modlib/help/__init__.py +3 -3
  42. lino/modlib/help/config/makehelp/conf.tpl.py +2 -2
  43. lino/modlib/help/fixtures/demo2.py +6 -1
  44. lino/modlib/help/management/commands/makehelp.py +4 -1
  45. lino/modlib/help/models.py +4 -1
  46. lino/modlib/help/utils.py +12 -6
  47. lino/modlib/linod/choicelists.py +57 -4
  48. lino/modlib/linod/fixtures/{linod.py → checkdata.py} +3 -13
  49. lino/modlib/linod/management/commands/linod.py +0 -13
  50. lino/modlib/linod/mixins.py +8 -0
  51. lino/modlib/linod/models.py +29 -30
  52. lino/modlib/memo/__init__.py +7 -7
  53. lino/modlib/memo/management/__init__,py +0 -0
  54. lino/modlib/memo/management/commands/__init__.py +0 -0
  55. lino/modlib/memo/management/commands/removeurls.py +67 -0
  56. lino/modlib/memo/mixins.py +1 -9
  57. lino/modlib/memo/parser.py +1 -1
  58. lino/modlib/notify/config/notify/summary.eml +5 -2
  59. lino/modlib/notify/fixtures/demo2.py +5 -6
  60. lino/modlib/notify/models.py +9 -10
  61. lino/modlib/periods/__init__.py +11 -8
  62. lino/modlib/periods/choicelists.py +16 -10
  63. lino/modlib/periods/models.py +45 -45
  64. lino/modlib/publisher/renderer.py +2 -5
  65. lino/modlib/summaries/fixtures/checksummaries.py +4 -2
  66. lino/modlib/system/models.py +17 -18
  67. lino/modlib/uploads/fixtures/demo.py +9 -3
  68. lino/modlib/uploads/mixins.py +5 -2
  69. lino/modlib/uploads/models.py +15 -9
  70. lino/modlib/uploads/utils.py +4 -1
  71. lino/modlib/users/__init__.py +59 -18
  72. lino/modlib/users/actions.py +24 -20
  73. lino/modlib/users/fixtures/demo_users.py +2 -35
  74. lino/modlib/users/mixins.py +3 -4
  75. lino/modlib/users/models.py +53 -13
  76. lino/modlib/users/ui.py +30 -16
  77. lino/modlib/users/utils.py +5 -6
  78. lino/projects/std/settings.py +1 -1
  79. lino/sphinxcontrib/logo/templates/footer.html +1 -0
  80. lino/utils/ajax.py +1 -1
  81. lino/utils/cycler.py +5 -0
  82. lino/utils/dbhash.py +4 -9
  83. lino/utils/dpy.py +2 -2
  84. lino/utils/format_date.py +4 -3
  85. lino/utils/html.py +13 -5
  86. lino/utils/jsgen.py +3 -2
  87. lino/utils/quantities.py +8 -0
  88. lino/utils/soup.py +75 -106
  89. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/METADATA +1 -1
  90. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/RECORD +93 -90
  91. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/WHEEL +0 -0
  92. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/AUTHORS.rst +0 -0
  93. {lino-25.6.1.dist-info → lino-25.7.1.dist-info}/licenses/COPYING +0 -0
@@ -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.action, a.model, ba.action.action_name
69
+ ba, a.model, ba.action.action_name
70
70
  )
71
- self.htl.install_help_text(ba.action, a, ba.action.action_name)
72
- self.htl.install_help_text(ba.action.__class__)
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.site_config.site_company %}
21
+ {% if settings.SITE.plugins.contacts.site_owner %}
22
22
  import datetime
23
- copyright = "{} {{settings.SITE.site_config.site_company}}".format(
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.site_config.site_company)
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):
@@ -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 == "update_missing_rates":
83
- # print("20181004 {} {} {}".format(cls, k, txt))
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
- try:
106
- fld.help_text = txt
107
- except AttributeError as e:
108
- raise AttributeError("20240329 {} {}".format(fld, e))
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))
@@ -1,11 +1,13 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2023-2024 Rumma & Ko Ltd
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 lino.api import dd, _
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__(self, func, class_name, **kwargs):
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("robin")
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
 
@@ -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):
@@ -1,13 +1,12 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2023-2024 Rumma & Ko Ltd
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, rt, _
6
+ from lino.api import dd, _
7
7
  from lino.core.roles import SiteStaff
8
8
  from lino import logger
9
- from lino.mixins import Sequenced
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
- Checks for procedures that do not yet have a background task.
53
- """
54
-
55
- verbose_name = _("Check for missing system tasks")
56
- model = None
57
-
58
- def get_checkdata_problems(self, ar, obj, fix=False):
59
- for proc in Procedures.get_list_items():
60
- if proc.class_name == "linod.SystemTask":
61
- if SystemTask.objects.filter(procedure=proc).count() == 0:
62
- msg = _("No {} for {}").format(
63
- SystemTask._meta.verbose_name, proc)
64
- yield (True, msg)
65
- if fix:
66
- logger.debug("Create background task for %r", proc)
67
- jr = SystemTask(procedure=proc, **proc.kwargs)
68
- # every_unit=proc.every_unit, every=proc.every_value)
69
- if jr.every_unit == "secondly":
70
- jr.log_level = "WARNING"
71
- jr.full_clean()
72
- jr.save()
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()
@@ -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
- 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)
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()
@@ -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 dd.get_plugin_setting("memo", "use_markup"):
173
+ if settings.SITE.plugins.memo.use_markup:
182
174
  full = markdown.markdown(full, **MARKDOWNCFG)
183
175
  return (short, full)
184
176
 
@@ -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
- + "'an': 'detail', "
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
- ({{fds(msg.created.date())}} {{msg.created.time().strftime('%H:%M')}})
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
- <H3>{{fds(msg.created.date())}} {{msg.created.time().strftime('%H:%M')}}</H3>
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, created=now
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
- yield rt.models.notify.Message.create_message(
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 []
@@ -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's maybe not exactly what we want (the url should depend on
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
 
@@ -16,16 +16,19 @@ class Plugin(ad.Plugin):
16
16
  fix_y2k = False
17
17
  short_ref = False
18
18
 
19
- def setup_config_menu(self, site, user_type, m, ar=None):
20
- p = self.get_menu_group()
21
- m = m.add_menu(p.app_label, p.verbose_name)
22
- m.add_action("periods.StoredYears")
23
- m.add_action("periods.StoredPeriods")
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")