lino 25.7.2__py3-none-any.whl → 25.8.0__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/doctest.py +28 -2
- lino/core/actions.py +12 -1
- lino/core/atomizer.py +9 -8
- lino/core/auth/utils.py +9 -1
- lino/core/callbacks.py +2 -2
- lino/core/dbtables.py +4 -1
- lino/core/fields.py +0 -1
- lino/core/kernel.py +9 -19
- lino/core/model.py +11 -3
- lino/core/site.py +10 -14
- lino/core/store.py +3 -3
- lino/core/utils.py +14 -13
- lino/help_texts.py +9 -7
- lino/management/commands/ddt.py +60 -0
- lino/management/commands/dump2py.py +13 -25
- lino/management/commands/prep.py +1 -1
- lino/mixins/__init__.py +3 -2
- lino/mixins/{duplicable.py → clonable.py} +45 -50
- lino/mixins/dupable.py +2 -2
- lino/mixins/registrable.py +3 -3
- lino/mixins/sequenced.py +12 -14
- lino/modlib/comments/mixins.py +5 -6
- lino/modlib/comments/models.py +14 -12
- lino/modlib/comments/ui.py +0 -1
- lino/modlib/dupable/models.py +2 -2
- lino/modlib/memo/mixins.py +2 -0
- lino/modlib/notify/api.py +5 -0
- lino/modlib/printing/mixins.py +2 -2
- lino/modlib/publisher/models.py +15 -8
- lino/modlib/summaries/mixins.py +6 -4
- lino/modlib/users/actions.py +5 -0
- lino/sphinxcontrib/__init__.py +1 -1
- lino/sphinxcontrib/actordoc.py +1 -1
- lino/utils/cycler.py +6 -3
- lino/utils/diag.py +2 -2
- lino/utils/dpy.py +70 -59
- lino/utils/instantiator.py +21 -1
- {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/METADATA +1 -1
- {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/RECORD +43 -42
- {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/WHEEL +0 -0
- {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/licenses/COPYING +0 -0
@@ -1,13 +1,10 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
2
|
# Copyright 2012-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
|
-
"
|
5
|
-
[du'plikəblə] means "able to produce a duplicate
|
6
|
-
['duplikət], ['du:plikeit]".
|
7
|
-
|
8
|
-
"""
|
4
|
+
"See :doc:`/dev/duplicate`."
|
9
5
|
|
10
6
|
from django.utils.translation import gettext_lazy as _
|
7
|
+
from django.utils.text import format_lazy
|
11
8
|
|
12
9
|
from lino import logger
|
13
10
|
from lino.core import actions
|
@@ -16,16 +13,12 @@ from lino.core.diff import ChangeWatcher
|
|
16
13
|
# from lino.core.roles import Expert
|
17
14
|
|
18
15
|
|
19
|
-
class
|
20
|
-
""
|
21
|
-
|
22
|
-
This will call :meth:`lino.core.model.Model.on_duplicate` on the
|
23
|
-
new object and on related objects.
|
16
|
+
class CloneRow(actions.Action):
|
17
|
+
"See :doc:`/dev/duplicate`."
|
24
18
|
|
25
|
-
"""
|
26
|
-
|
27
|
-
label = "
|
28
|
-
# label = _("Duplicate")
|
19
|
+
# button_text = "⚇" # \u2687 "white circle with two dots"
|
20
|
+
button_text = "🗗" # OVERLAP (U+1F5D7)
|
21
|
+
label = _("Duplicate")
|
29
22
|
# icon_name = 'arrow_divide'
|
30
23
|
sort_index = 11
|
31
24
|
show_in_workflow = False
|
@@ -34,6 +27,11 @@ class Duplicate(actions.Action):
|
|
34
27
|
|
35
28
|
# required_roles = set([Expert])
|
36
29
|
|
30
|
+
def get_help_text(self, ba):
|
31
|
+
return format_lazy(
|
32
|
+
_("Insert a new {} as a copy of this."),
|
33
|
+
ba.actor.model._meta.verbose_name)
|
34
|
+
|
37
35
|
def get_action_view_permission(self, actor, user_type):
|
38
36
|
# the action is readonly because it doesn't write to the
|
39
37
|
# current object, but since it does modify the database we
|
@@ -48,30 +46,9 @@ class Duplicate(actions.Action):
|
|
48
46
|
return super().get_action_view_permission(actor, user_type)
|
49
47
|
|
50
48
|
def run_from_code(self, ar, **known_values):
|
49
|
+
# CloneSequenced uses known_values to set seqno to seqno + 1
|
51
50
|
obj = ar.selected_rows[0]
|
52
|
-
related =
|
53
|
-
for m, fk in obj._lino_ddh.fklist:
|
54
|
-
# print(fk.name, m.allow_cascaded_delete, m.allow_cascaded_copy, obj)
|
55
|
-
if fk.name in m.allow_cascaded_delete or fk.name in m.allow_cascaded_copy:
|
56
|
-
related.append((fk, m.objects.filter(**{fk.name: obj})))
|
57
|
-
|
58
|
-
fields_list = obj._meta.concrete_fields
|
59
|
-
if True:
|
60
|
-
for f in fields_list:
|
61
|
-
if not f.primary_key:
|
62
|
-
if f.name not in known_values:
|
63
|
-
known_values[f.name] = getattr(obj, f.name)
|
64
|
-
new = obj.__class__(**known_values)
|
65
|
-
# 20120704 create_instances causes fill_from_person() on a
|
66
|
-
# CBSS request.
|
67
|
-
else:
|
68
|
-
# doesn't seem to want to work
|
69
|
-
new = obj
|
70
|
-
for f in fields_list:
|
71
|
-
if f.primary_key:
|
72
|
-
# causes Django to consider this an unsaved instance
|
73
|
-
setattr(new, f.name, None)
|
74
|
-
|
51
|
+
new, related = obj.duplication_plan(**known_values)
|
75
52
|
new.on_duplicate(ar, None)
|
76
53
|
new.full_clean()
|
77
54
|
new.save(force_insert=True)
|
@@ -94,11 +71,9 @@ class Duplicate(actions.Action):
|
|
94
71
|
|
95
72
|
logger.info("%s has been duplicated to %s (%d related rows)",
|
96
73
|
obj, new, relcount)
|
97
|
-
|
98
74
|
return new
|
99
75
|
|
100
76
|
def run_from_ui(self, ar, **kw):
|
101
|
-
"""This actually runs the action."""
|
102
77
|
|
103
78
|
if (msg := ar.actor.model.disable_create(ar)) is not None:
|
104
79
|
ar.error(msg)
|
@@ -126,18 +101,38 @@ class Duplicate(actions.Action):
|
|
126
101
|
)
|
127
102
|
|
128
103
|
|
129
|
-
class
|
130
|
-
"
|
131
|
-
of) the object it was called on.
|
104
|
+
class Clonable(model.Model):
|
105
|
+
"See :doc:`/dev/duplicate`."
|
132
106
|
|
133
|
-
|
134
|
-
|
135
|
-
except the primary key and all related objects that are
|
136
|
-
duplicable.
|
107
|
+
class Meta:
|
108
|
+
abstract = True
|
137
109
|
|
138
|
-
|
110
|
+
clone_row = CloneRow()
|
139
111
|
|
140
|
-
|
141
|
-
|
112
|
+
def duplication_plan(obj, **known_values):
|
113
|
+
related = []
|
114
|
+
for m, fk in obj._lino_ddh.fklist:
|
115
|
+
# if fk.name in m.suppress_cascaded_copy:
|
116
|
+
# continue
|
117
|
+
# print(fk.name, m.allow_cascaded_delete, m.allow_cascaded_copy, obj)
|
118
|
+
# if fk.name in m.allow_cascaded_delete or fk.name in m.allow_cascaded_copy:
|
119
|
+
if fk.name in m.allow_cascaded_copy:
|
120
|
+
related.append((fk, m.objects.filter(**{fk.name: obj})))
|
142
121
|
|
143
|
-
|
122
|
+
fields_list = obj._meta.concrete_fields
|
123
|
+
if True:
|
124
|
+
for f in fields_list:
|
125
|
+
if not f.primary_key:
|
126
|
+
if f.name not in known_values:
|
127
|
+
known_values[f.name] = getattr(obj, f.name)
|
128
|
+
new = obj.__class__(**known_values)
|
129
|
+
# 20120704 create_instances causes fill_from_person() on a
|
130
|
+
# CBSS request.
|
131
|
+
else:
|
132
|
+
# doesn't seem to want to work
|
133
|
+
new = obj
|
134
|
+
for f in fields_list:
|
135
|
+
if f.primary_key:
|
136
|
+
# causes Django to consider this an unsaved instance
|
137
|
+
setattr(new, f.name, None)
|
138
|
+
return new, related
|
lino/mixins/dupable.py
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
Defines the :class:`Dupable` model mixin and related functionality
|
6
6
|
to assist users in finding unwanted duplicate database records.
|
7
7
|
|
8
|
-
Don't mix up this module with :mod:`lino.mixins
|
9
|
-
are "
|
8
|
+
Don't mix up this module with :mod:`lino.mixins.`. Models
|
9
|
+
are "" if users may *want* to duplicate some instance
|
10
10
|
thereof, while "dupable" implies that the duplicates are *unwanted*.
|
11
11
|
To dupe *somebody* means "to make a dupe of; deceive; delude; trick."
|
12
12
|
(`reference.com <https://dictionary.reference.com/browse/dupe>`_), and
|
lino/mixins/registrable.py
CHANGED
@@ -143,7 +143,7 @@ class Registrable(model.Model):
|
|
143
143
|
# if not ar.bound_action.action.readonly:
|
144
144
|
if not ba.action.readonly:
|
145
145
|
return False
|
146
|
-
return super(
|
146
|
+
return super().get_row_permission(ar, state, ba)
|
147
147
|
|
148
148
|
def register(self, ar):
|
149
149
|
"""
|
@@ -193,7 +193,7 @@ class Registrable(model.Model):
|
|
193
193
|
|
194
194
|
@classmethod
|
195
195
|
def get_simple_parameters(cls):
|
196
|
-
for p in super(
|
196
|
+
for p in super().get_simple_parameters():
|
197
197
|
yield p
|
198
198
|
# if isinstance(cls.workflow_state_field, str):
|
199
199
|
# raise Exception("Unresolved workflow state field in {}".format(cls))
|
@@ -203,4 +203,4 @@ class Registrable(model.Model):
|
|
203
203
|
|
204
204
|
def on_duplicate(self, ar, master):
|
205
205
|
self.state = self.workflow_state_field.choicelist.draft
|
206
|
-
super(
|
206
|
+
super().on_duplicate(ar, master)
|
lino/mixins/sequenced.py
CHANGED
@@ -24,7 +24,7 @@ from lino.utils.html import E
|
|
24
24
|
from lino.utils import AttrDict
|
25
25
|
from lino.utils import join_elems
|
26
26
|
|
27
|
-
from .
|
27
|
+
from lino.mixins.clonable import Clonable, CloneRow
|
28
28
|
|
29
29
|
|
30
30
|
class MoveByN(actions.Action):
|
@@ -167,8 +167,7 @@ class MoveDown(actions.Action):
|
|
167
167
|
ar.success(**kw)
|
168
168
|
|
169
169
|
|
170
|
-
class
|
171
|
-
"""Duplicate this row."""
|
170
|
+
class CloneSequenced(CloneRow):
|
172
171
|
|
173
172
|
def run_from_code(self, ar, **kw):
|
174
173
|
obj = ar.selected_rows[0]
|
@@ -184,7 +183,7 @@ class DuplicateSequenced(Duplicate):
|
|
184
183
|
return super().run_from_code(ar, **kw)
|
185
184
|
|
186
185
|
|
187
|
-
class Sequenced(
|
186
|
+
class Sequenced(Clonable):
|
188
187
|
"""Mixin for models that have a field :attr:`seqno` containing a
|
189
188
|
"sequence number".
|
190
189
|
|
@@ -192,12 +191,11 @@ class Sequenced(Duplicable):
|
|
192
191
|
|
193
192
|
The sequence number of this item with its parent.
|
194
193
|
|
195
|
-
.. method::
|
194
|
+
.. method:: clone_row
|
196
195
|
|
197
|
-
Create a duplicate of this
|
198
|
-
below this one.
|
196
|
+
Create a duplicate of this row and insert the new row below this one.
|
199
197
|
|
200
|
-
Implemented by :class:`
|
198
|
+
Implemented by :class:`CloneSequenced`
|
201
199
|
|
202
200
|
.. attribute:: move_up
|
203
201
|
|
@@ -212,28 +210,28 @@ class Sequenced(Duplicable):
|
|
212
210
|
Displays buttons for certain actions on this row:
|
213
211
|
|
214
212
|
- :attr:`move_up` and :attr:`move_down`
|
215
|
-
-
|
213
|
+
- clone_row
|
216
214
|
|
217
215
|
.. attribute:: move_by_n
|
218
216
|
|
219
217
|
"""
|
220
218
|
|
221
|
-
move_action_names = ("move_up", "move_down", "
|
219
|
+
move_action_names = ("move_up", "move_down", "clone_row")
|
222
220
|
"""The names of the actions to display in the `move_buttons`
|
223
221
|
column.
|
224
222
|
|
225
223
|
Overridden by :class:`lino.modlib.dashboard.Widget` where the
|
226
|
-
|
224
|
+
clone_row button would be irritating.
|
227
225
|
|
228
226
|
"""
|
229
227
|
|
230
|
-
class Meta
|
228
|
+
class Meta:
|
231
229
|
abstract = True
|
232
230
|
ordering = ["seqno"]
|
233
231
|
|
234
232
|
seqno = models.IntegerField(_("No."), blank=True, null=False)
|
235
233
|
|
236
|
-
|
234
|
+
clone_row = CloneSequenced()
|
237
235
|
|
238
236
|
move_up = MoveUp()
|
239
237
|
move_down = MoveDown()
|
@@ -352,7 +350,7 @@ Sequenced.set_widget_options("move_buttons", width=5)
|
|
352
350
|
Sequenced.set_widget_options("seqno", hide_sum=True)
|
353
351
|
|
354
352
|
|
355
|
-
class Hierarchical(
|
353
|
+
class Hierarchical(Clonable):
|
356
354
|
"""Model mixin for things that have a "parent" and "siblings".
|
357
355
|
|
358
356
|
Pronounciation: [hai'ra:kikl]
|
lino/modlib/comments/mixins.py
CHANGED
@@ -123,10 +123,9 @@ class Commentable(MemoReferrable):
|
|
123
123
|
return _("Created new {model} {obj}.").format(
|
124
124
|
model=self.__class__._meta.verbose_name, obj=self)
|
125
125
|
|
126
|
-
@classmethod
|
127
|
-
def add_comments_filter(cls, qs, ar):
|
128
|
-
|
126
|
+
# @classmethod
|
127
|
+
# def add_comments_filter(cls, qs, ar):
|
128
|
+
# return qs
|
129
129
|
|
130
|
-
def
|
131
|
-
|
132
|
-
return dd.plugins.comments.private_default
|
130
|
+
def on_create_comment(self, comment, ar):
|
131
|
+
comment.private = dd.plugins.comments.private_default
|
lino/modlib/comments/models.py
CHANGED
@@ -9,7 +9,7 @@ from django.db import models
|
|
9
9
|
from django.db.models import Q
|
10
10
|
from django.core import validators
|
11
11
|
from django.utils.html import mark_safe, format_html, SafeString
|
12
|
-
from django.
|
12
|
+
from django.utils.translation import ngettext
|
13
13
|
from django.conf import settings
|
14
14
|
# from lino.utils.html import E, tostring, fromstring
|
15
15
|
|
@@ -24,6 +24,7 @@ from lino.modlib.search.mixins import ElasticSearchable
|
|
24
24
|
from lino.modlib.gfks.mixins import Controllable
|
25
25
|
from lino.modlib.memo.mixins import Previewable, MemoReferrable
|
26
26
|
from lino.modlib.publisher.mixins import Publishable
|
27
|
+
from lino_xl.lib.groups.mixins import Groupwise
|
27
28
|
from .choicelists import CommentEvents, Emotions
|
28
29
|
from .mixins import Commentable, MyEmotionField
|
29
30
|
from .roles import CommentsReader, CommentsStaff
|
@@ -49,8 +50,9 @@ class Comment(
|
|
49
50
|
Publishable,
|
50
51
|
DateRangeObservable,
|
51
52
|
MemoReferrable,
|
53
|
+
Groupwise
|
52
54
|
):
|
53
|
-
class Meta
|
55
|
+
class Meta:
|
54
56
|
app_label = "comments"
|
55
57
|
abstract = dd.is_abstract_model(__name__, "Comment")
|
56
58
|
verbose_name = _("Comment")
|
@@ -119,8 +121,7 @@ class Comment(
|
|
119
121
|
# more_text = dd.RichTextField(_("More text"), blank=True)
|
120
122
|
|
121
123
|
private = models.BooleanField(
|
122
|
-
_("Confidential"), default=dd.plugins.comments.private_default
|
123
|
-
)
|
124
|
+
_("Confidential"), default=dd.plugins.comments.private_default)
|
124
125
|
|
125
126
|
comment_type = dd.ForeignKey("comments.CommentType", blank=True, null=True)
|
126
127
|
# reply_vote = models.BooleanField(_("Upvote"), null=True, blank=True)
|
@@ -187,10 +188,10 @@ class Comment(
|
|
187
188
|
# if o.emotion.button_text:
|
188
189
|
# yield o.emotion.button_text
|
189
190
|
# yield " "
|
190
|
-
if False:
|
191
|
-
|
192
|
-
else:
|
193
|
-
|
191
|
+
# if False:
|
192
|
+
# attrs = {"class": "bordertext"}
|
193
|
+
# else:
|
194
|
+
# attrs = {}
|
194
195
|
s += ar.obj2htmls(o, naturaltime(o.created), title=t)
|
195
196
|
s += format_html(" {} ", _("by"))
|
196
197
|
if o.user is None:
|
@@ -284,7 +285,7 @@ class Comment(
|
|
284
285
|
def on_create(self, ar):
|
285
286
|
super().on_create(ar)
|
286
287
|
if self.owner_id:
|
287
|
-
self.
|
288
|
+
self.owner.on_create_comment(self, ar)
|
288
289
|
|
289
290
|
def after_ui_save(self, ar, cw):
|
290
291
|
super().after_ui_save(ar, cw)
|
@@ -355,9 +356,10 @@ class Comment(
|
|
355
356
|
qs = qs.filter(private=False)
|
356
357
|
elif not user.user_type.has_required_roles([CommentsStaff]):
|
357
358
|
qs = qs.filter(Q(private=False) | Q(user=user))
|
358
|
-
|
359
|
-
|
360
|
-
|
359
|
+
|
360
|
+
# sar = BaseRequest(user=ar.get_user())
|
361
|
+
# for m in rt.models_by_base(Commentable):
|
362
|
+
# qs = m.add_comments_filter(qs, sar)
|
361
363
|
|
362
364
|
qs = qs.annotate(num_replies=models.Count("replies_to_this"))
|
363
365
|
qs = qs.annotate(num_reactions=models.Count("reactions_to_this"))
|
lino/modlib/comments/ui.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 lino.modlib.publisher.choicelists import PageFillers
|
6
|
-
from django.utils.translation import ngettext
|
7
6
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
8
7
|
from django.contrib.contenttypes.models import ContentType
|
9
8
|
from django.db import models
|
lino/modlib/dupable/models.py
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
"""Defines the :class:`Dupable` model mixin and related functionality
|
5
5
|
to assist users in finding unwanted duplicate database records.
|
6
6
|
|
7
|
-
Don't mix up this module with :mod:`lino.mixins
|
8
|
-
are "
|
7
|
+
Don't mix up this module with :mod:`lino.mixins.`. Models
|
8
|
+
are "" if users may *want* to duplicate some instance
|
9
9
|
thereof, while "dupable" implies that the duplicates are *unwanted*.
|
10
10
|
To dupe *somebody* means "to make a dupe of; deceive; delude; trick."
|
11
11
|
(`reference.com <https://dictionary.reference.com/browse/dupe>`_), and
|
lino/modlib/memo/mixins.py
CHANGED
@@ -214,6 +214,8 @@ class Previewable(BasePreviewable):
|
|
214
214
|
body_short_preview = RichTextField(_("Preview"), blank=True, editable=False)
|
215
215
|
body_full_preview = RichTextField(_("Preview (full)"), blank=True, editable=False)
|
216
216
|
|
217
|
+
edit_body = dd.ShowEditor("body")
|
218
|
+
|
217
219
|
def get_body_parsed(self, ar, short=False):
|
218
220
|
if ar.renderer is settings.SITE.kernel.editing_front_end.renderer:
|
219
221
|
return self.body_short_preview if short else self.body_full_preview
|
lino/modlib/notify/api.py
CHANGED
@@ -8,6 +8,7 @@ from django.conf import settings
|
|
8
8
|
from django.utils.timezone import now
|
9
9
|
from lino.api import rt, dd
|
10
10
|
from lino.modlib.linod.utils import CHANNEL_NAME, BROADCAST_CHANNEL, get_channel_name
|
11
|
+
from lino_xl.lib.matrix.utils import send_notification_to_matrix_room, send_notification_direct
|
11
12
|
|
12
13
|
NOTIFICATION = "NOTIFICATION"
|
13
14
|
CHAT = "CHAT"
|
@@ -67,6 +68,10 @@ def send_notification(
|
|
67
68
|
if user is None or settings.SITE.loading_from_dump:
|
68
69
|
return
|
69
70
|
|
71
|
+
if dd.is_installed("matrix") and dd.plugins.matrix.credentials_file is not None:
|
72
|
+
# send_notification_to_matrix_room(f"{subject}\n\n{body}")
|
73
|
+
send_notification_direct(f"{subject}\n\n{body}", user)
|
74
|
+
|
70
75
|
if dd.get_plugin_setting("linod", "use_channels"):
|
71
76
|
# importing channels at module level would cause certain things to fail
|
72
77
|
# when channels isn't installed, e.g. `manage.py prep` in `lino_book.projects.workflows`.
|
lino/modlib/printing/mixins.py
CHANGED
@@ -16,7 +16,7 @@ from lino.modlib.checkdata.choicelists import Checker
|
|
16
16
|
|
17
17
|
from lino.utils.choosers import chooser
|
18
18
|
from lino.core.model import Model
|
19
|
-
from lino.mixins.
|
19
|
+
from lino.mixins.clonable import Clonable
|
20
20
|
|
21
21
|
from .choicelists import BuildMethods
|
22
22
|
from .actions import (
|
@@ -148,7 +148,7 @@ class Printable(Model):
|
|
148
148
|
return True
|
149
149
|
|
150
150
|
|
151
|
-
class CachedPrintable(
|
151
|
+
class CachedPrintable(Clonable, Printable):
|
152
152
|
class Meta(object):
|
153
153
|
abstract = True
|
154
154
|
|
lino/modlib/publisher/models.py
CHANGED
@@ -38,6 +38,7 @@ from lino.modlib.linod.choicelists import schedule_daily
|
|
38
38
|
from lino.modlib.memo.mixins import Previewable
|
39
39
|
from lino.mixins.polymorphic import Polymorphic
|
40
40
|
from lino_xl.lib.topics.mixins import Taggable
|
41
|
+
from lino_xl.lib.groups.mixins import Groupwise
|
41
42
|
# from .utils import render_node
|
42
43
|
|
43
44
|
from lino.api import rt, dd
|
@@ -50,19 +51,22 @@ from .ui import *
|
|
50
51
|
|
51
52
|
|
52
53
|
class Page(
|
53
|
-
Hierarchical, Sequenced, Previewable, Commentable, PublishableContent,
|
54
|
+
Hierarchical, Sequenced, Previewable, Commentable, PublishableContent,
|
55
|
+
Taggable # , Groupwise
|
54
56
|
):
|
55
57
|
class Meta:
|
56
58
|
verbose_name = _("Page")
|
57
59
|
verbose_name_plural = _("Pages")
|
58
60
|
abstract = dd.is_abstract_model(__name__, "Page")
|
61
|
+
# if dd.is_installed("groups"):
|
62
|
+
# unique_together = ["group", "ref", "language"]
|
63
|
+
# else:
|
64
|
+
# unique_together = ["ref", "language"]
|
59
65
|
unique_together = ["ref", "language"]
|
60
66
|
|
61
67
|
memo_command = "page"
|
62
68
|
|
63
|
-
ref =
|
64
|
-
_("Reference"), max_length=200, blank=True, null=True)
|
65
|
-
|
69
|
+
ref = dd.CharField(_("Reference"), max_length=200, blank=True, null=True)
|
66
70
|
title = dd.CharField(_("Title"), max_length=250, blank=True)
|
67
71
|
child_node_depth = models.IntegerField(default=1)
|
68
72
|
# page_type = PageTypes.field(blank=True, null=True)
|
@@ -362,10 +366,14 @@ class Page(
|
|
362
366
|
# return True
|
363
367
|
|
364
368
|
def get_absolute_url(self, **kwargs):
|
369
|
+
parts = []
|
370
|
+
if self.group is not None:
|
371
|
+
if self.group.ref is not None:
|
372
|
+
parts.append(self.group.ref)
|
365
373
|
if self.ref:
|
366
374
|
if self.ref != "index":
|
367
|
-
|
368
|
-
return dd.plugins.publisher.build_plain_url(**kwargs)
|
375
|
+
parts.append(self.group.ref)
|
376
|
+
return dd.plugins.publisher.build_plain_url(*parts, **kwargs)
|
369
377
|
|
370
378
|
def get_publisher_response(self, ar):
|
371
379
|
if ar and ar.request and self.language != ar.request.LANGUAGE_CODE:
|
@@ -382,8 +390,7 @@ class Page(
|
|
382
390
|
sources.add(p.id)
|
383
391
|
p = p.translated_from
|
384
392
|
qs = self.__class__.objects.filter(
|
385
|
-
language=rqlang, translated_from__in=sources
|
386
|
-
)
|
393
|
+
language=rqlang, translated_from__in=sources)
|
387
394
|
obj = qs.first()
|
388
395
|
# obj = self.translated_to.filter(language=rqlang).first()
|
389
396
|
# print("20231027 redirect to translation", tt.language, ar.request.LANGUAGE_CODE)
|
lino/modlib/summaries/mixins.py
CHANGED
@@ -38,7 +38,8 @@ class UpdateSummariesByMaster(ComputeResults):
|
|
38
38
|
|
39
39
|
|
40
40
|
class Summarized(dd.Model):
|
41
|
-
|
41
|
+
|
42
|
+
class Meta:
|
42
43
|
abstract = True
|
43
44
|
|
44
45
|
compute_results = ComputeResults()
|
@@ -86,15 +87,17 @@ class Summarized(dd.Model):
|
|
86
87
|
|
87
88
|
def get_summary_collectors(self):
|
88
89
|
raise NotImplementedError(
|
89
|
-
"{} must define get_summary_collectors()"
|
90
|
+
f"{self.__class__} must define get_summary_collectors()"
|
90
91
|
)
|
91
92
|
|
92
93
|
|
93
94
|
class SlaveSummarized(Summarized):
|
94
|
-
|
95
|
+
|
96
|
+
class Meta:
|
95
97
|
abstract = True
|
96
98
|
|
97
99
|
allow_cascaded_delete = "master"
|
100
|
+
allow_cascaded_copy = set()
|
98
101
|
|
99
102
|
@classmethod
|
100
103
|
def check_all_summaries(cls):
|
@@ -181,7 +184,6 @@ class DateSummarized(Summarized):
|
|
181
184
|
flt.update(month=period)
|
182
185
|
if year is not None:
|
183
186
|
flt.update(year=year)
|
184
|
-
# obj = cls.get_for_period(**flt)
|
185
187
|
for obj in cls.get_for_filter(**flt):
|
186
188
|
obj.compute_summary_values()
|
187
189
|
|
lino/modlib/users/actions.py
CHANGED
@@ -495,6 +495,11 @@ def get_social_auth_links_func(content_header, flex_row, ar=None):
|
|
495
495
|
else:
|
496
496
|
el = E.span(anchor, style=style)
|
497
497
|
links.append(el)
|
498
|
+
anchor = E.a(E.span(gettext("Smart ID")), href="/auth/smart_id")
|
499
|
+
if flex_row:
|
500
|
+
links.append(E.div(anchor, style=style))
|
501
|
+
else:
|
502
|
+
links.append(E.span(anchor, style=style))
|
498
503
|
elems.append(E.div(*links, style="text-align: center;"))
|
499
504
|
return tostring(E.div(*elems))
|
500
505
|
|
lino/sphinxcontrib/__init__.py
CHANGED
@@ -102,7 +102,7 @@ def configure(globals_dict, django_settings_module=None):
|
|
102
102
|
rstgen.sphinxconf.configure()
|
103
103
|
|
104
104
|
You can specify an additional positional argument `django_settings_module`
|
105
|
-
(the name of a Django settings
|
105
|
+
(the name of a Django settings file). If this argument is specified, call
|
106
106
|
:meth:`lino.startup` with it.
|
107
107
|
|
108
108
|
"""
|
lino/sphinxcontrib/actordoc.py
CHANGED
@@ -11,7 +11,7 @@ for a Lino application.
|
|
11
11
|
source code. This is like :rst:dir:`py2rst` but with the following
|
12
12
|
names defined:
|
13
13
|
|
14
|
-
:settings: The Django settings
|
14
|
+
:settings: The Django settings file which is active while building
|
15
15
|
the docs.
|
16
16
|
|
17
17
|
:dd: The :mod:`lino.api.dd` module.
|
lino/utils/cycler.py
CHANGED
@@ -51,14 +51,17 @@ class Cycler(object):
|
|
51
51
|
return item.pop()
|
52
52
|
return item
|
53
53
|
|
54
|
+
def reset(self):
|
55
|
+
self.current = 0
|
56
|
+
|
54
57
|
def __len__(self):
|
55
58
|
return len(self.items)
|
56
59
|
|
60
|
+
def __iter__(self):
|
61
|
+
return self.items.__iter__()
|
62
|
+
|
57
63
|
def __repr__(self):
|
58
64
|
return f"Cycler({self.current} of {len(self.items)} in loop {self.loop_no})"
|
59
65
|
|
60
|
-
def reset(self):
|
61
|
-
self.current = 0
|
62
|
-
|
63
66
|
def __getitem__(self, *args):
|
64
67
|
return self.items.__getitem__(*args)
|
lino/utils/diag.py
CHANGED
@@ -341,9 +341,9 @@ class Analyzer(object):
|
|
341
341
|
return "{0}.{1}".format(fmn(mfk[0]), mfk[1].name)
|
342
342
|
|
343
343
|
items1 = []
|
344
|
-
for target, dp in
|
344
|
+
for target, dp in tdp.items():
|
345
345
|
items2 = []
|
346
|
-
for dh, pl in
|
346
|
+
for dh, pl in dp.items():
|
347
347
|
items2.append(
|
348
348
|
"{0} : {1}".format(
|
349
349
|
dh.__name__, ", ".join([fk2str(mfk) for mfk in pl])
|