lino 25.4.3__py3-none-any.whl → 25.4.5__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/core/fields.py +4 -2
- lino/core/kernel.py +4 -1
- lino/core/requests.py +1 -1
- lino/core/site.py +58 -68
- lino/core/utils.py +7 -2
- lino/help_texts.py +0 -2
- lino/mixins/registrable.py +5 -4
- lino/modlib/checkdata/models.py +9 -1
- lino/modlib/jinja/mixins.py +2 -0
- lino/modlib/linod/management/commands/linod.py +5 -20
- lino/modlib/linod/mixins.py +0 -39
- lino/modlib/linod/routing.py +49 -46
- lino/modlib/publisher/choicelists.py +41 -20
- lino/modlib/publisher/config/publisher/page.pub.html +30 -0
- lino/modlib/publisher/fixtures/demo2.py +12 -0
- lino/modlib/publisher/fixtures/std.py +2 -1
- lino/modlib/publisher/fixtures/synodalworld.py +30 -0
- lino/modlib/publisher/mixins.py +7 -0
- lino/modlib/publisher/models.py +28 -3
- lino/modlib/publisher/ui.py +2 -2
- lino/static/bootstrap.css +1 -1
- lino/utils/__init__.py +1 -56
- lino/utils/dbfreader.py +64 -32
- lino/utils/dbhash.py +3 -2
- lino/utils/sums.py +75 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/METADATA +1 -1
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/RECORD +31 -28
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/WHEEL +0 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.4.3.dist-info → lino-25.4.5.dist-info}/licenses/COPYING +0 -0
lino/__init__.py
CHANGED
@@ -31,7 +31,7 @@ from django import VERSION
|
|
31
31
|
from django.apps import AppConfig
|
32
32
|
from django.conf import settings
|
33
33
|
import warnings
|
34
|
-
__version__ = '25.4.
|
34
|
+
__version__ = '25.4.5'
|
35
35
|
|
36
36
|
# import setuptools # avoid UserWarning "Distutils was imported before Setuptools"?
|
37
37
|
|
lino/core/fields.py
CHANGED
@@ -163,7 +163,7 @@ class PriceField(models.DecimalField):
|
|
163
163
|
"""
|
164
164
|
A thin wrapper around Django's `DecimalField
|
165
165
|
<https://docs.djangoproject.com/en/5.0/ref/models/fields/#decimalfield>`_
|
166
|
-
|
166
|
+
with price-like default values for `decimal_places`, `max_length` and
|
167
167
|
`max_digits`.
|
168
168
|
"""
|
169
169
|
|
@@ -656,8 +656,10 @@ class VirtualField(FakeField):
|
|
656
656
|
self.model = model
|
657
657
|
self.name = name
|
658
658
|
self.attname = name
|
659
|
+
# if getattr(self.return_type, "model", False):
|
659
660
|
if hasattr(self.return_type, "model"):
|
660
|
-
# logger.info("20200425 return_type for virtual
|
661
|
+
# logger.info("20200425 return_type for virtual "
|
662
|
+
# "field %s has a model %s (not %s)", self, self.return_type.model, model)
|
661
663
|
return
|
662
664
|
self.return_type.model = VirtualModel(model)
|
663
665
|
self.return_type.column = None
|
lino/core/kernel.py
CHANGED
@@ -37,6 +37,7 @@ from django.utils.translation import gettext_lazy as _
|
|
37
37
|
from django.core.exceptions import PermissionDenied, ValidationError
|
38
38
|
from django.db.utils import DatabaseError
|
39
39
|
from django.db import IntegrityError
|
40
|
+
from django.utils.log import DEFAULT_LOGGING
|
40
41
|
|
41
42
|
from django.db import models
|
42
43
|
|
@@ -162,7 +163,9 @@ class Kernel(object):
|
|
162
163
|
# logger.info("20231016 kernel_startup (%s)", site._history_aware_logging)
|
163
164
|
# print("20231019 kernel_startup()", site, site._history_aware_logging)
|
164
165
|
|
165
|
-
if site._history_aware_logging:
|
166
|
+
# if site._history_aware_logging:
|
167
|
+
# if not is_devserver():
|
168
|
+
if "file" in DEFAULT_LOGGING['handlers']:
|
166
169
|
if len(sys.argv) == 0:
|
167
170
|
process_name = "WSGI"
|
168
171
|
else:
|
lino/core/requests.py
CHANGED
@@ -2031,9 +2031,9 @@ class ActionRequest(BaseRequest):
|
|
2031
2031
|
if w is None:
|
2032
2032
|
WARNINGS_LOGGED[e] = True
|
2033
2033
|
# raise
|
2034
|
-
# logger.exception(e)
|
2035
2034
|
logger.warning(f"Error while executing {repr(self)}: {e}\n"
|
2036
2035
|
"(Subsequent warnings will be silenced.)")
|
2036
|
+
logger.exception(e)
|
2037
2037
|
|
2038
2038
|
if self._data_iterator is None:
|
2039
2039
|
raise Exception(f"No data iterator for {self}")
|
lino/core/site.py
CHANGED
@@ -36,12 +36,13 @@ from lino.utils.html import E, tostring
|
|
36
36
|
from lino.core.plugin import Plugin
|
37
37
|
from lino.utils import AttrDict, date_offset, i2d, buildurl
|
38
38
|
|
39
|
+
|
39
40
|
NO_REMOTE_AUTH = True
|
40
41
|
# 20240518 We have only one production site still using remote http
|
41
42
|
# authentication, and they will migrate to sessions-based auth with their next
|
42
43
|
# upgrade.
|
43
44
|
|
44
|
-
ASYNC_LOGGING = False
|
45
|
+
# ASYNC_LOGGING = False
|
45
46
|
# This is to fix the issue that the "started" and "ended" messages are not logged.
|
46
47
|
# But setting this to True causes #4986 (Unable to configure handler 'mail_admins')
|
47
48
|
# because since 20230529 we called logging..config.dictConfig() during
|
@@ -368,14 +369,14 @@ class Site(object):
|
|
368
369
|
csv_params = dict()
|
369
370
|
|
370
371
|
# attributes documented in book/docs/opics/loggin.rst:
|
371
|
-
_history_aware_logging =
|
372
|
+
_history_aware_logging = True
|
372
373
|
log_each_action_request = False
|
374
|
+
default_loglevel = "INFO"
|
373
375
|
logger_filename = "lino.log"
|
374
376
|
logger_format = (
|
375
377
|
"%(asctime)s %(levelname)s [%(name)s %(process)d %(thread)d] : %(message)s"
|
376
378
|
)
|
377
379
|
auto_configure_logger_names = "atelier lino"
|
378
|
-
log_sock_path = None
|
379
380
|
|
380
381
|
# appy_params = dict(ooPort=8100)
|
381
382
|
appy_params = dict(
|
@@ -559,21 +560,13 @@ class Site(object):
|
|
559
560
|
break
|
560
561
|
|
561
562
|
if self.master_site is None:
|
562
|
-
# cache_root = os.environ.get("LINO_CACHE_ROOT", None)
|
563
|
-
# if cache_root:
|
564
|
-
# # TODO: deprecate
|
565
|
-
# cr = Path(cache_root).absolute()
|
566
|
-
# if not cr.exists():
|
567
|
-
# msg = "LINO_CACHE_ROOT ({0}) does not exist!".format(cr)
|
568
|
-
# raise Exception(msg)
|
569
|
-
# self.site_dir = (cr / self.project_name).resolve()
|
570
|
-
# self.setup_cache_directory()
|
571
|
-
# else:
|
572
|
-
# self.site_dir = self.project_dir
|
573
563
|
self.site_dir = self.project_dir
|
574
|
-
|
575
|
-
|
576
|
-
|
564
|
+
self.django_settings.update(DATABASES={
|
565
|
+
"default": {
|
566
|
+
"ENGINE": "django.db.backends.sqlite3",
|
567
|
+
"NAME": str(self.site_dir / "default.db")
|
568
|
+
}
|
569
|
+
})
|
577
570
|
else:
|
578
571
|
self.site_dir = self.master_site.site_dir
|
579
572
|
self._history_aware_logging = self.master_site._history_aware_logging
|
@@ -605,31 +598,36 @@ class Site(object):
|
|
605
598
|
self.startup_time = datetime.datetime.now()
|
606
599
|
|
607
600
|
def setup_logging(self):
|
608
|
-
# documented in book/docs/
|
601
|
+
# documented in book/docs/topics/logging.rst
|
609
602
|
|
610
|
-
if
|
603
|
+
if self.auto_configure_logger_names is None:
|
611
604
|
return
|
612
605
|
|
613
|
-
if len(logging.root.handlers) > 0:
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
606
|
+
# if len(logging.root.handlers) > 0:
|
607
|
+
#
|
608
|
+
# # Logging has been configured by something else. This can happen
|
609
|
+
# # when Site is instantiated a second time. Or accidentaly (e.g. when
|
610
|
+
# # you call logging.basicConfig() in the settings.py), Or when some
|
611
|
+
# # testing environment runs multiple doctests in a same process. We
|
612
|
+
# # don't care, we restart configuration from scratch.
|
613
|
+
#
|
614
|
+
# for handler in logging.root.handlers[:]:
|
615
|
+
# logging.root.removeHandler(handler)
|
621
616
|
|
622
617
|
from django.utils.log import DEFAULT_LOGGING
|
623
618
|
|
624
619
|
d = DEFAULT_LOGGING
|
625
620
|
|
626
|
-
if d.get("logger_ok", False):
|
627
|
-
|
628
|
-
|
621
|
+
# if d.get("logger_ok", False):
|
622
|
+
# # raise Exception("20231017")
|
623
|
+
# return
|
629
624
|
|
630
|
-
level = os.environ.get("LINO_LOGLEVEL",
|
625
|
+
level = os.environ.get("LINO_LOGLEVEL", self.default_loglevel).upper()
|
631
626
|
file_level = os.environ.get("LINO_FILE_LOGLEVEL", level).upper()
|
632
|
-
|
627
|
+
sql_level = os.environ.get("LINO_SQL_LOGLEVEL", level).upper()
|
628
|
+
|
629
|
+
min_level = min(*[getattr(logging, k) for k in (
|
630
|
+
level, file_level, sql_level)])
|
633
631
|
|
634
632
|
# print("20231017 level is", level)
|
635
633
|
|
@@ -650,22 +648,13 @@ class Site(object):
|
|
650
648
|
|
651
649
|
# when Site is instantiated several times, we keep the existing file handler
|
652
650
|
# print("20231016", self.logger_filename, handlers.keys())
|
653
|
-
if
|
651
|
+
if "file" not in handlers:
|
654
652
|
logdir = self.site_dir / "log"
|
655
653
|
if logdir.is_dir():
|
656
654
|
self._history_aware_logging = True
|
657
655
|
log_file_path = logdir / self.logger_filename
|
658
656
|
# print("20231019 logging", file_level, "to", log_file_path)
|
659
|
-
|
660
|
-
if self.log_sock_path.exists():
|
661
|
-
# print("20231019 log via socket server")
|
662
|
-
handlers["file"] = {
|
663
|
-
"class": "lino.core.site.LinoSocketHandler",
|
664
|
-
"host": str(self.log_sock_path),
|
665
|
-
"port": None,
|
666
|
-
"level": file_level,
|
667
|
-
}
|
668
|
-
else:
|
657
|
+
if True:
|
669
658
|
# print("20231019 log directly to file")
|
670
659
|
formatters = d.setdefault("formatters", {})
|
671
660
|
formatters.setdefault(
|
@@ -679,6 +668,15 @@ class Site(object):
|
|
679
668
|
"encoding": "UTF-8",
|
680
669
|
"formatter": "verbose",
|
681
670
|
}
|
671
|
+
else:
|
672
|
+
try:
|
673
|
+
from systemd.journal import JournalHandler
|
674
|
+
handlers["file"] = {
|
675
|
+
"class": "systemd.journal.JournalHandler",
|
676
|
+
"SYSLOG_IDENTIFIER": str(self.project_name),
|
677
|
+
}
|
678
|
+
except ImportError:
|
679
|
+
pass
|
682
680
|
|
683
681
|
# when a file handler exists, we have the loggers use it even if this
|
684
682
|
# instance didn't create it:
|
@@ -689,9 +687,10 @@ class Site(object):
|
|
689
687
|
# if name not in d['loggers']:
|
690
688
|
d["loggers"][name] = loggercfg
|
691
689
|
|
692
|
-
|
693
|
-
|
694
|
-
|
690
|
+
if sql_level != level:
|
691
|
+
dblogger = d["loggers"].setdefault("django.db.backends", {})
|
692
|
+
dblogger["level"] = sql_level
|
693
|
+
dblogger["handlers"] = loggercfg["handlers"]
|
695
694
|
|
696
695
|
# # https://code.djangoproject.com/ticket/30554
|
697
696
|
# logger = d['loggers'].setdefault('django.utils.autoreload', {})
|
@@ -702,34 +701,25 @@ class Site(object):
|
|
702
701
|
# if item not in ['linod', 'root']:
|
703
702
|
# d['loggers'][item]['propagate'] = True
|
704
703
|
|
705
|
-
if ASYNC_LOGGING:
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
else:
|
717
|
-
|
704
|
+
# if ASYNC_LOGGING:
|
705
|
+
# config = d.copy()
|
706
|
+
#
|
707
|
+
# try:
|
708
|
+
# logging.config.dictConfig(config)
|
709
|
+
# # logging.config.dictConfig(d)
|
710
|
+
# finally:
|
711
|
+
# d.clear()
|
712
|
+
# # d["logger_ok"] = True
|
713
|
+
# d["version"] = 1
|
714
|
+
# d["disable_existing_loggers"] = False
|
715
|
+
# else:
|
716
|
+
# d["logger_ok"] = True
|
718
717
|
# self.update_settings(LOGGING=d)
|
719
718
|
# pprint(d)
|
720
719
|
# print("20161126 Site %s " % d['loggers'].keys())
|
721
720
|
# import yaml
|
722
721
|
# print("20231019", yaml.dump(d))
|
723
722
|
|
724
|
-
def get_database_settings(self):
|
725
|
-
if self.site_dir is None:
|
726
|
-
pass # raise Exception("20160516 No site_dir")
|
727
|
-
else:
|
728
|
-
dbname = self.site_dir / "default.db"
|
729
|
-
return {
|
730
|
-
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": str(dbname)}
|
731
|
-
}
|
732
|
-
|
733
723
|
def get_anonymous_user(self):
|
734
724
|
# The code below works even when users is not installed
|
735
725
|
from lino.modlib.users.choicelists import UserTypes
|
lino/core/utils.py
CHANGED
@@ -37,6 +37,9 @@ get_models = apps.get_models
|
|
37
37
|
|
38
38
|
validate_url = URLValidator()
|
39
39
|
|
40
|
+
DEVSERVER_COMMANDS = {
|
41
|
+
"runserver", "testserver", "test", "demotest", "makescreenshots", "shell"}
|
42
|
+
|
40
43
|
|
41
44
|
def djangoname(o):
|
42
45
|
return o.__module__.split(".")[-2] + "." + o.__name__
|
@@ -144,6 +147,8 @@ def is_devserver():
|
|
144
147
|
|
145
148
|
"""
|
146
149
|
# ~ print 20130315, sys.argv[1]
|
150
|
+
if settings.DEBUG:
|
151
|
+
return True # doctest under pytest
|
147
152
|
if sys.argv[0].startswith("-"):
|
148
153
|
return True # doctest under pytest
|
149
154
|
if len(sys.argv) <= 1:
|
@@ -153,9 +158,9 @@ def is_devserver():
|
|
153
158
|
# if sys.argv[0].endswith("doctest.py") or sys.argv[0].endswith("doctest_utf8.py"):
|
154
159
|
if sys.argv[0].endswith("doctest.py") or sys.argv[0].endswith("pytest"):
|
155
160
|
return True
|
156
|
-
if sys.argv[1] in
|
161
|
+
if sys.argv[1] in DEVSERVER_COMMANDS:
|
157
162
|
return True
|
158
|
-
# print(sys.argv
|
163
|
+
# print(sys.argv)
|
159
164
|
return False
|
160
165
|
|
161
166
|
|
lino/help_texts.py
CHANGED
@@ -182,8 +182,6 @@ help_texts = {
|
|
182
182
|
'lino.utils.IncompleteDate' : _("""Naive representation of a potentially incomplete gregorian date."""),
|
183
183
|
'lino.utils.IncompleteDate.parse' : _("""Parse the given string and return an IncompleteDate object."""),
|
184
184
|
'lino.utils.IncompleteDate.get_age' : _("""Return age in years as integer."""),
|
185
|
-
'lino.utils.SumCollector' : _("""A dictionary of sums to be collected using an arbitrary key."""),
|
186
|
-
'lino.utils.SumCollector.collect' : _("""Add the given value to the sum at the given key k."""),
|
187
185
|
'lino.utils.MissingRow' : _("""Represents a database row that is expected to exist but doesn’t."""),
|
188
186
|
'lino.utils.addressable.Addressable' : _("""General mixin (not only for Django models) to encapsulate the generating of “traditional” (“snail”) mail addresses."""),
|
189
187
|
'lino.utils.addressable.Addressable.address_person_lines' : _("""Yield one or more text lines, one for each line of the person part."""),
|
lino/mixins/registrable.py
CHANGED
@@ -1,18 +1,18 @@
|
|
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 defines the :class:`Registable` model mixin.
|
6
6
|
"""
|
7
7
|
|
8
8
|
from django.db import models
|
9
|
-
from django.utils.translation import gettext_lazy as _
|
9
|
+
# from django.utils.translation import gettext_lazy as _
|
10
10
|
|
11
|
+
from lino import logger
|
11
12
|
from lino.core import model
|
12
|
-
from lino.core.actions import Action
|
13
|
+
# from lino.core.actions import Action
|
13
14
|
from lino.core.workflows import ChangeStateAction
|
14
15
|
from lino.core.exceptions import ChangedAPI
|
15
|
-
|
16
16
|
from lino.core.workflows import State
|
17
17
|
|
18
18
|
|
@@ -180,6 +180,7 @@ class Registrable(model.Model):
|
|
180
180
|
state_field = self.workflow_state_field
|
181
181
|
target_state = state_field.choicelist.draft
|
182
182
|
self.set_workflow_state(ar, state_field, target_state)
|
183
|
+
logger.warning("%s deregisters %s", ar.get_user(), self)
|
183
184
|
|
184
185
|
# no longer needed after 20170826
|
185
186
|
# @classmethod
|
lino/modlib/checkdata/models.py
CHANGED
@@ -21,6 +21,9 @@ from lino.api import dd, rt, _
|
|
21
21
|
from .choicelists import Checker, Checkers
|
22
22
|
from .roles import CheckdataUser
|
23
23
|
|
24
|
+
MAX_LENGTH = 250
|
25
|
+
MORE = " (...)"
|
26
|
+
|
24
27
|
|
25
28
|
class CheckerAction(dd.Action):
|
26
29
|
fix_them = False
|
@@ -150,7 +153,7 @@ class Message(Controllable, UserAuthored):
|
|
150
153
|
checker = Checkers.field(verbose_name=_("Checker"))
|
151
154
|
# severity = Severities.field()
|
152
155
|
# feedback = Feedbacks.field(blank=True)
|
153
|
-
message = models.CharField(_("Message text"), max_length=
|
156
|
+
message = models.CharField(_("Message text"), max_length=MAX_LENGTH)
|
154
157
|
# fixable = models.BooleanField(_("Fixable"), default=False)
|
155
158
|
|
156
159
|
update_problem = UpdateMessage()
|
@@ -166,6 +169,11 @@ class Message(Controllable, UserAuthored):
|
|
166
169
|
def __str__(self):
|
167
170
|
return self.message
|
168
171
|
|
172
|
+
def full_clean(self):
|
173
|
+
if len(self.message) > MAX_LENGTH:
|
174
|
+
self.message = self.message[:MAX_LENGTH - len(MORE)] + MORE
|
175
|
+
super().full_clean()
|
176
|
+
|
169
177
|
@classmethod
|
170
178
|
def get_simple_parameters(cls):
|
171
179
|
for p in super(Message, cls).get_simple_parameters():
|
lino/modlib/jinja/mixins.py
CHANGED
@@ -11,6 +11,7 @@ from django.conf import settings
|
|
11
11
|
from django.utils.html import mark_safe, escape
|
12
12
|
|
13
13
|
from lino.api import dd, _
|
14
|
+
from lino.utils.sums import myround
|
14
15
|
from lino.utils.xml import validate_xml
|
15
16
|
from lino.utils.media import MediaFile
|
16
17
|
|
@@ -51,6 +52,7 @@ class XMLMaker(dd.Model):
|
|
51
52
|
context = self.get_printable_context(ar)
|
52
53
|
context.update(xml_element=xml_element)
|
53
54
|
context.update(base64=base64)
|
55
|
+
context.update(myround=myround)
|
54
56
|
xml = tpl.render(**context)
|
55
57
|
# parts = [
|
56
58
|
# dd.plugins.accounting.xml_media_dir,
|
@@ -3,13 +3,11 @@
|
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
5
|
# import time
|
6
|
-
import os
|
7
6
|
import asyncio
|
8
|
-
|
9
7
|
from django.conf import settings
|
10
8
|
from django.core.management import BaseCommand, call_command
|
11
|
-
from lino.api import dd
|
12
|
-
from lino.modlib.linod.mixins import
|
9
|
+
from lino.api import dd
|
10
|
+
from lino.modlib.linod.mixins import start_task_runner
|
13
11
|
from lino.core.requests import BaseRequest
|
14
12
|
|
15
13
|
if dd.plugins.linod.use_channels:
|
@@ -33,19 +31,6 @@ class Command(BaseCommand):
|
|
33
31
|
# default=False)
|
34
32
|
|
35
33
|
def handle(self, *args, **options):
|
36
|
-
log_sock_path = settings.SITE.log_sock_path
|
37
|
-
|
38
|
-
if log_sock_path and log_sock_path.exists():
|
39
|
-
if options.get("force"):
|
40
|
-
log_sock_path.unlink()
|
41
|
-
else:
|
42
|
-
raise Exception(
|
43
|
-
f"log socket already exists: {log_sock_path}\n"
|
44
|
-
"It's probable that a worker process is already running. "
|
45
|
-
"Try: 'ps awx | grep linod' OR 'sudo supervisorctl status | grep worker'\n"
|
46
|
-
"Or the last instance of the worker process did not finish properly. "
|
47
|
-
"In that case remove the file and run this command again."
|
48
|
-
)
|
49
34
|
|
50
35
|
if not dd.plugins.linod.use_channels:
|
51
36
|
# print("20240424 Run Lino daemon without channels")
|
@@ -57,8 +42,8 @@ class Command(BaseCommand):
|
|
57
42
|
except settings.SITE.user_model.DoesNotExist:
|
58
43
|
u = None
|
59
44
|
ar = BaseRequest(user=u)
|
60
|
-
#
|
61
|
-
await
|
45
|
+
# await asyncio.gather(start_log_server(), start_task_runner(ar))
|
46
|
+
await start_task_runner(ar)
|
62
47
|
# t1 = asyncio.create_task(settings.SITE.start_log_server())
|
63
48
|
# t2 = asyncio.create_task(start_task_runner(ar))
|
64
49
|
# await t1
|
@@ -84,7 +69,7 @@ class Command(BaseCommand):
|
|
84
69
|
async def initiate_linod():
|
85
70
|
layer = get_channel_layer()
|
86
71
|
# if log_sock_path is not None:
|
87
|
-
await layer.send(CHANNEL_NAME, {"type": "log.server"})
|
72
|
+
# await layer.send(CHANNEL_NAME, {"type": "log.server"})
|
88
73
|
# await asyncio.sleep(1)
|
89
74
|
await layer.send(CHANNEL_NAME, {"type": "run.background.tasks"})
|
90
75
|
|
lino/modlib/linod/mixins.py
CHANGED
@@ -291,42 +291,3 @@ async def start_task_runner(ar=None, max_count=None):
|
|
291
291
|
continue
|
292
292
|
await ar.adebug("Let task runner sleep for %s seconds.", to_sleep)
|
293
293
|
await asyncio.sleep(to_sleep)
|
294
|
-
|
295
|
-
|
296
|
-
class LogReceiver(asyncio.Protocol):
|
297
|
-
# def connection_made(self, transport):
|
298
|
-
# print("20231019 connection_made", transport)
|
299
|
-
|
300
|
-
def data_received(self, data: bytes):
|
301
|
-
data = pickle.loads(
|
302
|
-
data[4:]
|
303
|
-
) # first four bytes gives the size of the rest of the data
|
304
|
-
record = logging.makeLogRecord(data)
|
305
|
-
# print("20231019 data_received", record)
|
306
|
-
# 20231019 server_logger.handle(record)
|
307
|
-
logger.handle(record)
|
308
|
-
|
309
|
-
|
310
|
-
async def start_log_server():
|
311
|
-
# 'log.server' in linod.py
|
312
|
-
site = settings.SITE
|
313
|
-
log_sock_path = site.log_sock_path
|
314
|
-
if log_sock_path is None:
|
315
|
-
logger.info(
|
316
|
-
"No log server because there is no directory %s.", site.site_dir / "log"
|
317
|
-
)
|
318
|
-
return
|
319
|
-
if log_sock_path.exists():
|
320
|
-
raise Exception("Cannot start log server when socket file exists.")
|
321
|
-
logger.info("Log server starts listening on %s", log_sock_path)
|
322
|
-
|
323
|
-
def remove_sock_file():
|
324
|
-
logger.info("Remove socket file %s", site.log_sock_path)
|
325
|
-
site.log_sock_path.unlink(missing_ok=True)
|
326
|
-
|
327
|
-
site.register_shutdown_task(remove_sock_file)
|
328
|
-
loop = asyncio.get_running_loop()
|
329
|
-
server = await loop.create_unix_server(LogReceiver, log_sock_path)
|
330
|
-
# await server.serve_forever()
|
331
|
-
async with server:
|
332
|
-
await server.serve_forever()
|
lino/modlib/linod/routing.py
CHANGED
@@ -1,67 +1,70 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2022 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
|
-
from django.
|
5
|
+
from django.conf import settings
|
6
6
|
from django.core.asgi import get_asgi_application
|
7
|
-
from django.utils.functional import LazyObject
|
8
7
|
|
9
|
-
|
10
|
-
from channels.db import database_sync_to_async
|
11
|
-
from channels.sessions import SessionMiddlewareStack
|
12
|
-
from channels.routing import ProtocolTypeRouter, URLRouter, ChannelNameRouter
|
8
|
+
if settings.SITE.plugins.linod.use_channels:
|
13
9
|
|
14
|
-
from
|
15
|
-
from
|
10
|
+
from django.urls import re_path
|
11
|
+
from django.utils.functional import LazyObject
|
12
|
+
from channels.middleware import BaseMiddleware
|
13
|
+
from channels.db import database_sync_to_async
|
14
|
+
from channels.sessions import SessionMiddlewareStack
|
15
|
+
from channels.routing import ProtocolTypeRouter, URLRouter, ChannelNameRouter
|
16
16
|
|
17
|
-
from .
|
18
|
-
from .consumers import
|
17
|
+
from lino.core.auth import get_user
|
18
|
+
from lino.modlib.notify.consumers import ClientConsumer
|
19
19
|
|
20
|
+
from .utils import CHANNEL_NAME
|
21
|
+
from .consumers import LinodConsumer
|
20
22
|
|
21
|
-
class UserLazyObject(LazyObject):
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
class UserLazyObject(LazyObject):
|
24
|
+
"""
|
25
|
+
Throw a more useful error message when scope['user'] is accessed before it's resolved
|
26
|
+
"""
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
+
def _setup(self):
|
29
|
+
raise ValueError("Accessing scope user before it is ready.")
|
28
30
|
|
31
|
+
async def _get_user(scope):
|
32
|
+
class Wrapper:
|
33
|
+
def __init__(self, session):
|
34
|
+
self.session = session
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
def __init__(self, session):
|
33
|
-
self.session = session
|
36
|
+
r = Wrapper(scope["session"])
|
37
|
+
return await database_sync_to_async(get_user)(r)
|
34
38
|
|
35
|
-
|
36
|
-
|
39
|
+
class AuthMiddleware(BaseMiddleware):
|
40
|
+
def populate_scope(self, scope):
|
41
|
+
# Make sure we have a session
|
42
|
+
if "session" not in scope:
|
43
|
+
raise ValueError("AuthMiddleware cannot find session in scope.")
|
44
|
+
# Add it to the scope if it's not there already
|
45
|
+
if "user" not in scope:
|
46
|
+
scope["user"] = UserLazyObject()
|
37
47
|
|
48
|
+
async def resolve_scope(self, scope):
|
49
|
+
scope["user"]._wrapped = await _get_user(scope)
|
38
50
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
raise ValueError("AuthMiddleware cannot find session in scope.")
|
44
|
-
# Add it to the scope if it's not there already
|
45
|
-
if "user" not in scope:
|
46
|
-
scope["user"] = UserLazyObject()
|
51
|
+
async def __call__(self, scope, receive=None, send=None):
|
52
|
+
self.populate_scope(scope)
|
53
|
+
await self.resolve_scope(scope)
|
54
|
+
return await self.inner(scope, receive, send)
|
47
55
|
|
48
|
-
|
49
|
-
scope["user"]._wrapped = await _get_user(scope)
|
56
|
+
routes = [re_path(r"^WS/$", ClientConsumer.as_asgi())]
|
50
57
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
58
|
+
protocol_mapping = dict(
|
59
|
+
websocket=SessionMiddlewareStack(AuthMiddleware(URLRouter(routes))),
|
60
|
+
channel=ChannelNameRouter({CHANNEL_NAME: LinodConsumer.as_asgi()}),
|
61
|
+
http=get_asgi_application(),
|
62
|
+
)
|
55
63
|
|
64
|
+
application = ProtocolTypeRouter(protocol_mapping)
|
56
65
|
|
57
|
-
|
66
|
+
# raise Exception("20240424")
|
58
67
|
|
59
|
-
|
60
|
-
websocket=SessionMiddlewareStack(AuthMiddleware(URLRouter(routes))),
|
61
|
-
channel=ChannelNameRouter({CHANNEL_NAME: LinodConsumer.as_asgi()}),
|
62
|
-
http=get_asgi_application(),
|
63
|
-
)
|
68
|
+
else:
|
64
69
|
|
65
|
-
application =
|
66
|
-
|
67
|
-
# raise Exception("20240424")
|
70
|
+
application = get_asgi_application()
|