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.
Files changed (43) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +28 -2
  3. lino/core/actions.py +12 -1
  4. lino/core/atomizer.py +9 -8
  5. lino/core/auth/utils.py +9 -1
  6. lino/core/callbacks.py +2 -2
  7. lino/core/dbtables.py +4 -1
  8. lino/core/fields.py +0 -1
  9. lino/core/kernel.py +9 -19
  10. lino/core/model.py +11 -3
  11. lino/core/site.py +10 -14
  12. lino/core/store.py +3 -3
  13. lino/core/utils.py +14 -13
  14. lino/help_texts.py +9 -7
  15. lino/management/commands/ddt.py +60 -0
  16. lino/management/commands/dump2py.py +13 -25
  17. lino/management/commands/prep.py +1 -1
  18. lino/mixins/__init__.py +3 -2
  19. lino/mixins/{duplicable.py → clonable.py} +45 -50
  20. lino/mixins/dupable.py +2 -2
  21. lino/mixins/registrable.py +3 -3
  22. lino/mixins/sequenced.py +12 -14
  23. lino/modlib/comments/mixins.py +5 -6
  24. lino/modlib/comments/models.py +14 -12
  25. lino/modlib/comments/ui.py +0 -1
  26. lino/modlib/dupable/models.py +2 -2
  27. lino/modlib/memo/mixins.py +2 -0
  28. lino/modlib/notify/api.py +5 -0
  29. lino/modlib/printing/mixins.py +2 -2
  30. lino/modlib/publisher/models.py +15 -8
  31. lino/modlib/summaries/mixins.py +6 -4
  32. lino/modlib/users/actions.py +5 -0
  33. lino/sphinxcontrib/__init__.py +1 -1
  34. lino/sphinxcontrib/actordoc.py +1 -1
  35. lino/utils/cycler.py +6 -3
  36. lino/utils/diag.py +2 -2
  37. lino/utils/dpy.py +70 -59
  38. lino/utils/instantiator.py +21 -1
  39. {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/METADATA +1 -1
  40. {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/RECORD +43 -42
  41. {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/WHEEL +0 -0
  42. {lino-25.7.2.dist-info → lino-25.8.0.dist-info}/licenses/AUTHORS.rst +0 -0
  43. {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
- """Defines the model mixin :class:`Duplicable`. "duplicable"
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 Duplicate(actions.Action):
20
- """Duplicate the selected row.
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 = "\u2687" # ⚇ "white circle with two dots"
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 Duplicable(model.Model):
130
- """Adds a row action "Duplicate", which duplicates (creates a clone
131
- of) the object it was called on.
104
+ class Clonable(model.Model):
105
+ "See :doc:`/dev/duplicate`."
132
106
 
133
- Subclasses may override :meth:`lino.core.model.Model.on_duplicate`
134
- to customize the default behaviour, which is to copy all fields
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
- class Meta(object):
141
- abstract = True
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
- duplicate = Duplicate()
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.duplicable`. Models
9
- are "duplicable" if users may *want* to duplicate some instance
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
@@ -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(Registrable, self).get_row_permission(ar, state, ba)
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(Registrable, cls).get_simple_parameters():
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(Registrable, self).on_duplicate(ar, master)
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 .duplicable import Duplicable, Duplicate
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 DuplicateSequenced(Duplicate):
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(Duplicable):
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:: duplicate
194
+ .. method:: clone_row
196
195
 
197
- Create a duplicate of this object and insert the new object
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:`DuplicateSequenced`
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
- - duplicate
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", "duplicate")
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
- duplicate button would be irritating.
224
+ clone_row button would be irritating.
227
225
 
228
226
  """
229
227
 
230
- class Meta(object):
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
- duplicate = DuplicateSequenced()
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(Duplicable):
353
+ class Hierarchical(Clonable):
356
354
  """Model mixin for things that have a "parent" and "siblings".
357
355
 
358
356
  Pronounciation: [hai'ra:kikl]
@@ -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
- return qs
126
+ # @classmethod
127
+ # def add_comments_filter(cls, qs, ar):
128
+ # return qs
129
129
 
130
- def is_comment_private(self, comment, ar):
131
- """Whether the given comment should be private."""
132
- return dd.plugins.comments.private_default
130
+ def on_create_comment(self, comment, ar):
131
+ comment.private = dd.plugins.comments.private_default
@@ -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.contrib.contenttypes.models import ContentType
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(object):
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
- attrs = {"class": "bordertext"}
192
- else:
193
- attrs = {}
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.private = self.owner.is_comment_private(self, ar)
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
- sar = BaseRequest(user=ar.get_user())
359
- for m in rt.models_by_base(Commentable):
360
- qs = m.add_comments_filter(qs, sar)
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"))
@@ -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
@@ -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.duplicable`. Models
8
- are "duplicable" if users may *want* to duplicate some instance
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
@@ -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`.
@@ -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.duplicable import Duplicable
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(Duplicable, Printable):
151
+ class CachedPrintable(Clonable, Printable):
152
152
  class Meta(object):
153
153
  abstract = True
154
154
 
@@ -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, Taggable
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 = models.CharField(
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
- return dd.plugins.publisher.build_plain_url(self.ref, **kwargs)
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)
@@ -38,7 +38,8 @@ class UpdateSummariesByMaster(ComputeResults):
38
38
 
39
39
 
40
40
  class Summarized(dd.Model):
41
- class Meta(object):
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()".format(self.__class__)
90
+ f"{self.__class__} must define get_summary_collectors()"
90
91
  )
91
92
 
92
93
 
93
94
  class SlaveSummarized(Summarized):
94
- class Meta(object):
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
 
@@ -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
 
@@ -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 module). If this argument is specified, call
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
  """
@@ -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 module which is active while building
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 list(tdp.items()):
344
+ for target, dp in tdp.items():
345
345
  items2 = []
346
- for dh, pl in list(dp.items()):
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])