lino 25.4.0__py3-none-any.whl → 25.4.2__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 +19 -19
- lino/core/requests.py +24 -9
- lino/core/site.py +101 -68
- lino/core/tables.py +27 -29
- lino/help_texts.py +7 -3
- lino/mixins/__init__.py +27 -30
- lino/modlib/about/models.py +1 -1
- lino/modlib/changes/models.py +2 -2
- 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/jinja/mixins.py +4 -1
- lino/modlib/linod/mixins.py +17 -12
- lino/modlib/memo/parser.py +4 -4
- lino/modlib/notify/mixins.py +14 -0
- lino/modlib/notify/models.py +10 -25
- lino/modlib/uploads/mixins.py +1 -1
- 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.0.dist-info → lino-25.4.2.dist-info}/METADATA +1 -1
- {lino-25.4.0.dist-info → lino-25.4.2.dist-info}/RECORD +33 -33
- {lino-25.4.0.dist-info → lino-25.4.2.dist-info}/WHEEL +0 -0
- {lino-25.4.0.dist-info → lino-25.4.2.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.4.0.dist-info → lino-25.4.2.dist-info}/licenses/COPYING +0 -0
lino/help_texts.py
CHANGED
@@ -153,13 +153,13 @@ help_texts = {
|
|
153
153
|
'lino.modlib.memo.Plugin' : _("""Base class for this plugin."""),
|
154
154
|
'lino.modlib.memo.Plugin.parser' : _("""An instance of lino.modlib.memo.parser.Parser."""),
|
155
155
|
'lino.modlib.memo.Plugin.front_end' : _("""The front end to use when writing previews."""),
|
156
|
+
'lino.modlib.memo.parser.Suggester' : _("""Holds the configuration for the behaviour of a given “trigger”."""),
|
156
157
|
'lino.modlib.memo.parser.Parser' : _("""The memo parser."""),
|
157
158
|
'lino.modlib.memo.parser.Parser.add_suggester' : _("""Add a Suggester (see there for args and kwargs)."""),
|
158
|
-
'lino.modlib.memo.parser.Parser.get_referred_objects' : _("""Yield all database objects referred in the given text using a suggester."""),
|
159
|
-
'lino.modlib.memo.parser.Parser.parse' : _("""Parse the given string src, replacing memo commands by their result."""),
|
160
159
|
'lino.modlib.memo.parser.Parser.register_command' : _("""Register a memo command identified by the given text cmd."""),
|
161
160
|
'lino.modlib.memo.parser.Parser.register_django_model' : _("""Register the given string name as command for referring to database rows of the given Django database model model."""),
|
162
|
-
'lino.modlib.memo.parser.
|
161
|
+
'lino.modlib.memo.parser.Parser.get_referred_objects' : _("""Yield all database objects referred in the given text using a suggester."""),
|
162
|
+
'lino.modlib.memo.parser.Parser.parse' : _("""Parse the given string src, replacing memo commands by their result."""),
|
163
163
|
'lino.modlib.restful.Plugin' : _("""See /dev/plugins."""),
|
164
164
|
'lino.modlib.smtpd.Plugin' : _("""See /dev/plugins."""),
|
165
165
|
'lino.modlib.system.Plugin' : _("""See /dev/plugins."""),
|
@@ -253,6 +253,8 @@ help_texts = {
|
|
253
253
|
'lino.utils.jsgen.VisibleComponent' : _("""A visible component"""),
|
254
254
|
'lino.utils.jsgen.VisibleComponent.install_permission_handler' : _("""Define the allow_read handler used by get_view_permission(). This must be done only once, but after having configured debug_permissions and required_roles."""),
|
255
255
|
'lino.utils.media.MediaFile' : _("""Represents a file on the server below MEDIA_ROOT with two properties path and url."""),
|
256
|
+
'lino.utils.media.MediaFile.path' : _("""A pathlib.Path naming the file on the server’s file system."""),
|
257
|
+
'lino.utils.media.MediaFile.url' : _("""The URL to use for getting this file from a web client."""),
|
256
258
|
'lino.utils.mldbc.fields.BabelCharField' : _("""Define a variable number of CharField database fields, one for each language of your lino.core.site.Site.languages. See mldbc."""),
|
257
259
|
'lino.utils.mldbc.fields.BabelTextField' : _("""Used for the clones of the master field, one for each non-default language. See mldbc."""),
|
258
260
|
'lino.utils.mldbc.fields.LanguageField' : _("""A field that lets the user select a language from the available lino.core.site.Site.languages."""),
|
@@ -551,6 +553,8 @@ help_texts = {
|
|
551
553
|
'lino.modlib.jinja.XMLMaker.xml_file_name' : _("""The name of the XML file to generate. This file will be overwritten without asking. The name formatted with one name self in the context."""),
|
552
554
|
'lino.modlib.jinja.XMLMaker.xml_file_template' : _("""The name of a Jinja template to render for generating the XML content."""),
|
553
555
|
'lino.modlib.jinja.XMLMaker.xml_validator_file' : _("""The name of a “validator” to use for validating the XML content."""),
|
556
|
+
'lino.modlib.jinja.XMLMaker.get_xml_file' : _("""Get the name of the XML file to be generated for this database row."""),
|
557
|
+
'lino.modlib.jinja.XMLMaker.make_xml_file' : _("""Make the XML file for this database row."""),
|
554
558
|
'lino.modlib.memo.Previewable' : _("""Adds three rich text fields (lino.core.fields.RichTextField):"""),
|
555
559
|
'lino.modlib.memo.Previewable.body' : _("""An editable text body."""),
|
556
560
|
'lino.modlib.memo.Previewable.body_short_preview' : _("""A read-only preview of the first paragraph of body."""),
|
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,7 +116,7 @@ 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):
|
@@ -124,7 +143,7 @@ class Created(model.Model):
|
|
124
143
|
|
125
144
|
def save(self, *args, **kwargs):
|
126
145
|
if self.created is None and not settings.SITE.loading_from_dump:
|
127
|
-
self.created =
|
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/modlib/about/models.py
CHANGED
@@ -119,7 +119,7 @@ class About(EmptyTable):
|
|
119
119
|
packages = set(["django"])
|
120
120
|
|
121
121
|
items.append(
|
122
|
-
E.li(gettext("Server timestamp"), " : ", E.b(dtfmt(site.
|
122
|
+
E.li(gettext("Server timestamp"), " : ", E.b(dtfmt(site.lino_version)))
|
123
123
|
)
|
124
124
|
|
125
125
|
for p in site.installed_plugins:
|
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
|
@@ -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.
|
1484
|
+
yield "GEN_TIMESTAMP = %s;" % py2js(settings.SITE.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.
|
141
|
+
if lv is None or float(lv) == settings.SITE.lino_version:
|
142
142
|
return {}
|
143
143
|
# print("20201217", lv, settings.SITE.kernel.code_mtime)
|
144
144
|
cache.clear()
|
lino/modlib/jinja/mixins.py
CHANGED
@@ -35,6 +35,9 @@ class XMLMaker(dd.Model):
|
|
35
35
|
yield 'xml'
|
36
36
|
yield self.get_printable_target_stem() + ".xml"
|
37
37
|
|
38
|
+
def get_xml_file(self):
|
39
|
+
return MediaFile(False, *self.get_xml_file_parts())
|
40
|
+
|
38
41
|
def make_xml_file(self, ar):
|
39
42
|
renderer = settings.SITE.plugins.jinja.renderer
|
40
43
|
tpl = renderer.jinja_env.get_template(self.xml_file_template)
|
@@ -45,7 +48,7 @@ class XMLMaker(dd.Model):
|
|
45
48
|
# parts = [
|
46
49
|
# dd.plugins.accounting.xml_media_dir,
|
47
50
|
# self.xml_file_name.format(self=self)]
|
48
|
-
xmlfile =
|
51
|
+
xmlfile = self.get_xml_file()
|
49
52
|
# xmlfile = Path(settings.MEDIA_ROOT, *parts)
|
50
53
|
ar.logger.info("Make %s from %s ...", xmlfile.path, self)
|
51
54
|
xmlfile.path.parent.mkdir(exist_ok=True, parents=True)
|
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/memo/parser.py
CHANGED
@@ -13,6 +13,9 @@ TODO:
|
|
13
13
|
which would be empty for # and @ but "]" for memo commands.
|
14
14
|
|
15
15
|
"""
|
16
|
+
from etgen import etree
|
17
|
+
from django.db.models import Model
|
18
|
+
from django.conf import settings
|
16
19
|
from lino import logger
|
17
20
|
|
18
21
|
import re
|
@@ -24,9 +27,6 @@ import warnings
|
|
24
27
|
|
25
28
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
26
29
|
|
27
|
-
from django.conf import settings
|
28
|
-
from django.db.models import Model
|
29
|
-
from etgen import etree
|
30
30
|
|
31
31
|
# COMMAND_REGEX = re.compile(r"\[(\w+)\s*((?:[^[\]]|\[.*?\])*?)\]")
|
32
32
|
# ===...... .......=
|
@@ -139,7 +139,7 @@ class Parser:
|
|
139
139
|
for key in self.suggesters.keys()
|
140
140
|
]
|
141
141
|
)
|
142
|
-
return re.compile(r"([^\w])?([" + triggers + "])(\w+)")
|
142
|
+
return re.compile(r"([^\w])?([" + triggers + r"])(\w+)")
|
143
143
|
|
144
144
|
def register_command(self, cmdname, func: Callable[[Any, str, str, dict], None]):
|
145
145
|
"""Register a memo command identified by the given text `cmd`.
|
lino/modlib/notify/mixins.py
CHANGED
@@ -7,6 +7,20 @@ from lino.api import dd, rt, _
|
|
7
7
|
# PUBLIC_GROUP = "all_users_channel"
|
8
8
|
|
9
9
|
|
10
|
+
def get_updatables(instance, ar=None):
|
11
|
+
data = {
|
12
|
+
"actorIDs": instance.updatable_panels,
|
13
|
+
"pk": instance.pk,
|
14
|
+
"model": f"{instance._meta.app_label}.{instance.__class__.__name__}",
|
15
|
+
"mk": None, "master_model": None
|
16
|
+
}
|
17
|
+
if ar is None:
|
18
|
+
return data
|
19
|
+
if mi := ar.master_instance:
|
20
|
+
data.update(mk=mi.pk, master_model=f"{mi._meta.app_label}.{mi.__class__.__name__}")
|
21
|
+
return data
|
22
|
+
|
23
|
+
|
10
24
|
class ChangeNotifier(dd.Model):
|
11
25
|
|
12
26
|
class Meta:
|
lino/modlib/notify/models.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2011-
|
2
|
+
# Copyright 2011-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
5
|
# import json
|
@@ -9,7 +9,6 @@ from datetime import timedelta
|
|
9
9
|
|
10
10
|
from django.db import models
|
11
11
|
from django.conf import settings
|
12
|
-
from django.utils import timezone
|
13
12
|
from django.utils import translation
|
14
13
|
|
15
14
|
from lino import logger
|
@@ -25,6 +24,7 @@ from lino.modlib.users.mixins import UserAuthored, My
|
|
25
24
|
from lino.modlib.linod.choicelists import schedule_daily, schedule_often
|
26
25
|
from lino.modlib.office.roles import OfficeUser
|
27
26
|
|
27
|
+
from .mixins import get_updatables
|
28
28
|
from .choicelists import MessageTypes, MailModes
|
29
29
|
from .api import send_notification, NOTIFICATION, send_panel_update
|
30
30
|
|
@@ -57,7 +57,7 @@ class MarkAllSeen(dd.Action):
|
|
57
57
|
user=ar.get_user(), seen__isnull=True
|
58
58
|
)
|
59
59
|
for obj in qs:
|
60
|
-
obj.seen =
|
60
|
+
obj.seen = dd.now()
|
61
61
|
obj.save()
|
62
62
|
ar.success(
|
63
63
|
eval_js='window.top.document.querySelectorAll(".'
|
@@ -81,7 +81,7 @@ class MarkSeen(dd.Action):
|
|
81
81
|
|
82
82
|
def run_from_ui(self, ar):
|
83
83
|
for obj in ar.selected_rows:
|
84
|
-
obj.seen =
|
84
|
+
obj.seen = dd.now()
|
85
85
|
obj.save()
|
86
86
|
ar.success(refresh_all=True)
|
87
87
|
|
@@ -297,9 +297,9 @@ class Message(UserAuthored, Controllable, Created):
|
|
297
297
|
# dd.logger.info("20240902 %s", sender)
|
298
298
|
ar.send_email(subject, sender, body, [user.email])
|
299
299
|
for msg in messages:
|
300
|
-
msg.sent =
|
300
|
+
msg.sent = dd.now()
|
301
301
|
if dd.plugins.notify.mark_seen_when_sent:
|
302
|
-
msg.seen =
|
302
|
+
msg.seen = dd.now()
|
303
303
|
msg.save()
|
304
304
|
|
305
305
|
# def send_browser_message_for_all_users(self, user):
|
@@ -500,7 +500,7 @@ if remove_after:
|
|
500
500
|
def clear_seen_messages(ar):
|
501
501
|
Message = rt.models.notify.Message
|
502
502
|
qs = Message.objects.filter(
|
503
|
-
created__lt=
|
503
|
+
created__lt=dd.now() - timedelta(days=remove_after)
|
504
504
|
)
|
505
505
|
what = "notification messages older than {} days".format(remove_after)
|
506
506
|
if dd.plugins.notify.keep_unseen:
|
@@ -522,26 +522,11 @@ if remove_after:
|
|
522
522
|
|
523
523
|
|
524
524
|
@dd.receiver(dd.post_ui_save)
|
525
|
-
def notify_panels(sender, instance, ar, **kwargs):
|
525
|
+
def notify_panels(sender, instance, ar=None, **kwargs):
|
526
526
|
if (
|
527
527
|
not dd.get_plugin_setting("linod", "use_channels", False)
|
528
|
-
or not
|
529
|
-
or not hasattr(ar, "rqdata")
|
528
|
+
or not instance.updatable_panels
|
530
529
|
):
|
531
530
|
return
|
532
|
-
data =
|
533
|
-
"actorIDs": ups,
|
534
|
-
"pk": instance.pk,
|
535
|
-
"model": f"{instance._meta.app_label}.{instance.__class__.__name__}",
|
536
|
-
}
|
537
|
-
data.update(
|
538
|
-
**(
|
539
|
-
{
|
540
|
-
"mk": (mi := ar.master_instance).pk,
|
541
|
-
"master_model": f"{mi._meta.app_label}.{mi.__class__.__name__}",
|
542
|
-
}
|
543
|
-
if ar.master_instance
|
544
|
-
else {"mk": None, "master_model": None}
|
545
|
-
)
|
546
|
-
)
|
531
|
+
data = get_updatables(instance, ar)
|
547
532
|
send_panel_update(data)
|
lino/modlib/uploads/mixins.py
CHANGED
@@ -72,7 +72,7 @@ def make_uploaded_file(filename, src=None, upload_date=None):
|
|
72
72
|
def base64_to_image(imgstring):
|
73
73
|
type, file = imgstring.split(";base64,")
|
74
74
|
imgdata = base64.b64decode(file)
|
75
|
-
return make_captured_image(imgdata,
|
75
|
+
return make_captured_image(imgdata, dd.now(), ext=f".{type.split('/')[1]}")
|
76
76
|
|
77
77
|
|
78
78
|
def make_captured_image(imgdata, upload_date=None, filename=None, ext='.jpg'):
|
lino/modlib/users/actions.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2011-
|
2
|
+
# Copyright 2011-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
5
|
import datetime
|
@@ -9,7 +9,6 @@ from lino.utils.html import E, tostring
|
|
9
9
|
from django.db import models
|
10
10
|
from django.conf import settings
|
11
11
|
from django.http import HttpResponse
|
12
|
-
from django.utils import timezone
|
13
12
|
from django.utils.translation import gettext
|
14
13
|
from django.contrib.auth.password_validation import validate_password
|
15
14
|
from django.core.exceptions import ValidationError
|
@@ -115,7 +114,6 @@ class CreateAccount(dd.Action):
|
|
115
114
|
obj.full_clean()
|
116
115
|
obj.save()
|
117
116
|
|
118
|
-
|
119
117
|
ar.selected_rows = [obj]
|
120
118
|
recipients = ["{} <{}>".format(obj.get_full_name(), obj.email)]
|
121
119
|
send_welcome_email(ar, obj, recipients)
|
@@ -452,7 +450,7 @@ class SignOut(dd.Action):
|
|
452
450
|
def validate_sessions_limit(request):
|
453
451
|
if dd.plugins.users.active_sessions_limit == -1:
|
454
452
|
return
|
455
|
-
qs = rt.models.sessions.Session.objects.filter(expire_date__gt=
|
453
|
+
qs = rt.models.sessions.Session.objects.filter(expire_date__gt=dd.now())
|
456
454
|
if request.session.session_key:
|
457
455
|
qs = qs.exclude(session_key=request.session.session_key)
|
458
456
|
if qs.count() >= dd.plugins.users.active_sessions_limit:
|
lino/modlib/users/models.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2011-
|
2
|
+
# Copyright 2011-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
|
+
import random
|
6
|
+
import string
|
5
7
|
from datetime import timedelta
|
6
8
|
from django.db import models
|
7
9
|
from django.db.models import Q
|
@@ -9,15 +11,17 @@ from django.conf import settings
|
|
9
11
|
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
10
12
|
from django.utils import timezone
|
11
13
|
|
12
|
-
from lino.utils.html import E
|
13
14
|
from lino.api import dd, rt, _
|
14
15
|
from lino.core import userprefs
|
15
|
-
|
16
|
-
# from lino.core.fields import NullCharField
|
16
|
+
from lino.core.roles import Supervisor
|
17
17
|
from lino.core.roles import SiteAdmin
|
18
|
-
|
18
|
+
# from lino.core.fields import NullCharField
|
19
19
|
from lino.mixins import CreatedModified, Contactable
|
20
20
|
from lino.mixins import DateRange
|
21
|
+
from lino.modlib.about.choicelists import TimeZones, DateFormats
|
22
|
+
from lino.modlib.publisher.mixins import Publishable
|
23
|
+
from lino.modlib.about.models import About
|
24
|
+
from lino.utils.html import E
|
21
25
|
|
22
26
|
from .choicelists import UserTypes
|
23
27
|
from .mixins import UserAuthored # , TimezoneHolder
|
@@ -25,14 +29,8 @@ from .actions import ChangePassword, SignOut, CheckedSubmitInsert
|
|
25
29
|
from .actions import SendWelcomeMail, SignIn, ConnectAccount
|
26
30
|
from .actions import SendWelcomeMail, CreateAccount, ResetPassword, VerifyUser, VerifyMe
|
27
31
|
|
28
|
-
|
29
|
-
from lino.modlib.about.choicelists import TimeZones, DateFormats
|
30
|
-
from lino.modlib.publisher.mixins import Publishable
|
32
|
+
from .ui import *
|
31
33
|
|
32
|
-
from lino.core.roles import Supervisor
|
33
|
-
|
34
|
-
import random
|
35
|
-
import string
|
36
34
|
|
37
35
|
if multi_ledger := dd.is_installed("ledgers"):
|
38
36
|
from lino_xl.lib.ledgers.actions import SubscribeToLedger
|
@@ -100,7 +98,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
100
98
|
if dd.plugins.users.with_nickname:
|
101
99
|
nickname = models.CharField(_("Nickname"), max_length=50, blank=True)
|
102
100
|
else:
|
103
|
-
nickname = dd.DummyField()
|
101
|
+
nickname = dd.DummyField("")
|
104
102
|
first_name = models.CharField(_("First name"), max_length=30, blank=True)
|
105
103
|
last_name = models.CharField(_("Last name"), max_length=30, blank=True)
|
106
104
|
remarks = models.TextField(_("Remarks"), blank=True) # ,null=True)
|
@@ -156,7 +154,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
156
154
|
|
157
155
|
def must_verify(self):
|
158
156
|
self.verification_code = id_generator(12)
|
159
|
-
self.verification_code_sent_on =
|
157
|
+
self.verification_code_sent_on = dd.now()
|
160
158
|
|
161
159
|
def is_verified(self):
|
162
160
|
return not self.verification_code
|
@@ -170,7 +168,7 @@ class User(AbstractBaseUser, Contactable, CreatedModified, Publishable, DateRang
|
|
170
168
|
return (
|
171
169
|
self.verification_code_sent_on
|
172
170
|
+ timedelta(minutes=dd.plugins.users.verification_code_expires)
|
173
|
-
<
|
171
|
+
< dd.now()
|
174
172
|
)
|
175
173
|
|
176
174
|
def get_as_user(self):
|
@@ -451,8 +449,6 @@ class Permission(dd.Model):
|
|
451
449
|
abstract = True
|
452
450
|
|
453
451
|
|
454
|
-
from lino.modlib.about.models import About
|
455
|
-
|
456
452
|
About.sign_in = SignIn()
|
457
453
|
About.reset_password = ResetPassword()
|
458
454
|
About.verify_user = VerifyUser()
|
@@ -478,7 +474,5 @@ def setup_memo_commands(sender=None, **kwargs):
|
|
478
474
|
)
|
479
475
|
|
480
476
|
|
481
|
-
from .ui import *
|
482
|
-
|
483
477
|
if dd.get_plugin_setting("users", "third_party_authentication"):
|
484
478
|
Me.connect_account = ConnectAccount()
|
lino/modlib/users/ui.py
CHANGED
@@ -118,7 +118,7 @@ class AllUsers(Users):
|
|
118
118
|
class UsersOverview(Users):
|
119
119
|
required_roles = set([])
|
120
120
|
column_names = "username user_type language"
|
121
|
-
exclude =
|
121
|
+
exclude = models.Q(user_type="")
|
122
122
|
# abstract = not settings.SITE.is_demo_site
|
123
123
|
detail_layout = None
|
124
124
|
|
@@ -5,7 +5,6 @@ This website is part of the Synodalsoft project:
|
|
5
5
|
<a class="reference external" href="https://using.lino-framework.org">User Guide</a> |
|
6
6
|
<a class="reference external" href="https://hosting.lino-framework.org">Hosting Guide</a> |
|
7
7
|
<a class="reference external" href="https://dev.lino-framework.org">Developer Guide</a> |
|
8
|
-
<a class="reference external" href="https://community.lino-framework.org">Community Guide</a> |
|
9
8
|
<a class="reference external" href="https://luc.lino-framework.org">Luc’s blog</a>
|
10
9
|
</p><p>
|
11
10
|
<a href="https://www.synodalsoft.net">
|
lino/utils/format_date.py
CHANGED
@@ -109,13 +109,18 @@ def day_and_weekday(d):
|
|
109
109
|
# return d.strftime("%a%d")
|
110
110
|
|
111
111
|
|
112
|
-
def
|
113
|
-
# "format time
|
112
|
+
def fts(t):
|
113
|
+
# "format time short"
|
114
|
+
return t.strftime(settings.SITE.time_format_strftime)
|
115
|
+
|
116
|
+
|
117
|
+
def fdtl(t):
|
118
|
+
# "format datetime long"
|
114
119
|
return "{} {}".format(
|
115
120
|
t.strftime(settings.SITE.date_format_strftime),
|
116
121
|
t.strftime(settings.SITE.time_format_strftime))
|
117
122
|
|
118
123
|
|
119
|
-
def
|
120
|
-
# "format
|
121
|
-
return "{} ({})".format(
|
124
|
+
def fdtf(t):
|
125
|
+
# "format datetime full"
|
126
|
+
return "{} ({})".format(fdtl(t), naturaltime(t))
|