lino 25.4.1__py3-none-any.whl → 25.4.3__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 +8 -10
- lino/core/dbtables.py +2 -3
- lino/core/fields.py +30 -15
- lino/core/kernel.py +33 -7
- lino/core/renderer.py +3 -3
- lino/core/requests.py +24 -9
- lino/core/site.py +38 -29
- lino/core/tables.py +27 -29
- lino/help_texts.py +7 -3
- lino/management/commands/demotest.py +16 -22
- lino/mixins/__init__.py +32 -35
- lino/mixins/dupable.py +2 -4
- lino/mixins/registrable.py +5 -2
- lino/modlib/about/models.py +2 -2
- lino/modlib/changes/models.py +2 -2
- lino/modlib/checkdata/choicelists.py +4 -4
- lino/modlib/checkdata/models.py +2 -2
- lino/modlib/comments/fixtures/demo2.py +4 -0
- lino/modlib/comments/models.py +1 -1
- lino/modlib/dupable/mixins.py +3 -5
- lino/modlib/export_excel/__init__.py +6 -8
- lino/modlib/export_excel/models.py +1 -3
- lino/modlib/extjs/ext_renderer.py +1 -1
- lino/modlib/extjs/views.py +1 -1
- lino/modlib/help/fixtures/demo2.py +3 -2
- lino/modlib/jinja/mixins.py +20 -4
- lino/modlib/linod/mixins.py +17 -12
- lino/modlib/linod/models.py +1 -1
- lino/modlib/memo/mixins.py +3 -2
- lino/modlib/notify/api.py +33 -14
- lino/modlib/notify/mixins.py +16 -1
- lino/modlib/notify/models.py +10 -25
- lino/modlib/printing/mixins.py +1 -1
- lino/modlib/publisher/models.py +55 -6
- lino/modlib/publisher/ui.py +3 -3
- lino/modlib/publisher/views.py +9 -2
- lino/modlib/system/models.py +1 -1
- lino/modlib/uploads/mixins.py +1 -1
- lino/modlib/uploads/models.py +2 -2
- lino/modlib/users/actions.py +2 -4
- lino/modlib/users/models.py +13 -19
- lino/modlib/users/ui.py +1 -1
- lino/sphinxcontrib/logo/templates/part-of-synodalsoft.html +0 -1
- lino/utils/format_date.py +10 -5
- lino/utils/media.py +16 -31
- {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/METADATA +1 -1
- {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/RECORD +51 -52
- lino/management/commands/monitor.py +0 -160
- {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/WHEEL +0 -0
- {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/licenses/COPYING +0 -0
@@ -68,34 +68,32 @@ class TestCase(DemoTestCase):
|
|
68
68
|
|
69
69
|
# For this test we reduce max_blacklist_time because we are going to
|
70
70
|
# simulate a hacker who patiently waits:
|
71
|
-
ipdict.max_blacklist_time = timedelta(seconds=
|
71
|
+
ipdict.max_blacklist_time = timedelta(seconds=4)
|
72
72
|
|
73
73
|
self.assertEqual(ipdict.ip_records, {})
|
74
74
|
|
75
|
-
def login(pwd):
|
75
|
+
def login(pwd, expected):
|
76
76
|
d = self.login("robin", pwd)
|
77
|
-
|
77
|
+
if d.message != expected:
|
78
|
+
self.fail(f"Expected {expected} but got {d.message} ({d})")
|
78
79
|
|
79
|
-
|
80
|
+
login("bad", "Failed to sign in as robin.")
|
80
81
|
rec = ipdict.ip_records["127.0.0.1"]
|
81
82
|
self.assertEqual(rec.login_failures, 1)
|
82
|
-
|
83
|
+
login("bad", "Failed to sign in as robin.")
|
83
84
|
self.assertEqual(rec.login_failures, 2)
|
84
|
-
|
85
|
+
login("bad", "Failed to sign in as robin.")
|
85
86
|
self.assertEqual(rec.login_failures, 3)
|
86
|
-
|
87
|
+
login("bad", "Failed to sign in as robin.")
|
87
88
|
self.assertEqual(rec.login_failures, 4)
|
88
|
-
|
89
|
-
login("bad"), "Too many authentication failures from 127.0.0.1"
|
90
|
-
)
|
89
|
+
login("bad", "Too many authentication failures from 127.0.0.1")
|
91
90
|
|
92
91
|
# login_failures doesn't continue to increase when the ip is blacklisted:
|
93
92
|
self.assertEqual(rec.login_failures, 4)
|
94
93
|
|
95
94
|
# Even with the right password you cannot unlock a blacklisted ip
|
96
|
-
|
97
|
-
|
98
|
-
)
|
95
|
+
login(dd.plugins.users.demo_password,
|
96
|
+
"Too many authentication failures from 127.0.0.1")
|
99
97
|
|
100
98
|
# After max_blacklist_time, the IP gets removed from the blacklist, but
|
101
99
|
# every new failure will now blacklist it again, the
|
@@ -103,24 +101,20 @@ class TestCase(DemoTestCase):
|
|
103
101
|
|
104
102
|
# time.sleep(1.5)
|
105
103
|
time.sleep(5)
|
106
|
-
|
104
|
+
login("bad", "Failed to sign in as robin.")
|
107
105
|
self.assertEqual(rec.login_failures, 5)
|
108
|
-
|
109
|
-
login("bad"), "Too many authentication failures from 127.0.0.1"
|
110
|
-
)
|
106
|
+
login("bad", "Too many authentication failures from 127.0.0.1")
|
111
107
|
self.assertEqual(rec.login_failures, 5)
|
112
108
|
|
113
|
-
time.sleep(
|
114
|
-
|
115
|
-
login(dd.plugins.users.demo_password),
|
116
|
-
"Now signed in as Robin Rood")
|
109
|
+
time.sleep(5)
|
110
|
+
login(dd.plugins.users.demo_password, "Now signed in as Robin Rood")
|
117
111
|
|
118
112
|
# Once you manage to authenticate, your ip address gets removed from the
|
119
113
|
# blacklist, i.e. when you log out and in for some reason, you get again
|
120
114
|
# max_failed_auth_per_ip attempts
|
121
115
|
|
122
116
|
self.assertEqual(ipdict.ip_records, {})
|
123
|
-
|
117
|
+
login("bad", "Failed to sign in as robin.")
|
124
118
|
rec = ipdict.ip_records["127.0.0.1"]
|
125
119
|
self.assertEqual(rec.login_failures, 1)
|
126
120
|
|
lino/mixins/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2010-
|
2
|
+
# Copyright 2010-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
"""
|
5
5
|
This package contains model mixins, some of which are heavily used
|
@@ -22,18 +22,37 @@ by applications and the :ref:`xl`. But none of them is mandatory.
|
|
22
22
|
from django.db import models
|
23
23
|
from django.conf import settings
|
24
24
|
from django.utils.translation import gettext_lazy as _
|
25
|
-
from django.utils.html import format_html
|
26
|
-
from django.utils.text import format_lazy
|
27
|
-
from django.utils import timezone
|
25
|
+
# from django.utils.html import format_html
|
26
|
+
# from django.utils.text import format_lazy
|
27
|
+
# from django.utils import timezone
|
28
28
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
29
29
|
|
30
|
+
# Note that reordering the imports here can cause the field ordering to change
|
31
|
+
# in models like lino_voga.lib.courses.TeacherType, which inherits from
|
32
|
+
# `(Referrable, BabelNamed, Printable)`. This can cause doctests like
|
33
|
+
# docs/specs/voga/courses.rst to fail because the `ref` field then came after
|
34
|
+
# the name field. The TeacherTypes table has no explicit `column_names`, so it
|
35
|
+
# uses the "natural" field ordering, which is, as this observation shows, quite
|
36
|
+
# unpredictable.
|
37
|
+
|
30
38
|
from lino.core import actions
|
31
39
|
from lino.core import fields
|
32
40
|
from lino.core import model
|
33
|
-
from lino.core.workflows import ChangeStateAction
|
41
|
+
# from lino.core.workflows import ChangeStateAction
|
42
|
+
# from lino.core.exceptions import ChangedAPI
|
34
43
|
from lino.utils.mldbc.fields import LanguageField
|
35
|
-
from lino.core.exceptions import ChangedAPI
|
36
44
|
from lino.utils.html import E
|
45
|
+
from lino.utils.mldbc.mixins import BabelNamed, BabelDesignated
|
46
|
+
from lino.utils.mldbc.fields import BabelCharField, BabelTextField
|
47
|
+
|
48
|
+
from .human import Human
|
49
|
+
from .polymorphic import Polymorphic
|
50
|
+
from .periods import ObservedDateRange, Yearly, Monthly, Today
|
51
|
+
from .periods import DateRange
|
52
|
+
from .sequenced import Sequenced, Hierarchical
|
53
|
+
from .duplicable import Duplicable, Duplicate
|
54
|
+
from .registrable import Registrable, RegistrableState
|
55
|
+
from .ref import Referrable, StructuredReferrable
|
37
56
|
|
38
57
|
|
39
58
|
class Contactable(model.Model):
|
@@ -97,19 +116,19 @@ class Modified(model.Model):
|
|
97
116
|
super().save(*args, **kwargs)
|
98
117
|
|
99
118
|
def touch(self):
|
100
|
-
self.modified =
|
119
|
+
self.modified = settings.SITE.now()
|
101
120
|
|
102
121
|
|
103
122
|
class Created(model.Model):
|
104
123
|
"""
|
105
|
-
Adds a timestamp field
|
106
|
-
individual database
|
124
|
+
Adds a timestamp field that holds the creation time of every
|
125
|
+
individual :term:`database row`.
|
107
126
|
|
108
127
|
.. attribute:: created
|
109
128
|
|
110
|
-
The time when this
|
129
|
+
The time when this :term:`database row` was created.
|
111
130
|
|
112
|
-
Does
|
131
|
+
Does not use Django's `auto_now` and `auto_now_add` features
|
113
132
|
because their deserialization would be problematic.
|
114
133
|
"""
|
115
134
|
|
@@ -123,8 +142,8 @@ class Created(model.Model):
|
|
123
142
|
return naturaltime(self.created)
|
124
143
|
|
125
144
|
def save(self, *args, **kwargs):
|
126
|
-
if self.created is None and not settings.SITE.loading_from_dump:
|
127
|
-
self.created =
|
145
|
+
if self.created is None: # and not settings.SITE.loading_from_dump:
|
146
|
+
self.created = settings.SITE.now()
|
128
147
|
super().save(*args, **kwargs)
|
129
148
|
|
130
149
|
|
@@ -264,25 +283,3 @@ class Draggable(model.Model):
|
|
264
283
|
|
265
284
|
def on_dropped(self, ar, **kwargs):
|
266
285
|
pass
|
267
|
-
|
268
|
-
|
269
|
-
from .ref import Referrable, StructuredReferrable
|
270
|
-
from .registrable import Registrable, RegistrableState
|
271
|
-
from lino.mixins.duplicable import Duplicable, Duplicate
|
272
|
-
from lino.mixins.sequenced import Sequenced, Hierarchical
|
273
|
-
from lino.mixins.periods import DateRange
|
274
|
-
from lino.mixins.periods import ObservedDateRange, Yearly, Monthly, Today
|
275
|
-
from lino.mixins.polymorphic import Polymorphic
|
276
|
-
|
277
|
-
# Observation: moving the following two lines to the top (to be together with
|
278
|
-
# the import of LanguageField) caused the field ordering to change in models
|
279
|
-
# like `lino_voga.lib.courses.TeacherType` which inherits from `(Referrable,
|
280
|
-
# BabelNamed, Printable)`. Which caused docs/specs/voga/courses.rst to fail
|
281
|
-
# because the `ref` field then came after the name field. The TeacherTypes table
|
282
|
-
# has no explicit `column_names`, so it uses the "natural" field ordering, which
|
283
|
-
# is, as this observation shows, quite unpredictable.
|
284
|
-
|
285
|
-
from lino.utils.mldbc.fields import BabelCharField, BabelTextField
|
286
|
-
from lino.utils.mldbc.mixins import BabelNamed, BabelDesignated
|
287
|
-
|
288
|
-
from lino.mixins.human import Human
|
lino/mixins/dupable.py
CHANGED
@@ -39,6 +39,7 @@ from lino.core.actions import SubmitInsert
|
|
39
39
|
from lino.utils import join_elems
|
40
40
|
from lino.utils.html import E, tostring, mark_safe
|
41
41
|
from lino.core import constants
|
42
|
+
from lino.modlib.checkdata.choicelists import Checker
|
42
43
|
|
43
44
|
|
44
45
|
class CheckedSubmitInsert(SubmitInsert):
|
@@ -242,9 +243,6 @@ class Dupable(dd.Model):
|
|
242
243
|
return qs[:limit]
|
243
244
|
|
244
245
|
|
245
|
-
from lino.modlib.checkdata.choicelists import Checker
|
246
|
-
|
247
|
-
|
248
246
|
class DupableChecker(Checker):
|
249
247
|
"""Checks for the following repairable problem:
|
250
248
|
|
@@ -255,7 +253,7 @@ class DupableChecker(Checker):
|
|
255
253
|
verbose_name = _("Check for missing phonetic words")
|
256
254
|
model = Dupable
|
257
255
|
|
258
|
-
def get_checkdata_problems(self, obj, fix=False):
|
256
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
259
257
|
msg = obj.update_dupable_words(fix)
|
260
258
|
if msg:
|
261
259
|
yield (True, msg)
|
lino/mixins/registrable.py
CHANGED
@@ -119,8 +119,11 @@ class Registrable(model.Model):
|
|
119
119
|
|
120
120
|
def disabled_fields(self, ar):
|
121
121
|
if not self.state.is_editable:
|
122
|
-
return self._registrable_fields
|
123
|
-
|
122
|
+
# return self._registrable_fields
|
123
|
+
# Copy _registrable_fields otherwise _registrable_fields get
|
124
|
+
# modified as more disabled fields are added to the set.
|
125
|
+
return self._registrable_fields.copy()
|
126
|
+
return super().disabled_fields(ar)
|
124
127
|
|
125
128
|
def get_row_permission(self, ar, state, ba):
|
126
129
|
"""Only rows in an editable state may be edited.
|
lino/modlib/about/models.py
CHANGED
@@ -119,8 +119,8 @@ class About(EmptyTable):
|
|
119
119
|
packages = set(["django"])
|
120
120
|
|
121
121
|
items.append(
|
122
|
-
E.li(gettext("Server timestamp"), " : ",
|
123
|
-
|
122
|
+
E.li(gettext("Server timestamp"), " : ",
|
123
|
+
E.b(dtfmt(site.kernel.lino_version))))
|
124
124
|
|
125
125
|
for p in site.installed_plugins:
|
126
126
|
packages.add(p.app_name.split(".")[0])
|
lino/modlib/changes/models.py
CHANGED
@@ -159,7 +159,7 @@ class ChangesByMaster(Changes):
|
|
159
159
|
def log_change(type, request, master, obj, msg="", changed_fields=""):
|
160
160
|
Change(
|
161
161
|
type=type,
|
162
|
-
time=
|
162
|
+
time=dd.now(),
|
163
163
|
master=master,
|
164
164
|
user=request.user,
|
165
165
|
object=obj,
|
@@ -177,7 +177,7 @@ if remove_after:
|
|
177
177
|
def delete_older_changes(ar):
|
178
178
|
days = datetime.timedelta(days=remove_after)
|
179
179
|
# django.core.exceptions.FieldError: Cannot resolve keyword 'time_lt' into field. Choices are: changed_fields, diff, id, list_item, master, master_id, master_type, master_type_id, name_column, navigation_panel, object, object_id, object_type, object_type_id, overview, time, type, user, user_id, workflow_buttons
|
180
|
-
qs = Change.objects.filter(time__lt=
|
180
|
+
qs = Change.objects.filter(time__lt=dd.now() - days)
|
181
181
|
if qs.count() > 0:
|
182
182
|
ar.logger.info(
|
183
183
|
"Removing %d changes older than %d days.", qs.count(), remove_after
|
@@ -64,8 +64,8 @@ class Checker(dd.Choice):
|
|
64
64
|
ar.logger.info(msg.format(len(todo), len(done), cls.self))
|
65
65
|
|
66
66
|
@classmethod
|
67
|
-
def check_instance(cls, *args, **kwargs):
|
68
|
-
return cls.self.get_checkdata_problems(*args, **kwargs)
|
67
|
+
def check_instance(cls, ar, *args, **kwargs):
|
68
|
+
return cls.self.get_checkdata_problems(ar, *args, **kwargs)
|
69
69
|
|
70
70
|
def get_checkable_models(self):
|
71
71
|
if self.model is None:
|
@@ -92,7 +92,7 @@ class Checker(dd.Choice):
|
|
92
92
|
|
93
93
|
done = []
|
94
94
|
todo = []
|
95
|
-
for fixable, msg in self.get_checkdata_problems(obj, fix):
|
95
|
+
for fixable, msg in self.get_checkdata_problems(ar, obj, fix):
|
96
96
|
if fixable:
|
97
97
|
# attn: do not yet translate
|
98
98
|
# msg = string_concat(u"(\u2605) ", msg)
|
@@ -126,7 +126,7 @@ class Checker(dd.Choice):
|
|
126
126
|
prb.save()
|
127
127
|
return (todo, done)
|
128
128
|
|
129
|
-
def get_checkdata_problems(self, obj, fix=False):
|
129
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
130
130
|
return []
|
131
131
|
|
132
132
|
def get_responsible_user(self, obj):
|
lino/modlib/checkdata/models.py
CHANGED
@@ -301,9 +301,9 @@ def get_checkers_for(model):
|
|
301
301
|
return checkers
|
302
302
|
|
303
303
|
|
304
|
-
def check_instance(obj, **kwargs):
|
304
|
+
def check_instance(ar, obj, **kwargs):
|
305
305
|
for chk in get_checkers_for(obj.__class__):
|
306
|
-
for fixable, msg in chk.check_instance(obj, **kwargs):
|
306
|
+
for fixable, msg in chk.check_instance(ar, obj, **kwargs):
|
307
307
|
if fixable:
|
308
308
|
msg = f"(\u2605) {msg}"
|
309
309
|
print(msg)
|
@@ -42,12 +42,16 @@ BODIES.items.insert(0, "")
|
|
42
42
|
|
43
43
|
|
44
44
|
def objects():
|
45
|
+
|
45
46
|
Comment = rt.models.comments.Comment
|
46
47
|
User = rt.models.users.User
|
47
48
|
Comment.auto_touch = False
|
48
49
|
# use_linod = settings.SITE.use_linod
|
49
50
|
# settings.SITE.use_linod = False
|
50
51
|
|
52
|
+
# avoid channels.exceptions.ChannelFull:
|
53
|
+
settings.SITE.loading_from_dump = True
|
54
|
+
|
51
55
|
MENTIONED = Cycler()
|
52
56
|
for model in rt.models_by_base(Commentable):
|
53
57
|
if model.memo_command is not None:
|
lino/modlib/comments/models.py
CHANGED
@@ -429,7 +429,7 @@ class CommentChecker(Checker):
|
|
429
429
|
model = Comment
|
430
430
|
msg_missing = _("Missing owner in reply to comment.")
|
431
431
|
|
432
|
-
def get_checkdata_problems(self, obj, fix=False):
|
432
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
433
433
|
if obj.reply_to_id and not obj.owner_id and obj.reply_to.owner_id:
|
434
434
|
yield (True, self.msg_missing)
|
435
435
|
if fix:
|
lino/modlib/dupable/mixins.py
CHANGED
@@ -13,6 +13,7 @@ from lino.api import dd, rt, _
|
|
13
13
|
from lino.core.actions import SubmitInsert
|
14
14
|
from lino.core.gfks import gfk2lookup
|
15
15
|
from lino.core.gfks import ContentType
|
16
|
+
from lino.modlib.checkdata.choicelists import Checker
|
16
17
|
|
17
18
|
|
18
19
|
class CheckedSubmitInsert(SubmitInsert):
|
@@ -154,9 +155,6 @@ class Dupable(dd.Model):
|
|
154
155
|
return qs[:limit]
|
155
156
|
|
156
157
|
|
157
|
-
from lino.modlib.checkdata.choicelists import Checker
|
158
|
-
|
159
|
-
|
160
158
|
class DupableChecker(Checker):
|
161
159
|
"""Checks for the following repairable problem:
|
162
160
|
|
@@ -167,7 +165,7 @@ class DupableChecker(Checker):
|
|
167
165
|
verbose_name = _("Check for missing phonetic words")
|
168
166
|
model = Dupable
|
169
167
|
|
170
|
-
def get_checkdata_problems(self, obj, fix=False):
|
168
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
171
169
|
msg = obj.update_dupable_words(fix)
|
172
170
|
if msg:
|
173
171
|
yield (True, msg)
|
@@ -180,7 +178,7 @@ class SimilarObjectsChecker(Checker):
|
|
180
178
|
model = Dupable
|
181
179
|
verbose_name = _("Check for similar objects")
|
182
180
|
|
183
|
-
def get_checkdata_problems(self, obj, fix=False):
|
181
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
184
182
|
lst = list(obj.find_similar_instances(1))
|
185
183
|
if len(lst):
|
186
184
|
msg = _("Similar clients: {clients}").format(
|
@@ -1,12 +1,7 @@
|
|
1
|
-
# Copyright 2014-
|
1
|
+
# Copyright 2014-2025 Rumma & Ko Ltd
|
2
2
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
3
|
-
"""
|
4
|
-
|
5
|
-
To use it, simply add the following line to your
|
6
|
-
:meth:`lino.core.site.Site.get_installed_plugins`::
|
7
|
-
|
8
|
-
yield 'lino.modlib.export_excel'
|
9
|
-
|
3
|
+
"""
|
4
|
+
See :doc:`/plugins/export_excel`.
|
10
5
|
"""
|
11
6
|
|
12
7
|
from lino import ad, _
|
@@ -16,3 +11,6 @@ class Plugin(ad.Plugin):
|
|
16
11
|
"See :doc:`/dev/plugins`."
|
17
12
|
|
18
13
|
verbose_name = _("Export to Excel xls format")
|
14
|
+
|
15
|
+
def get_requirements(self, site):
|
16
|
+
yield "openpyxl"
|
@@ -1,9 +1,7 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2014-
|
2
|
+
# Copyright 2014-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
|
-
"""Database models for `lino.modlib.export_excel`.
|
5
4
|
|
6
|
-
"""
|
7
5
|
import os
|
8
6
|
|
9
7
|
from django.conf import settings
|
@@ -1481,7 +1481,7 @@ class ExtRenderer(JsCacheRenderer):
|
|
1481
1481
|
# if settings.SITE.never_build_site_cache:
|
1482
1482
|
# yield "GEN_TIMESTAMP = '%s';" % settings.SITE.kernel.lino_version
|
1483
1483
|
# else:
|
1484
|
-
yield "GEN_TIMESTAMP = %s;" % py2js(settings.SITE.lino_version)
|
1484
|
+
yield "GEN_TIMESTAMP = %s;" % py2js(settings.SITE.kernel.lino_version)
|
1485
1485
|
|
1486
1486
|
return "\n".join(fn())
|
1487
1487
|
|
lino/modlib/extjs/views.py
CHANGED
@@ -138,7 +138,7 @@ def test_version_mismatch(request):
|
|
138
138
|
if os.environ.get("PYCHARM_HOSTED", False):
|
139
139
|
return {}
|
140
140
|
lv = request.GET.get(constants.URL_PARAM_LINO_VERSION)
|
141
|
-
if lv is None or float(lv) == settings.SITE.lino_version:
|
141
|
+
if lv is None or float(lv) == settings.SITE.kernel.lino_version:
|
142
142
|
return {}
|
143
143
|
# print("20201217", lv, settings.SITE.kernel.code_mtime)
|
144
144
|
cache.clear()
|
@@ -8,15 +8,16 @@ from lino.api import dd, rt, _
|
|
8
8
|
if dd.get_plugin_setting("help", "use_contacts"):
|
9
9
|
|
10
10
|
from lino.api.shell import help, contacts
|
11
|
+
from lino_xl.lib.contacts.models import PARTNER_NUMBERS_START_AT as PS
|
11
12
|
|
12
13
|
def site_contact(type, company=None, **kwargs):
|
13
14
|
return help.SiteContact(site_contact_type=type, company=company, **kwargs)
|
14
15
|
|
15
16
|
def objects():
|
16
17
|
yield site_contact("owner", settings.SITE.site_config.site_company)
|
17
|
-
yield site_contact("serveradmin", contacts.Company.objects.get(pk=
|
18
|
+
yield site_contact("serveradmin", contacts.Company.objects.get(pk=PS+6))
|
18
19
|
yield site_contact(
|
19
20
|
"hotline",
|
20
|
-
contact_person=contacts.Person.objects.get(pk=
|
21
|
+
contact_person=contacts.Person.objects.get(pk=PS+13),
|
21
22
|
**dd.babelkw("remark", _("Mon and Fri from 11:30 to 12:00")),
|
22
23
|
)
|
lino/modlib/jinja/mixins.py
CHANGED
@@ -8,10 +8,9 @@ from pathlib import Path
|
|
8
8
|
from lxml import etree
|
9
9
|
|
10
10
|
from django.conf import settings
|
11
|
-
from django.utils import translation
|
12
11
|
from django.utils.html import mark_safe, escape
|
13
12
|
|
14
|
-
from lino.api import dd
|
13
|
+
from lino.api import dd, _
|
15
14
|
from lino.utils.xml import validate_xml
|
16
15
|
from lino.utils.media import MediaFile
|
17
16
|
|
@@ -31,10 +30,21 @@ class XMLMaker(dd.Model):
|
|
31
30
|
xml_file_template = None
|
32
31
|
# xml_file_name = None
|
33
32
|
|
33
|
+
_xmlfile = None
|
34
|
+
|
35
|
+
@property
|
36
|
+
def xmlfile(self):
|
37
|
+
if self._xmlfile is None:
|
38
|
+
self._xmlfile = MediaFile(False, *self.get_xml_file_parts())
|
39
|
+
return self._xmlfile
|
40
|
+
|
34
41
|
def get_xml_file_parts(self):
|
35
42
|
yield 'xml'
|
36
43
|
yield self.get_printable_target_stem() + ".xml"
|
37
44
|
|
45
|
+
def get_xml_file(self):
|
46
|
+
return self.xmlfile
|
47
|
+
|
38
48
|
def make_xml_file(self, ar):
|
39
49
|
renderer = settings.SITE.plugins.jinja.renderer
|
40
50
|
tpl = renderer.jinja_env.get_template(self.xml_file_template)
|
@@ -45,9 +55,9 @@ class XMLMaker(dd.Model):
|
|
45
55
|
# parts = [
|
46
56
|
# dd.plugins.accounting.xml_media_dir,
|
47
57
|
# self.xml_file_name.format(self=self)]
|
48
|
-
xmlfile =
|
58
|
+
xmlfile = self.xmlfile
|
49
59
|
# xmlfile = Path(settings.MEDIA_ROOT, *parts)
|
50
|
-
ar.logger.
|
60
|
+
ar.logger.debug("Make %s from %s ...", xmlfile.path, self)
|
51
61
|
xmlfile.path.parent.mkdir(exist_ok=True, parents=True)
|
52
62
|
xmlfile.path.write_text(xml)
|
53
63
|
# xmlfile.write_text(etree.tostring(xml))
|
@@ -71,3 +81,9 @@ class XMLMaker(dd.Model):
|
|
71
81
|
# return mark_safe(f"""<a href="{url}">{url}</a>""")
|
72
82
|
# return (xmlfile, url)
|
73
83
|
return xmlfile
|
84
|
+
|
85
|
+
@dd.displayfield(_("XML file"))
|
86
|
+
def xml_file(self, ar):
|
87
|
+
mf = self.xmlfile
|
88
|
+
href = settings.SITE.media_root / mf.url
|
89
|
+
return mark_safe(f"<a href=\"{href}\" target=\"blank\">{mf.path.name}</a>")
|
lino/modlib/linod/mixins.py
CHANGED
@@ -45,15 +45,15 @@ class RunNow(dd.Action):
|
|
45
45
|
|
46
46
|
def run_from_ui(self, ar, **kwargs):
|
47
47
|
# print("20231102 RunNow", ar.selected_rows)
|
48
|
+
now = dd.now()
|
48
49
|
for obj in ar.selected_rows:
|
49
50
|
assert issubclass(obj.__class__, Runnable)
|
50
51
|
if True: # dd.plugins.linod.use_channels:
|
51
52
|
obj.last_start_time = None
|
52
53
|
obj.last_end_time = None
|
53
|
-
obj.requested_at =
|
54
|
-
|
55
|
-
|
56
|
-
)
|
54
|
+
obj.requested_at = now
|
55
|
+
tpl = _("{0} requested to run this task at {1}.")
|
56
|
+
obj.message = tpl.format(ar.get_user(), dd.fdtl(now))
|
57
57
|
# obj.disabled = False
|
58
58
|
obj.full_clean()
|
59
59
|
obj.save()
|
@@ -155,10 +155,12 @@ class Runnable(Sequenced, RecurrenceSet):
|
|
155
155
|
await ar.adebug("Start %s with logging level %s", self, self.log_level)
|
156
156
|
# ar.info("Start %s with logging level %s", astr(self), self.log_level)
|
157
157
|
# forget about any previous run:
|
158
|
-
|
158
|
+
now = await sync_to_async(dd.now)()
|
159
|
+
self.last_start_time = now
|
159
160
|
self.requested_at = None
|
160
161
|
self.last_end_time = None
|
161
|
-
self.message = ""
|
162
|
+
self.message = f"Started at {self.last_start_time} " \
|
163
|
+
f"with logging level {self.log_level}"
|
162
164
|
# print("20231102 full_clean")
|
163
165
|
await sync_to_async(self.full_clean)()
|
164
166
|
# self.full_clean()
|
@@ -184,19 +186,21 @@ class Runnable(Sequenced, RecurrenceSet):
|
|
184
186
|
self.disabled = True
|
185
187
|
await ar.awarning("Disabled %s after exception %s", self, e)
|
186
188
|
# ar.warning("Disabled %s after exception %s", astr(self), e)
|
187
|
-
|
189
|
+
now = await sync_to_async(dd.now)()
|
190
|
+
self.last_end_time = now
|
188
191
|
self.message = "<pre>" + self.message + "</pre>"
|
189
192
|
await sync_to_async(self.full_clean)()
|
190
193
|
# self.full_clean()
|
191
194
|
await self.asave()
|
195
|
+
await sync_to_async(dd.post_ui_save.send)(sender=self.__class__, instance=self)
|
192
196
|
|
193
197
|
@dd.displayfield("Status")
|
194
198
|
def status(self, ar=None):
|
195
199
|
if self.is_running():
|
196
|
-
return _("Running since {}").format(dd.
|
200
|
+
return _("Running since {}").format(dd.fdtf(self.last_start_time))
|
197
201
|
if self.requested_at is not None:
|
198
202
|
return _("Requested to run asap (since {})").format(
|
199
|
-
dd.
|
203
|
+
dd.fdtf(self.requested_at))
|
200
204
|
if self.disabled:
|
201
205
|
return _("Disabled")
|
202
206
|
if self.last_start_time is None or self.last_end_time is None:
|
@@ -206,7 +210,7 @@ class Runnable(Sequenced, RecurrenceSet):
|
|
206
210
|
next_time = self.get_next_suggested_date(self.last_end_time)
|
207
211
|
if next_time is None:
|
208
212
|
return _("Not scheduled")
|
209
|
-
return _("Scheduled to run at {}").format(dd.
|
213
|
+
return _("Scheduled to run at {}").format(dd.fdtf(next_time))
|
210
214
|
|
211
215
|
|
212
216
|
async def start_task_runner(ar=None, max_count=None):
|
@@ -219,7 +223,7 @@ async def start_task_runner(ar=None, max_count=None):
|
|
219
223
|
while True:
|
220
224
|
await ar.adebug("Start next task runner loop.")
|
221
225
|
|
222
|
-
now =
|
226
|
+
now = await sync_to_async(dd.now)()
|
223
227
|
next_time = now + \
|
224
228
|
timedelta(seconds=dd.plugins.linod.background_sleep_time)
|
225
229
|
|
@@ -282,7 +286,8 @@ async def start_task_runner(ar=None, max_count=None):
|
|
282
286
|
if max_count is not None and count >= max_count:
|
283
287
|
await ar.ainfo("Stop after %s loops.", max_count)
|
284
288
|
return next_time
|
285
|
-
|
289
|
+
now = await sync_to_async(dd.now)()
|
290
|
+
if (to_sleep := (next_time - now).total_seconds()) <= 0:
|
286
291
|
continue
|
287
292
|
await ar.adebug("Let task runner sleep for %s seconds.", to_sleep)
|
288
293
|
await asyncio.sleep(to_sleep)
|
lino/modlib/linod/models.py
CHANGED
@@ -55,7 +55,7 @@ class SystemTaskChecker(Checker):
|
|
55
55
|
verbose_name = _("Check for missing system tasks")
|
56
56
|
model = None
|
57
57
|
|
58
|
-
def get_checkdata_problems(self, obj, fix=False):
|
58
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
59
59
|
for proc in Procedures.get_list_items():
|
60
60
|
if proc.class_name == "linod.SystemTask":
|
61
61
|
if SystemTask.objects.filter(procedure=proc).count() == 0:
|
lino/modlib/memo/mixins.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
5
|
from lxml.html import fragments_fromstring
|
6
|
-
from lino.utils.html import E, tostring, mark_safe
|
7
6
|
import lxml
|
8
7
|
|
9
8
|
try:
|
@@ -19,6 +18,7 @@ from django.utils.html import format_html
|
|
19
18
|
from lino.core.gfks import gfk2lookup
|
20
19
|
from lino.core.model import Model
|
21
20
|
from lino.core.fields import fields_list, RichTextField, PreviewTextField
|
21
|
+
from lino.utils.html import E, tostring, mark_safe
|
22
22
|
from lino.utils.restify import restify
|
23
23
|
from lino.utils.soup import truncate_comment
|
24
24
|
from lino.utils.mldbc.fields import BabelTextField
|
@@ -33,6 +33,7 @@ def django_truncate_comment(html_str):
|
|
33
33
|
settings.SITE.plugins.memo.short_preview_length, html=True
|
34
34
|
)
|
35
35
|
|
36
|
+
|
36
37
|
MARKDOWNCFG = dict(
|
37
38
|
extensions=["toc"], extension_configs=dict(toc=dict(toc_depth=3, permalink=True))
|
38
39
|
)
|
@@ -293,7 +294,7 @@ class PreviewableChecker(Checker):
|
|
293
294
|
obj.save()
|
294
295
|
# self.synchronize_mentions(mentions)
|
295
296
|
|
296
|
-
def get_checkdata_problems(self, obj, fix=False):
|
297
|
+
def get_checkdata_problems(self, ar, obj, fix=False):
|
297
298
|
for x in self._get_checkdata_problems(settings.SITE.DEFAULT_LANGUAGE, obj, fix):
|
298
299
|
yield x
|
299
300
|
if isinstance(obj, BabelPreviewable):
|