lino 25.2.3__py3-none-any.whl → 25.3.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 (81) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +11 -48
  3. lino/api/doctest.py +34 -36
  4. lino/core/actions.py +25 -23
  5. lino/core/actors.py +37 -17
  6. lino/core/choicelists.py +10 -8
  7. lino/core/dbtables.py +1 -1
  8. lino/core/elems.py +46 -30
  9. lino/core/fields.py +19 -9
  10. lino/core/inject.py +7 -6
  11. lino/core/kernel.py +26 -66
  12. lino/core/model.py +44 -31
  13. lino/core/plugin.py +4 -4
  14. lino/core/requests.py +76 -55
  15. lino/core/site.py +84 -30
  16. lino/core/store.py +5 -2
  17. lino/core/utils.py +12 -7
  18. lino/help_texts.py +3 -8
  19. lino/management/commands/prep.py +1 -1
  20. lino/mixins/duplicable.py +6 -4
  21. lino/mixins/sequenced.py +17 -6
  22. lino/modlib/__init__.py +0 -2
  23. lino/modlib/changes/models.py +21 -10
  24. lino/modlib/checkdata/models.py +59 -24
  25. lino/modlib/comments/fixtures/demo2.py +12 -3
  26. lino/modlib/comments/models.py +7 -7
  27. lino/modlib/comments/ui.py +8 -5
  28. lino/modlib/export_excel/models.py +7 -5
  29. lino/modlib/extjs/views.py +39 -20
  30. lino/modlib/help/management/commands/makehelp.py +5 -2
  31. lino/modlib/jinja/mixins.py +25 -14
  32. lino/modlib/linod/__init__.py +1 -0
  33. lino/modlib/linod/choicelists.py +21 -0
  34. lino/modlib/linod/consumers.py +13 -4
  35. lino/modlib/linod/management/commands/linod.py +6 -2
  36. lino/modlib/linod/mixins.py +16 -11
  37. lino/modlib/linod/models.py +4 -2
  38. lino/modlib/notify/models.py +18 -10
  39. lino/modlib/printing/actions.py +41 -30
  40. lino/modlib/printing/choicelists.py +11 -9
  41. lino/modlib/printing/mixins.py +25 -20
  42. lino/modlib/publisher/models.py +5 -5
  43. lino/modlib/summaries/models.py +3 -2
  44. lino/modlib/system/models.py +28 -29
  45. lino/modlib/uploads/__init__.py +5 -5
  46. lino/modlib/uploads/actions.py +2 -8
  47. lino/modlib/uploads/choicelists.py +10 -10
  48. lino/modlib/uploads/fixtures/std.py +17 -0
  49. lino/modlib/uploads/mixins.py +20 -8
  50. lino/modlib/uploads/models.py +60 -35
  51. lino/modlib/uploads/ui.py +10 -7
  52. lino/utils/media.py +45 -23
  53. lino/utils/report.py +5 -4
  54. lino/utils/soup.py +22 -4
  55. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/METADATA +1 -1
  56. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/RECORD +59 -80
  57. lino/mixins/uploadable.py +0 -3
  58. lino/sandbox/bcss/PerformInvestigation.py +0 -2260
  59. lino/sandbox/bcss/SSDNReply.py +0 -3924
  60. lino/sandbox/bcss/SSDNRequest.py +0 -3723
  61. lino/sandbox/bcss/__init__.py +0 -0
  62. lino/sandbox/bcss/readme.txt +0 -1
  63. lino/sandbox/bcss/test.py +0 -92
  64. lino/sandbox/bcss/test2.py +0 -128
  65. lino/sandbox/bcss/test3.py +0 -161
  66. lino/sandbox/bcss/test4.py +0 -167
  67. lino/sandbox/contacts/__init__.py +0 -0
  68. lino/sandbox/contacts/fixtures/__init__.py +0 -0
  69. lino/sandbox/contacts/fixtures/demo.py +0 -365
  70. lino/sandbox/contacts/manage.py +0 -10
  71. lino/sandbox/contacts/models.py +0 -395
  72. lino/sandbox/contacts/settings.py +0 -67
  73. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.wsdl +0 -65
  74. lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.xsd +0 -286
  75. lino/sandbox/tx25/XSD/rn25_Release201104.xsd +0 -2855
  76. lino/sandbox/tx25/xsd2py1.py +0 -68
  77. lino/sandbox/tx25/xsd2py2.py +0 -62
  78. lino/sandbox/tx25/xsd2py3.py +0 -56
  79. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/WHEEL +0 -0
  80. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/AUTHORS.rst +0 -0
  81. {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/COPYING +0 -0
@@ -20,6 +20,7 @@ class Plugin(ad.Plugin):
20
20
  verbose_name = _("Lino daemon")
21
21
  use_channels = False
22
22
  background_sleep_time = 5 # in seconds
23
+ daemon_user = "robin" # TODO: find a better solution
23
24
 
24
25
  def on_plugins_loaded(self, site):
25
26
  assert self.site is site
@@ -80,3 +80,24 @@ add("INFO")
80
80
  add("WARNING")
81
81
  add("ERROR")
82
82
  add("CRITICAL")
83
+
84
+
85
+ def background_task(**kwargs):
86
+ if "class_name" not in kwargs:
87
+ kwargs["class_name"] = "linod.SystemTask"
88
+
89
+ def decorator(func):
90
+ Procedures.add_item(func, **kwargs)
91
+ return func
92
+
93
+ return decorator
94
+
95
+
96
+ def schedule_often(every=10, **kwargs):
97
+ kwargs.update(every_unit="secondly", every=every)
98
+ return background_task(**kwargs)
99
+
100
+
101
+ def schedule_daily(**kwargs):
102
+ kwargs.update(every_unit="daily", every=1)
103
+ return background_task(**kwargs)
@@ -36,7 +36,10 @@ from .utils import BROADCAST_CHANNEL
36
36
  # logger.addHandler(logging.StreamHandler())
37
37
  # logger.setLevel(logging.INFO)
38
38
 
39
- is_slave = lambda actor: actor.master is not None
39
+
40
+ def is_slave(actor): return actor.master is not None
41
+
42
+
40
43
  match_master = (
41
44
  lambda ContentType, MasterModel, Master: Master == MasterModel
42
45
  or ContentType == Master
@@ -57,8 +60,12 @@ class LinodConsumer(AsyncConsumer):
57
60
  async def run_background_tasks(self, event: dict):
58
61
  # 'run.background.tasks' in `pm linod`
59
62
  from lino.modlib.linod.mixins import start_task_runner
63
+ # from lino.core.utils import login
64
+ # ar = login(settings.SITE.plugins.linod.daemon_user)
60
65
  from lino.core.requests import BaseRequest
61
- ar = BaseRequest()
66
+ u = await settings.SITE.user_model.objects.aget(
67
+ username=settings.SITE.plugins.linod.daemon_user)
68
+ ar = BaseRequest(user=u)
62
69
  asyncio.ensure_future(start_task_runner(ar))
63
70
 
64
71
  async def send_push(self, event):
@@ -82,7 +89,8 @@ class LinodConsumer(AsyncConsumer):
82
89
  if user is None:
83
90
  subs = settings.SITE.models.notify.Subscription.objects.all()
84
91
  else:
85
- subs = settings.SITE.models.notify.Subscription.objects.filter(user=user)
92
+ subs = settings.SITE.models.notify.Subscription.objects.filter(
93
+ user=user)
86
94
  async for sub in subs.aiterator():
87
95
  sub_info = {
88
96
  "endpoint": sub.endpoint,
@@ -122,7 +130,8 @@ class LinodConsumer(AsyncConsumer):
122
130
  data.update(
123
131
  **dict(
124
132
  [
125
- (a.actor_id, {"mk": msg["mk"], "mt": mt, "pk": msg["pk"]})
133
+ (a.actor_id, {"mk": msg["mk"],
134
+ "mt": mt, "pk": msg["pk"]})
126
135
  for a in ups
127
136
  if is_slave(a)
128
137
  and match_master(ContentType, MasterModel, a.master)
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2022-2024 Rumma & Ko Ltd
2
+ # Copyright 2022-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  # import time
@@ -10,6 +10,7 @@ from django.conf import settings
10
10
  from django.core.management import BaseCommand, call_command
11
11
  from lino.api import dd, rt
12
12
  from lino.modlib.linod.mixins import start_log_server, start_task_runner
13
+ from lino.core.requests import BaseRequest
13
14
 
14
15
  if dd.plugins.linod.use_channels:
15
16
  import threading
@@ -50,7 +51,10 @@ class Command(BaseCommand):
50
51
  # print("20240424 Run Lino daemon without channels")
51
52
 
52
53
  async def main():
53
- ar = rt.login()
54
+ u = await settings.SITE.user_model.objects.aget(
55
+ username=settings.SITE.plugins.linod.daemon_user)
56
+ ar = BaseRequest(user=u)
57
+ # ar = rt.login(dd.plugins.linod.daemon_user)
54
58
  await asyncio.gather(start_log_server(), start_task_runner(ar))
55
59
  # t1 = asyncio.create_task(settings.SITE.start_log_server())
56
60
  # t2 = asyncio.create_task(start_task_runner(ar))
@@ -4,20 +4,18 @@
4
4
  # See https://dev.lino-framework.org/plugins/linod.html
5
5
 
6
6
  import logging
7
- import sys
8
7
  import traceback
9
8
  import asyncio
10
9
  import pickle
11
10
  from datetime import timedelta
12
- from io import StringIO
13
11
  from django.conf import settings
14
12
  from django.db import models
15
13
  from django.utils import timezone
16
- from django.core.exceptions import ValidationError
14
+ # from django.core.exceptions import ValidationError
17
15
  from asgiref.sync import sync_to_async, async_to_sync
18
16
 
19
17
  from lino import logger
20
- from lino.api import dd, rt, _
18
+ from lino.api import dd, _
21
19
  from lino.mixins import Sequenced
22
20
  from lino.modlib.system.mixins import RecurrenceSet
23
21
  from lino.modlib.system.choicelists import Recurrences
@@ -34,8 +32,9 @@ def astr(self):
34
32
 
35
33
  class RunNow(dd.Action):
36
34
  label = _("Run now")
35
+ help_text = _("Mark the task as to be executed asap by linod.")
37
36
  select_rows = True
38
-
37
+ button_text = "▶"
39
38
  # icon_name = 'bell'
40
39
  # icon_name = 'lightning'
41
40
 
@@ -44,7 +43,6 @@ class RunNow(dd.Action):
44
43
  for obj in ar.selected_rows:
45
44
  assert issubclass(obj.__class__, Runnable)
46
45
  if True: # dd.plugins.linod.use_channels:
47
- # Mark the task as to be executed asap by linod.
48
46
  obj.last_start_time = None
49
47
  obj.last_end_time = None
50
48
  obj.message = "{} requested to run this task at {}.".format(
@@ -65,9 +63,11 @@ class Runnable(Sequenced, RecurrenceSet):
65
63
 
66
64
  log_level = LogLevels.field(default="INFO")
67
65
  disabled = dd.BooleanField(_("Disabled"), default=False)
68
- last_start_time = dd.DateTimeField(_("Started at"), null=True, editable=False)
66
+ last_start_time = dd.DateTimeField(
67
+ _("Started at"), null=True, editable=False)
69
68
  last_end_time = dd.DateTimeField(_("Ended at"), null=True, editable=False)
70
- message = dd.RichTextField(_("Logged messages"), format="plain", editable=False)
69
+ message = dd.RichTextField(
70
+ _("Logged messages"), format="plain", editable=False)
71
71
 
72
72
  # procedure = Procedures.field(strict=False, unique=True, editable=False)
73
73
  procedure = Procedures.field(strict=False)
@@ -76,7 +76,8 @@ class Runnable(Sequenced, RecurrenceSet):
76
76
  run_now = RunNow()
77
77
 
78
78
  def __str__(self):
79
- r = "{} #{} ({})".format(self._meta.verbose_name, self.seqno, self.name)
79
+ r = "{} #{} ({})".format(
80
+ self._meta.verbose_name, self.seqno, self.name)
80
81
  return r
81
82
 
82
83
  def full_clean(self, *args, **kwargs):
@@ -131,6 +132,9 @@ class Runnable(Sequenced, RecurrenceSet):
131
132
  await ar.adebug("Successfully terminated %s", self)
132
133
  # ar.info("Successfully terminated %s", astr(self))
133
134
  self.message = out.getvalue()
135
+ except Warning as e:
136
+ await ar.adebug("Terminated %s with warning %s", self, str(e))
137
+ self.message = out.getvalue()
134
138
  except Exception as e:
135
139
  self.message = out.getvalue()
136
140
  self.message += "\n" + "".join(traceback.format_exception(e))
@@ -159,7 +163,7 @@ class Runnable(Sequenced, RecurrenceSet):
159
163
  return _("Scheduled to run at {}").format(dd.ftl(next_time))
160
164
 
161
165
 
162
- async def start_task_runner(ar, max_count=None):
166
+ async def start_task_runner(ar=None, max_count=None):
163
167
  # called from consumers.LinoConsumer.run_background_tasks()
164
168
  await ar.ainfo("Start task runner using %s...", ar.logger)
165
169
  # ar.info("Start task runner using %s...", ar.logger)
@@ -170,7 +174,8 @@ async def start_task_runner(ar, max_count=None):
170
174
  await ar.adebug("Start next task runner loop.")
171
175
 
172
176
  now = timezone.now()
173
- next_time = now + timedelta(seconds=dd.plugins.linod.background_sleep_time)
177
+ next_time = now + \
178
+ timedelta(seconds=dd.plugins.linod.background_sleep_time)
174
179
 
175
180
  for cls in Procedures.task_classes():
176
181
  # asyncio.ensure_future(m.start_task_runner(ar.spawn_request()))
@@ -30,11 +30,12 @@ class SystemTask(Runnable):
30
30
  class SystemTasks(dd.Table):
31
31
  # label = _("System tasks")
32
32
  model = "linod.SystemTask"
33
+ order_by = ['seqno']
33
34
  required_roles = dd.login_required(SiteStaff)
34
35
  column_names = "seqno name log_level disabled status procedure *"
35
36
  detail_layout = """
36
37
  seqno procedure
37
- name
38
+ name
38
39
  every every_unit
39
40
  log_level disabled status
40
41
  last_start_time last_end_time
@@ -58,7 +59,8 @@ class SystemTaskChecker(Checker):
58
59
  for proc in Procedures.get_list_items():
59
60
  if proc.class_name == "linod.SystemTask":
60
61
  if SystemTask.objects.filter(procedure=proc).count() == 0:
61
- msg = _("No {} for {}").format(SystemTask._meta.verbose_name, proc)
62
+ msg = _("No {} for {}").format(
63
+ SystemTask._meta.verbose_name, proc)
62
64
  yield (True, msg)
63
65
  if fix:
64
66
  logger.debug("Create background task for %r", proc)
@@ -22,6 +22,7 @@ from lino.utils.format_date import fds
22
22
  from lino.mixins import Created, ObservedDateRange
23
23
  from lino.modlib.gfks.mixins import Controllable
24
24
  from lino.modlib.users.mixins import UserAuthored, My
25
+ from lino.modlib.linod.choicelists import schedule_daily, schedule_often
25
26
  from lino.modlib.office.roles import OfficeUser
26
27
 
27
28
  from .choicelists import MessageTypes, MailModes
@@ -29,7 +30,9 @@ from .api import send_notification, NOTIFICATION, send_panel_update
29
30
 
30
31
  html_parser = etree.HTMLParser()
31
32
 
32
- subaddress_separator = dd.get_plugin_setting('inbox', 'subaddress_separator', None)
33
+ subaddress_separator = dd.get_plugin_setting(
34
+ 'inbox', 'subaddress_separator', None)
35
+
33
36
 
34
37
  def groupname(s):
35
38
  # Remove any invalid characters from the given string so that it can
@@ -126,7 +129,8 @@ class Message(UserAuthored, Controllable, Created):
126
129
  message_type = MessageTypes.field(default="change")
127
130
  seen = models.DateTimeField(_("seen"), null=True, editable=False)
128
131
  sent = models.DateTimeField(_("sent"), null=True, editable=False)
129
- body = dd.RichTextField(_("Body"), editable=False, format="html", default="")
132
+ body = dd.RichTextField(_("Body"), editable=False,
133
+ format="html", default="")
130
134
  mail_mode = MailModes.field(default="often")
131
135
  subject = models.CharField(_("Subject"), max_length=250, editable=False)
132
136
  reply_to = dd.ForeignKey(
@@ -171,7 +175,8 @@ class Message(UserAuthored, Controllable, Created):
171
175
  ba = owner.get_detail_action(ar)
172
176
  if ba is not None:
173
177
  push_options.update(
174
- action_url=ar.renderer.get_detail_url(ar, ba.actor, owner.pk)
178
+ action_url=ar.renderer.get_detail_url(
179
+ ar, ba.actor, owner.pk)
175
180
  )
176
181
  # push_options.update(action_url=ar.renderer.obj2url(ar, owner))
177
182
  me = ar.get_user()
@@ -267,7 +272,8 @@ class Message(UserAuthored, Controllable, Created):
267
272
  collect(u, obj)
268
273
  else:
269
274
  collect(obj.user, obj)
270
- ar.logger.debug("Send out '%s' summaries for %d users.", mm, len(users))
275
+ ar.logger.debug(
276
+ "Send out '%s' summaries for %d users.", mm, len(users))
271
277
  for user, messages in users.items():
272
278
  with translation.override(user.language):
273
279
  sender = sender_parts[0]
@@ -276,9 +282,11 @@ class Message(UserAuthored, Controllable, Created):
276
282
  subject = msg.subject
277
283
  if subaddress_separator is not None:
278
284
  if msg.reply_to_id:
279
- sender += subaddress_separator + str(msg.reply_to_id)
285
+ sender += subaddress_separator + \
286
+ str(msg.reply_to_id)
280
287
  elif msg.owner_id:
281
- sender += subaddress_separator + str(msg.owner_type)
288
+ sender += subaddress_separator + \
289
+ str(msg.owner_type)
282
290
  sender += subaddress_separator + str(msg.owner_id)
283
291
  else:
284
292
  subject = _("{} notifications").format(len(messages))
@@ -460,18 +468,18 @@ class MyMessages(My, Messages):
460
468
  # h)
461
469
 
462
470
 
463
- @dd.schedule_often(every=10)
471
+ @schedule_often(every=10)
464
472
  def send_pending_emails_often(ar):
465
473
  # print("20231021 send_pending_emails_often()", ar.logger)
466
474
  rt.models.notify.Message.send_summary_emails(ar, MailModes.often)
467
475
 
468
476
 
469
- @dd.schedule_daily()
477
+ @schedule_daily()
470
478
  def send_pending_emails_daily(ar):
471
479
  rt.models.notify.Message.send_summary_emails(ar, MailModes.daily)
472
480
 
473
481
 
474
- # @dd.schedule_often(every=10)
482
+ # @schedule_often(every=10)
475
483
  # def send_pending_emails_often(ar):
476
484
  # Message = rt.models.notify.Message
477
485
  # qs = Message.objects.filter(sent__isnull=True)
@@ -488,7 +496,7 @@ remove_after = dd.plugins.notify.remove_after
488
496
 
489
497
  if remove_after:
490
498
 
491
- @dd.schedule_daily()
499
+ @schedule_daily()
492
500
  def clear_seen_messages(ar):
493
501
  Message = rt.models.notify.Message
494
502
  qs = Message.objects.filter(
@@ -33,7 +33,7 @@ class BasePrintAction(Action):
33
33
  hotkey = Hotkey("p", code="KeyP", ctrl=True)
34
34
 
35
35
  def __init__(self, build_method=None, label=None, **kwargs):
36
- super(BasePrintAction, self).__init__(label, **kwargs)
36
+ super().__init__(label, **kwargs)
37
37
  if build_method is not None:
38
38
  self.build_method = build_method
39
39
 
@@ -46,7 +46,7 @@ class BasePrintAction(Action):
46
46
  return False
47
47
  # if actor.__name__ == 'ExcerptsByProject':
48
48
  # logger.info("20140401 attach_to_actor() %r", self)
49
- return super(BasePrintAction, self).attach_to_actor(actor, name)
49
+ return super().attach_to_actor(actor, name)
50
50
 
51
51
  def get_print_templates(self, bm, elem):
52
52
  # print("20190506 BasePrintAction.get_print_templates", elem)
@@ -65,17 +65,19 @@ class BasePrintAction(Action):
65
65
  """Return the target filename if a document needs to be built,
66
66
  otherwise return ``None``.
67
67
  """
68
- elem.before_printable_build(bm)
69
- # raise Exception("20170519 before_build didn't warn")
70
- filename = bm.get_target_name(self, elem)
71
- if not filename:
68
+ # elem.before_printable_build(bm)
69
+ if not elem.must_build_printable(bm):
72
70
  return
73
- if os.path.exists(filename):
74
- logger.debug("%s %s -> overwrite existing %s.", bm, elem, filename)
75
- os.remove(filename)
76
- else:
77
- # logger.info("20121221 makedirs_if_missing %s",os.path.dirname(filename))
78
- rt.makedirs_if_missing(os.path.dirname(filename))
71
+ # raise Exception("20170519 before_build didn't warn")
72
+ filename = bm.get_target_file(self, elem).path
73
+ filename.unlink(missing_ok=True)
74
+ filename.parent.mkdir(exist_ok=True, parents=True)
75
+ # if filename.exists():
76
+ # logger.debug("%s %s -> overwrite existing %s.", bm, elem, filename)
77
+ # os.remove(filename)
78
+ # else:
79
+ # # logger.info("20121221 makedirs_if_missing %s",os.path.dirname(filename))
80
+ # rt.makedirs_if_missing(os.path.dirname(filename))
79
81
  logger.debug("%s : %s -> %s", bm, elem, filename)
80
82
  return filename
81
83
 
@@ -90,7 +92,8 @@ class BasePrintAction(Action):
90
92
  "If it doesn't, please "
91
93
  "ask your system administrator."
92
94
  )
93
- msg = msg.format(etree.tostring(E.a(leaf, href=url), encoding="unicode"))
95
+ msg = msg.format(etree.tostring(
96
+ E.a(leaf, href=url), encoding="unicode"))
94
97
  # msg %= dict(doc=leaf, help=etree.tostring(
95
98
  # help_url, encoding="unicode"))
96
99
  kw.update(message=msg, alert=True)
@@ -104,9 +107,10 @@ class BasePrintAction(Action):
104
107
  if isinstance(bm, str):
105
108
  bm = BuildMethods.get_by_value(bm)
106
109
  bm.build(ar, self, elem)
107
- mf = bm.get_target(self, elem)
108
- leaf = mf.parts[-1]
109
- self.notify_done(ar, bm, leaf, mf.get_url(ar.request), **kw)
110
+ mf = bm.get_target_file(self, elem)
111
+ # leaf = mf.parts[-1]
112
+ leaf = mf.path.name
113
+ self.notify_done(ar, bm, leaf, mf.url, **kw)
110
114
 
111
115
 
112
116
  class DirectPrintAction(BasePrintAction):
@@ -168,23 +172,25 @@ class CachedPrintAction(BasePrintAction):
168
172
  if len(ar.selected_rows) == 1:
169
173
  obj = ar.selected_rows[0]
170
174
  bm = obj.get_build_method()
171
- mf = bm.get_target(self, obj)
172
- leaf = mf.parts[-1]
175
+ mf = bm.get_target_file(self, obj)
176
+ # leaf = mf.parts[-1]
177
+ leaf = mf.path.name
173
178
  if obj.build_time is None:
174
179
  obj.build_target(ar)
175
180
  ar.debug("%s has been built.", leaf)
176
181
  else:
177
182
  ar.debug("Reused %s from cache.", leaf)
178
183
 
179
- url = mf.get_url(ar.request)
180
- self.notify_done(ar, bm, leaf, url, **kw)
184
+ # url = mf.get_url(ar.request)
185
+ self.notify_done(ar, bm, leaf, mf.url, **kw)
181
186
  ar.set_response(refresh=True)
182
187
  return
183
188
 
184
189
  def ok(ar2):
185
190
  # qs = [ar.actor.get_row_by_pk(pk) for pk in ar.selected_pks]
186
191
  mf = self.print_multiple(ar, ar.selected_rows)
187
- ar2.success(open_url=mf.get_url(ar.request))
192
+ ar2.success(open_url=mf.url)
193
+ # ar2.success(open_url=mf.get_url(ar.request))
188
194
  # kw.update(refresh_all=True)
189
195
  # return kw
190
196
 
@@ -197,13 +203,16 @@ class CachedPrintAction(BasePrintAction):
197
203
  # assert isinstance(obj,CachedPrintable)
198
204
  if obj.printed_by_id is None:
199
205
  obj.build_target(ar)
200
- pdf = obj.get_target_name()
201
- assert pdf is not None
202
- pdfs.append(pdf)
206
+ # pdf = obj.get_target_file().name
207
+ # assert pdf is not None
208
+ # pdfs.append(pdf)
209
+ mf = obj.get_target_file()
210
+ pdfs.append(mf.path)
203
211
 
204
212
  mf = TmpMediaFile(ar, "pdf")
205
- rt.makedirs_if_missing(os.path.dirname(mf.name))
206
- merge_pdfs(pdfs, mf.name)
213
+ # rt.makedirs_if_missing(os.path.dirname(mf.name))
214
+ rt.makedirs_if_missing(mf.path.parent)
215
+ merge_pdfs(pdfs, mf.path)
207
216
  return mf
208
217
 
209
218
 
@@ -217,7 +226,7 @@ class EditTemplate(BasePrintAction):
217
226
  # if not settings.SITE.is_installed('davlink'):
218
227
  if not settings.SITE.webdav_protocol:
219
228
  return False
220
- return super(EditTemplate, self).attach_to_actor(actor, name)
229
+ return super().attach_to_actor(actor, name)
221
230
 
222
231
  def run_from_ui(self, ar, **kw):
223
232
  lcd = settings.SITE.confdirs.LOCAL_CONFIG_DIR
@@ -269,7 +278,8 @@ class EditTemplate(BasePrintAction):
269
278
  ar.debug("Gonna copy %s to %s", filename, local_file)
270
279
 
271
280
  def ok(ar2):
272
- logger.info("%s made local template copy %s", ar.user, local_file)
281
+ logger.info("%s made local template copy %s",
282
+ ar.user, local_file)
273
283
  rt.makedirs_if_missing(os.path.dirname(local_file))
274
284
  shutil.copyfile(filename, local_file)
275
285
  # shutil.copy() can cause PermissionError if dst exists and is
@@ -299,14 +309,15 @@ class ClearCacheAction(Action):
299
309
  # should be visible in the UI
300
310
  if obj is not None and not obj.build_time:
301
311
  return False
302
- return super(ClearCacheAction, self).get_action_permission(ar, obj, state)
312
+ return super().get_action_permission(ar, obj, state)
303
313
 
304
314
  def run_from_ui(self, ar):
305
315
  elem = ar.selected_rows[0]
306
316
 
307
317
  def doit(ar):
308
318
  elem.clear_cache()
309
- ar.success(_("%s printable cache has been cleared.") % elem, refresh=True)
319
+ ar.success(_("%s printable cache has been cleared.") %
320
+ elem, refresh=True)
310
321
 
311
322
  t = elem.get_cache_mtime()
312
323
  if t is not None:
@@ -44,21 +44,21 @@ class BuildMethod(Choice):
44
44
  names = self.name
45
45
  super().__init__(names, self.__class__.__name__, names, **kwargs)
46
46
 
47
- def get_target(self, action, obj):
47
+ def get_target_file(self, action, obj):
48
48
  # Used by get_target_name()
49
49
  # assert self.name is not None
50
50
  return MediaFile(
51
51
  self.use_webdav,
52
52
  self.cache_name,
53
53
  self.value,
54
- obj.filename_root() + self.target_ext,
54
+ obj.get_printable_target_stem() + self.target_ext,
55
55
  )
56
56
 
57
- def get_target_name(self, action, elem):
58
- return self.get_target(action, elem).name
57
+ # def get_target_name(self, action, elem):
58
+ # return self.get_target_file(action, elem).name
59
59
 
60
60
  def get_target_url(self, action, elem):
61
- return self.get_target(action, elem).url
61
+ return self.get_target_file(action, elem).url
62
62
 
63
63
  def build(self, ar, action, elem):
64
64
  raise NotImplementedError
@@ -108,7 +108,8 @@ class DjangoBuildMethod(TemplatedBuildMethod):
108
108
  except TemplateDoesNotExist as e:
109
109
  raise Warning("No template found for %s (%s)" % (e, tpls2))
110
110
  except Exception as e:
111
- raise Exception("Error while loading template for %s : %s" % (tpls2, e))
111
+ raise Exception(
112
+ "Error while loading template for %s : %s" % (tpls2, e))
112
113
 
113
114
  # ,MEDIA_URL=settings.MEDIA_URL):
114
115
  def render_template(self, elem, tpl, **context):
@@ -170,7 +171,8 @@ class SimpleBuildMethod(TemplatedBuildMethod):
170
171
 
171
172
  lang = elem.get_print_language() or translation.get_language()
172
173
  if lang != settings.SITE.DEFAULT_LANGUAGE.django_code:
173
- name = tpl_leaf[: -len(self.template_ext)] + "_" + lang + self.template_ext
174
+ name = tpl_leaf[: -len(self.template_ext)] + \
175
+ "_" + lang + self.template_ext
174
176
  if rt.find_config_file(name, *elem.get_template_groups()):
175
177
  return name
176
178
  return tpl_leaf
@@ -238,7 +240,6 @@ class XmlBuildMethod(DjangoBuildMethod):
238
240
  lang = str(elem.get_print_language() or translation.get_language())
239
241
  # or settings.SITE.DEFAULT_LANGUAGE.django_code)
240
242
 
241
-
242
243
  with translation.override(lang):
243
244
  cmd_options = elem.get_build_options(self)
244
245
  logger.info(
@@ -288,7 +289,8 @@ class BuildMethods(ChoiceList):
288
289
  if bm is None:
289
290
  raise Exception(
290
291
  "Invalid default_build_method '{}', choices are {}".format(
291
- settings.SITE.default_build_method, tuple(cls.get_list_items())
292
+ settings.SITE.default_build_method, tuple(
293
+ cls.get_list_items())
292
294
  )
293
295
  )
294
296
  return bm
@@ -1,5 +1,5 @@
1
1
  # -*- coding: UTF-8 -*-
2
- # Copyright 2009-2024 Rumma & Ko Ltd
2
+ # Copyright 2009-2025 Rumma & Ko Ltd
3
3
  # License: GNU Affero General Public License v3 (see file COPYING for details)
4
4
 
5
5
  import os
@@ -137,14 +137,15 @@ class Printable(Model):
137
137
  kw = ar.get_printable_context(**kw)
138
138
  kw.update(this=self) # for backward compatibility
139
139
  kw.update(obj=self) # preferred in new templates
140
- kw.update(language=self.get_print_language() or translation.get_language())
140
+ kw.update(language=self.get_print_language()
141
+ or translation.get_language())
141
142
  # settings.SITE.DEFAULT_LANGUAGE.django_code)
142
143
  kw.update(site=settings.SITE)
143
144
  kw.update(weekdays=weekdays)
144
145
  return kw
145
146
 
146
- def before_printable_build(self, bm):
147
- pass
147
+ def must_build_printable(self, bm):
148
+ return True
148
149
 
149
150
 
150
151
  class CachedPrintable(Duplicable, Printable):
@@ -155,23 +156,26 @@ class CachedPrintable(Duplicable, Printable):
155
156
  do_clear_cache = ClearCacheAction()
156
157
  edit_template = EditTemplate()
157
158
 
158
- build_time = models.DateTimeField(_("build time"), null=True, editable=False)
159
-
159
+ build_time = models.DateTimeField(
160
+ _("build time"), null=True, editable=False)
160
161
  build_method = BuildMethods.field(blank=True, null=True)
161
162
 
162
163
  def full_clean(self, *args, **kwargs):
163
164
  if not self.build_method:
164
165
  self.build_method = self.get_default_build_method()
165
- super(CachedPrintable, self).full_clean(*args, **kwargs)
166
+ super().full_clean(*args, **kwargs)
166
167
 
167
168
  def on_duplicate(self, ar, master):
168
- super(CachedPrintable, self).on_duplicate(ar, master)
169
+ super().on_duplicate(ar, master)
169
170
  self.build_time = None
170
171
  self.build_method = None
171
172
 
172
- def get_target_name(self):
173
- if self.build_time:
174
- return self.get_build_method().get_target_name(self.do_print, self)
173
+ def must_build_printable(self, bm):
174
+ return self.build_time is None
175
+
176
+ # def get_target_name(self):
177
+ # if self.build_time:
178
+ # return self.get_build_method().get_target_name(self.do_print, self)
175
179
 
176
180
  def get_build_method(self):
177
181
  return self.build_method or self.get_default_build_method()
@@ -179,19 +183,20 @@ class CachedPrintable(Duplicable, Printable):
179
183
  def get_target_url(self):
180
184
  return self.build_method.get_target_url(self.do_print, self)
181
185
 
186
+ def get_target_file(self):
187
+ return self.build_method.get_target_file(self.do_print, self)
188
+
182
189
  def get_cache_mtime(self):
183
190
  """Return the modification time (a `datetime`) of the generated cache
184
191
  file, or `None` if no such file exists.
185
192
 
186
193
  """
187
- filename = self.get_target_name()
194
+ filename = self.get_target_file().path
188
195
  if not filename:
189
196
  return None
190
- try:
191
- t = os.path.getmtime(filename)
192
- except OSError:
197
+ if not filename.exists():
193
198
  return None
194
- return datetime.datetime.fromtimestamp(t)
199
+ return datetime.datetime.fromtimestamp(filename.lstat().st_mtime)
195
200
 
196
201
  def clear_cache(self):
197
202
  self.build_time = None
@@ -223,14 +228,14 @@ class TypedPrintable(CachedPrintable):
223
228
  def get_template_groups(self):
224
229
  ptype = self.get_printable_type()
225
230
  if ptype is None:
226
- return super(TypedPrintable, self).get_template_groups()
231
+ return super().get_template_groups()
227
232
  return ptype.get_template_groups()
228
233
 
229
234
  def get_default_build_method(self):
230
235
  ptype = self.get_printable_type()
231
236
  if ptype and ptype.build_method:
232
237
  return ptype.build_method
233
- return super(TypedPrintable, self).get_default_build_method()
238
+ return super().get_default_build_method()
234
239
 
235
240
  # def get_build_method(self):
236
241
  # if not self.build_method:
@@ -239,12 +244,12 @@ class TypedPrintable(CachedPrintable):
239
244
  # ptype = self.get_printable_type()
240
245
  # if ptype and ptype.build_method:
241
246
  # return ptype.build_method
242
- # return super(TypedPrintable, self).get_build_method()
247
+ # return super().get_build_method()
243
248
 
244
249
  def get_print_templates(self, bm, action):
245
250
  ptype = self.get_printable_type()
246
251
  if ptype is None:
247
- return super(TypedPrintable, self).get_print_templates(bm, action)
252
+ return super().get_print_templates(bm, action)
248
253
 
249
254
  if ptype.template:
250
255
  return [ptype.template]