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.
Files changed (52) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/dd.py +8 -10
  3. lino/core/dbtables.py +2 -3
  4. lino/core/fields.py +30 -15
  5. lino/core/kernel.py +33 -7
  6. lino/core/renderer.py +3 -3
  7. lino/core/requests.py +24 -9
  8. lino/core/site.py +38 -29
  9. lino/core/tables.py +27 -29
  10. lino/help_texts.py +7 -3
  11. lino/management/commands/demotest.py +16 -22
  12. lino/mixins/__init__.py +32 -35
  13. lino/mixins/dupable.py +2 -4
  14. lino/mixins/registrable.py +5 -2
  15. lino/modlib/about/models.py +2 -2
  16. lino/modlib/changes/models.py +2 -2
  17. lino/modlib/checkdata/choicelists.py +4 -4
  18. lino/modlib/checkdata/models.py +2 -2
  19. lino/modlib/comments/fixtures/demo2.py +4 -0
  20. lino/modlib/comments/models.py +1 -1
  21. lino/modlib/dupable/mixins.py +3 -5
  22. lino/modlib/export_excel/__init__.py +6 -8
  23. lino/modlib/export_excel/models.py +1 -3
  24. lino/modlib/extjs/ext_renderer.py +1 -1
  25. lino/modlib/extjs/views.py +1 -1
  26. lino/modlib/help/fixtures/demo2.py +3 -2
  27. lino/modlib/jinja/mixins.py +20 -4
  28. lino/modlib/linod/mixins.py +17 -12
  29. lino/modlib/linod/models.py +1 -1
  30. lino/modlib/memo/mixins.py +3 -2
  31. lino/modlib/notify/api.py +33 -14
  32. lino/modlib/notify/mixins.py +16 -1
  33. lino/modlib/notify/models.py +10 -25
  34. lino/modlib/printing/mixins.py +1 -1
  35. lino/modlib/publisher/models.py +55 -6
  36. lino/modlib/publisher/ui.py +3 -3
  37. lino/modlib/publisher/views.py +9 -2
  38. lino/modlib/system/models.py +1 -1
  39. lino/modlib/uploads/mixins.py +1 -1
  40. lino/modlib/uploads/models.py +2 -2
  41. lino/modlib/users/actions.py +2 -4
  42. lino/modlib/users/models.py +13 -19
  43. lino/modlib/users/ui.py +1 -1
  44. lino/sphinxcontrib/logo/templates/part-of-synodalsoft.html +0 -1
  45. lino/utils/format_date.py +10 -5
  46. lino/utils/media.py +16 -31
  47. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/METADATA +1 -1
  48. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/RECORD +51 -52
  49. lino/management/commands/monitor.py +0 -160
  50. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/WHEEL +0 -0
  51. {lino-25.4.1.dist-info → lino-25.4.3.dist-info}/licenses/AUTHORS.rst +0 -0
  52. {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=1)
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
- return d.message
77
+ if d.message != expected:
78
+ self.fail(f"Expected {expected} but got {d.message} ({d})")
78
79
 
79
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
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
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
83
+ login("bad", "Failed to sign in as robin.")
83
84
  self.assertEqual(rec.login_failures, 2)
84
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
85
+ login("bad", "Failed to sign in as robin.")
85
86
  self.assertEqual(rec.login_failures, 3)
86
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
87
+ login("bad", "Failed to sign in as robin.")
87
88
  self.assertEqual(rec.login_failures, 4)
88
- self.assertEqual(
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
- self.assertEqual(
97
- login(dd.plugins.users.demo_password), "Too many authentication failures from 127.0.0.1"
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
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
104
+ login("bad", "Failed to sign in as robin.")
107
105
  self.assertEqual(rec.login_failures, 5)
108
- self.assertEqual(
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(1.5)
114
- self.assertEqual(
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
- self.assertEqual(login("bad"), "Failed to sign in as robin.")
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-2021 Rumma & Ko Ltd
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 = timezone.now()
119
+ self.modified = settings.SITE.now()
101
120
 
102
121
 
103
122
  class Created(model.Model):
104
123
  """
105
- Adds a timestamp field which holds the creation time of every
106
- individual database object.
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 object was created.
129
+ The time when this :term:`database row` was created.
111
130
 
112
- Does nut use Django's `auto_now` and `auto_now_add` features
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 = timezone.now()
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)
@@ -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
- return super(Registrable, self).disabled_fields(ar)
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.
@@ -119,8 +119,8 @@ class About(EmptyTable):
119
119
  packages = set(["django"])
120
120
 
121
121
  items.append(
122
- E.li(gettext("Server timestamp"), " : ", E.b(dtfmt(site.lino_version)))
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])
@@ -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=timezone.now(),
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=timezone.now() - days)
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):
@@ -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:
@@ -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:
@@ -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-2020 Rumma & Ko Ltd
1
+ # Copyright 2014-2025 Rumma & Ko Ltd
2
2
  # License: GNU Affero General Public License v3 (see file COPYING for details)
3
- """This plugin installs a button to export any table to excel xls format.
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-2024 Rumma & Ko Ltd
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
 
@@ -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=106))
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=113),
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
  )
@@ -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 = MediaFile(False, *self.get_xml_file_parts())
58
+ xmlfile = self.xmlfile
49
59
  # xmlfile = Path(settings.MEDIA_ROOT, *parts)
50
- ar.logger.info("Make %s from %s ...", xmlfile.path, self)
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>")
@@ -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 = timezone.now()
54
- obj.message = "{} requested to run this task at {}.".format(
55
- ar.get_user(), dd.ftl(timezone.now())
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
- self.last_start_time = timezone.now()
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
- self.last_end_time = timezone.now()
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.ftf(self.last_start_time))
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.ftf(self.requested_at))
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.ftf(next_time))
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 = timezone.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
- if (to_sleep := (next_time - timezone.now()).total_seconds()) <= 0:
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)
@@ -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:
@@ -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):