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.
- lino/__init__.py +1 -1
- lino/api/dd.py +11 -48
- lino/api/doctest.py +34 -36
- lino/core/actions.py +25 -23
- lino/core/actors.py +37 -17
- lino/core/choicelists.py +10 -8
- lino/core/dbtables.py +1 -1
- lino/core/elems.py +46 -30
- lino/core/fields.py +19 -9
- lino/core/inject.py +7 -6
- lino/core/kernel.py +26 -66
- lino/core/model.py +44 -31
- lino/core/plugin.py +4 -4
- lino/core/requests.py +76 -55
- lino/core/site.py +84 -30
- lino/core/store.py +5 -2
- lino/core/utils.py +12 -7
- lino/help_texts.py +3 -8
- lino/management/commands/prep.py +1 -1
- lino/mixins/duplicable.py +6 -4
- lino/mixins/sequenced.py +17 -6
- lino/modlib/__init__.py +0 -2
- lino/modlib/changes/models.py +21 -10
- lino/modlib/checkdata/models.py +59 -24
- lino/modlib/comments/fixtures/demo2.py +12 -3
- lino/modlib/comments/models.py +7 -7
- lino/modlib/comments/ui.py +8 -5
- lino/modlib/export_excel/models.py +7 -5
- lino/modlib/extjs/views.py +39 -20
- lino/modlib/help/management/commands/makehelp.py +5 -2
- lino/modlib/jinja/mixins.py +25 -14
- lino/modlib/linod/__init__.py +1 -0
- lino/modlib/linod/choicelists.py +21 -0
- lino/modlib/linod/consumers.py +13 -4
- lino/modlib/linod/management/commands/linod.py +6 -2
- lino/modlib/linod/mixins.py +16 -11
- lino/modlib/linod/models.py +4 -2
- lino/modlib/notify/models.py +18 -10
- lino/modlib/printing/actions.py +41 -30
- lino/modlib/printing/choicelists.py +11 -9
- lino/modlib/printing/mixins.py +25 -20
- lino/modlib/publisher/models.py +5 -5
- lino/modlib/summaries/models.py +3 -2
- lino/modlib/system/models.py +28 -29
- lino/modlib/uploads/__init__.py +5 -5
- lino/modlib/uploads/actions.py +2 -8
- lino/modlib/uploads/choicelists.py +10 -10
- lino/modlib/uploads/fixtures/std.py +17 -0
- lino/modlib/uploads/mixins.py +20 -8
- lino/modlib/uploads/models.py +60 -35
- lino/modlib/uploads/ui.py +10 -7
- lino/utils/media.py +45 -23
- lino/utils/report.py +5 -4
- lino/utils/soup.py +22 -4
- {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/METADATA +1 -1
- {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/RECORD +59 -80
- lino/mixins/uploadable.py +0 -3
- lino/sandbox/bcss/PerformInvestigation.py +0 -2260
- lino/sandbox/bcss/SSDNReply.py +0 -3924
- lino/sandbox/bcss/SSDNRequest.py +0 -3723
- lino/sandbox/bcss/__init__.py +0 -0
- lino/sandbox/bcss/readme.txt +0 -1
- lino/sandbox/bcss/test.py +0 -92
- lino/sandbox/bcss/test2.py +0 -128
- lino/sandbox/bcss/test3.py +0 -161
- lino/sandbox/bcss/test4.py +0 -167
- lino/sandbox/contacts/__init__.py +0 -0
- lino/sandbox/contacts/fixtures/__init__.py +0 -0
- lino/sandbox/contacts/fixtures/demo.py +0 -365
- lino/sandbox/contacts/manage.py +0 -10
- lino/sandbox/contacts/models.py +0 -395
- lino/sandbox/contacts/settings.py +0 -67
- lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.wsdl +0 -65
- lino/sandbox/tx25/XSD/RetrieveTIGroupsV3.xsd +0 -286
- lino/sandbox/tx25/XSD/rn25_Release201104.xsd +0 -2855
- lino/sandbox/tx25/xsd2py1.py +0 -68
- lino/sandbox/tx25/xsd2py2.py +0 -62
- lino/sandbox/tx25/xsd2py3.py +0 -56
- {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/WHEEL +0 -0
- {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.2.3.dist-info → lino-25.3.1.dist-info}/licenses/COPYING +0 -0
lino/modlib/linod/__init__.py
CHANGED
lino/modlib/linod/choicelists.py
CHANGED
@@ -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)
|
lino/modlib/linod/consumers.py
CHANGED
@@ -36,7 +36,10 @@ from .utils import BROADCAST_CHANNEL
|
|
36
36
|
# logger.addHandler(logging.StreamHandler())
|
37
37
|
# logger.setLevel(logging.INFO)
|
38
38
|
|
39
|
-
|
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
|
-
|
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(
|
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"],
|
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-
|
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
|
-
|
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))
|
lino/modlib/linod/mixins.py
CHANGED
@@ -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,
|
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(
|
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(
|
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(
|
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 +
|
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()))
|
lino/modlib/linod/models.py
CHANGED
@@ -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(
|
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)
|
lino/modlib/notify/models.py
CHANGED
@@ -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(
|
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,
|
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(
|
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(
|
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 +
|
285
|
+
sender += subaddress_separator + \
|
286
|
+
str(msg.reply_to_id)
|
280
287
|
elif msg.owner_id:
|
281
|
-
sender += subaddress_separator +
|
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
|
-
@
|
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
|
-
@
|
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
|
-
# @
|
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
|
-
@
|
499
|
+
@schedule_daily()
|
492
500
|
def clear_seen_messages(ar):
|
493
501
|
Message = rt.models.notify.Message
|
494
502
|
qs = Message.objects.filter(
|
lino/modlib/printing/actions.py
CHANGED
@@ -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(
|
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(
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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(
|
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.
|
108
|
-
leaf = mf.parts[-1]
|
109
|
-
|
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.
|
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.
|
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.
|
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
|
-
|
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(
|
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",
|
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(
|
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.") %
|
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
|
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.
|
54
|
+
obj.get_printable_target_stem() + self.target_ext,
|
55
55
|
)
|
56
56
|
|
57
|
-
def get_target_name(self, action, elem):
|
58
|
-
|
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.
|
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(
|
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)] +
|
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(
|
292
|
+
settings.SITE.default_build_method, tuple(
|
293
|
+
cls.get_list_items())
|
292
294
|
)
|
293
295
|
)
|
294
296
|
return bm
|
lino/modlib/printing/mixins.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2009-
|
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()
|
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
|
147
|
-
|
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(
|
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(
|
166
|
+
super().full_clean(*args, **kwargs)
|
166
167
|
|
167
168
|
def on_duplicate(self, ar, master):
|
168
|
-
super(
|
169
|
+
super().on_duplicate(ar, master)
|
169
170
|
self.build_time = None
|
170
171
|
self.build_method = None
|
171
172
|
|
172
|
-
def
|
173
|
-
|
174
|
-
|
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.
|
194
|
+
filename = self.get_target_file().path
|
188
195
|
if not filename:
|
189
196
|
return None
|
190
|
-
|
191
|
-
t = os.path.getmtime(filename)
|
192
|
-
except OSError:
|
197
|
+
if not filename.exists():
|
193
198
|
return None
|
194
|
-
return datetime.datetime.fromtimestamp(
|
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(
|
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(
|
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(
|
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(
|
252
|
+
return super().get_print_templates(bm, action)
|
248
253
|
|
249
254
|
if ptype.template:
|
250
255
|
return [ptype.template]
|