lino 25.7.1__py3-none-any.whl → 25.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lino/__init__.py +1 -1
- lino/api/doctest.py +41 -12
- lino/core/__init__.py +0 -2
- lino/core/actions.py +15 -7
- lino/core/actors.py +2 -162
- lino/core/atomizer.py +9 -8
- lino/core/auth/utils.py +9 -1
- lino/core/callbacks.py +2 -2
- lino/core/elems.py +1 -1
- lino/core/fields.py +3 -1
- lino/core/kernel.py +14 -18
- lino/core/layouts.py +5 -7
- lino/core/model.py +12 -3
- lino/core/plugin.py +1 -1
- lino/core/renderer.py +1 -1
- lino/core/requests.py +3 -4
- lino/core/site.py +1 -1
- lino/core/store.py +3 -3
- lino/core/utils.py +20 -17
- lino/help_texts.py +10 -7
- lino/mixins/__init__.py +3 -2
- lino/mixins/{duplicable.py → clonable.py} +45 -50
- lino/mixins/dupable.py +2 -2
- lino/mixins/registrable.py +7 -5
- lino/mixins/sequenced.py +12 -14
- lino/modlib/dupable/models.py +2 -2
- lino/modlib/extjs/views.py +7 -0
- lino/modlib/linod/__init__.py +1 -1
- lino/modlib/memo/__init__.py +1 -2
- lino/modlib/notify/api.py +5 -0
- lino/modlib/office/roles.py +0 -1
- lino/modlib/printing/actions.py +2 -6
- lino/modlib/printing/choicelists.py +6 -6
- lino/modlib/printing/mixins.py +4 -4
- lino/modlib/publisher/__init__.py +21 -30
- lino/modlib/publisher/models.py +3 -1
- lino/modlib/publisher/views.py +4 -11
- lino/modlib/summaries/mixins.py +6 -4
- lino/modlib/users/actions.py +5 -0
- lino/modlib/weasyprint/__init__.py +9 -0
- lino/modlib/weasyprint/choicelists.py +14 -9
- lino/modlib/weasyprint/config/weasyprint/base.weasy.html +15 -13
- lino/sphinxcontrib/__init__.py +1 -1
- lino/sphinxcontrib/actordoc.py +1 -1
- lino/utils/diag.py +2 -2
- lino/utils/instantiator.py +21 -1
- {lino-25.7.1.dist-info → lino-25.7.3.dist-info}/METADATA +1 -1
- {lino-25.7.1.dist-info → lino-25.7.3.dist-info}/RECORD +51 -55
- lino/modlib/forms/__init__.py +0 -51
- lino/modlib/forms/models.py +0 -0
- lino/modlib/forms/renderer.py +0 -74
- lino/modlib/forms/views.py +0 -311
- {lino-25.7.1.dist-info → lino-25.7.3.dist-info}/WHEEL +0 -0
- {lino-25.7.1.dist-info → lino-25.7.3.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.7.1.dist-info → lino-25.7.3.dist-info}/licenses/COPYING +0 -0
lino/core/model.py
CHANGED
@@ -47,8 +47,9 @@ class Model(models.Model, fields.TableRow):
|
|
47
47
|
class Meta(object):
|
48
48
|
abstract = True
|
49
49
|
|
50
|
-
allow_cascaded_copy = frozenset()
|
51
50
|
allow_cascaded_delete = frozenset()
|
51
|
+
allow_cascaded_copy = None
|
52
|
+
# suppress_cascaded_copy = frozenset()
|
52
53
|
grid_post = actions.CreateRow()
|
53
54
|
submit_insert = actions.SubmitInsert()
|
54
55
|
allow_merge_action = False
|
@@ -479,14 +480,20 @@ class Model(models.Model, fields.TableRow):
|
|
479
480
|
elem.after_ui_save(ar, None)
|
480
481
|
|
481
482
|
def save_watched_instance(elem, ar, watcher):
|
483
|
+
# raise Exception("20250726")
|
482
484
|
if watcher.is_dirty():
|
483
485
|
# pre_ui_save.send(sender=elem.__class__, instance=elem, ar=ar)
|
484
486
|
# elem.before_ui_save(ar, watcher)
|
485
487
|
elem.save(force_update=True)
|
486
488
|
watcher.send_update(ar)
|
487
489
|
ar.success(_("%s has been updated.") % obj2unicode(elem))
|
488
|
-
else:
|
489
|
-
|
490
|
+
# else:
|
491
|
+
# ar.success(_("%s : nothing to save.") % obj2unicode(elem))
|
492
|
+
# 20250726 The "nothing to save" message is confusing e.g. in
|
493
|
+
# ratings.ResponsesByExam when setting score1 or score2. These virtual
|
494
|
+
# fields store the value in their respective ChallengeRating object but
|
495
|
+
# the examResponse remains unchanged.
|
496
|
+
|
490
497
|
elem.after_ui_save(ar, watcher)
|
491
498
|
|
492
499
|
def delete_instance(self, ar):
|
@@ -948,6 +955,7 @@ LINO_MODEL_ATTRIBS = (
|
|
948
955
|
"before_ui_save",
|
949
956
|
"allow_cascaded_delete",
|
950
957
|
"allow_cascaded_copy",
|
958
|
+
# "suppress_cascaded_copy",
|
951
959
|
"workflow_state_field",
|
952
960
|
"workflow_owner_field",
|
953
961
|
"disabled_fields",
|
@@ -994,6 +1002,7 @@ LINO_MODEL_ATTRIBS = (
|
|
994
1002
|
"_lino_tables",
|
995
1003
|
"show_in_site_search",
|
996
1004
|
"allow_merge_action",
|
1005
|
+
"get_overview_elems",
|
997
1006
|
)
|
998
1007
|
|
999
1008
|
|
lino/core/plugin.py
CHANGED
lino/core/renderer.py
CHANGED
@@ -498,7 +498,7 @@ class HtmlRenderer(Renderer):
|
|
498
498
|
return E.p(*buttons)
|
499
499
|
|
500
500
|
def get_home_url(self, *args, **kw):
|
501
|
-
return settings.SITE.kernel.
|
501
|
+
return settings.SITE.kernel.editing_front_end.build_plain_url(*args, **kw)
|
502
502
|
|
503
503
|
def obj2url(self, ar, obj):
|
504
504
|
ba = obj.get_detail_action(ar)
|
lino/core/requests.py
CHANGED
@@ -1842,10 +1842,9 @@ class ActionRequest(BaseRequest):
|
|
1842
1842
|
pv = self.actor.param_defaults(self)
|
1843
1843
|
for k in pv.keys():
|
1844
1844
|
if k not in self.actor.parameters:
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
)
|
1845
|
+
msg = f"{self.actor} param_defaults() returned keyword {k}"
|
1846
|
+
msg += f" (must be one of {sorted(self.actor.parameters.keys())})"
|
1847
|
+
raise Exception(msg)
|
1849
1848
|
|
1850
1849
|
# New since 20120913. E.g. newcomers.Newcomers is a
|
1851
1850
|
# simple pcsw.Clients with
|
lino/core/site.py
CHANGED
@@ -862,7 +862,7 @@ class Site(object):
|
|
862
862
|
needed_by = ip
|
863
863
|
# while needed_by.needed_by is not None:
|
864
864
|
# needed_by = needed_by.needed_by
|
865
|
-
for dep in ip.
|
865
|
+
for dep in ip.get_needed_plugins():
|
866
866
|
k2 = dep.rsplit(".")[-1]
|
867
867
|
if k2 not in self.plugins:
|
868
868
|
install_plugin(dep, needed_by=needed_by)
|
lino/core/store.py
CHANGED
@@ -203,7 +203,6 @@ class StoreField(object):
|
|
203
203
|
|
204
204
|
def set_value_in_object(self, ar, instance, v):
|
205
205
|
# logger.info("20180712 super set_value_in_object() %s", v)
|
206
|
-
|
207
206
|
old_value = self.value_from_object(instance, ar.request)
|
208
207
|
# old_value = getattr(instance,self.field.attname)
|
209
208
|
if old_value != v:
|
@@ -865,8 +864,8 @@ class FileFieldStoreField(StoreField):
|
|
865
864
|
|
866
865
|
|
867
866
|
class MethodStoreField(StoreField):
|
868
|
-
"""
|
869
|
-
Still used for DISPLAY_MODE_HTML.
|
867
|
+
"""
|
868
|
+
Still used for DISPLAY_MODE_HTML and writable virtual fields.
|
870
869
|
"""
|
871
870
|
|
872
871
|
def full_value_from_object(self, obj, ar=None):
|
@@ -898,6 +897,7 @@ class MethodStoreField(StoreField):
|
|
898
897
|
# pass
|
899
898
|
|
900
899
|
def form2obj(self, request, instance, post_data, is_new):
|
900
|
+
# print("20250726", self, post_data)
|
901
901
|
pass
|
902
902
|
# return instance
|
903
903
|
# raise Exception("Cannot update a virtual field")
|
lino/core/utils.py
CHANGED
@@ -664,10 +664,11 @@ class Parametrizable:
|
|
664
664
|
parameters = None
|
665
665
|
params_layout = None
|
666
666
|
params_panel_hidden = True
|
667
|
-
params_panel_pos = "
|
667
|
+
params_panel_pos = "bottom" # allowed values "top", "bottom", "left" and "right"
|
668
668
|
use_detail_param_panel = False
|
669
669
|
|
670
670
|
_params_layout_class = NotImplementedError
|
671
|
+
_field_actions = dict()
|
671
672
|
|
672
673
|
def get_window_layout(self, actor):
|
673
674
|
return self.params_layout
|
@@ -809,29 +810,30 @@ def error2str(self, e):
|
|
809
810
|
|
810
811
|
|
811
812
|
def resolve_fields_list(model, k, collection_type=tuple, default=None):
|
812
|
-
|
813
|
-
if
|
813
|
+
value = getattr(model, k)
|
814
|
+
if value is None:
|
814
815
|
setattr(model, k, default)
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
816
|
+
return
|
817
|
+
elif value == default:
|
818
|
+
return
|
819
|
+
elif isinstance(value, collection_type):
|
820
|
+
return
|
821
|
+
if isinstance(value, str):
|
822
|
+
value = value.split()
|
823
|
+
if isinstance(value, (list, tuple)):
|
820
824
|
lst = []
|
821
|
-
for n in
|
825
|
+
for n in value:
|
822
826
|
f = model.get_data_elem(n)
|
823
827
|
if f is None:
|
824
|
-
msg = "Invalid field {} in {} of {}"
|
825
|
-
msg = msg.format(n, k, model)
|
828
|
+
msg = f"Invalid field {n} in {k} of {model}"
|
826
829
|
raise Exception(msg)
|
827
830
|
lst.append(f)
|
828
831
|
setattr(model, k, collection_type(lst))
|
829
832
|
# fields.fields_list(model, model.quick_search_fields))
|
830
833
|
else:
|
831
834
|
raise ChangedAPI(
|
832
|
-
"{
|
833
|
-
"of space-separated field names (not {
|
834
|
-
)
|
835
|
+
f"{model}.{k} must be None or a string "
|
836
|
+
f"of space-separated field names (not {value})")
|
835
837
|
|
836
838
|
|
837
839
|
def class_dict_items(cl, exclude=None):
|
@@ -1238,11 +1240,12 @@ def register_params(cls):
|
|
1238
1240
|
cls.params_layout = cls._params_layout_class.join_str.join(
|
1239
1241
|
cls.parameters.keys()
|
1240
1242
|
)
|
1243
|
+
if cls.params_layout is not None:
|
1241
1244
|
install_layout(cls, "params_layout", cls._params_layout_class)
|
1242
1245
|
|
1243
|
-
# e.g. accounting.ByJournal is just a mixin but provides a default value for its children
|
1244
|
-
elif cls.params_layout is not None:
|
1245
|
-
|
1246
|
+
# # e.g. accounting.ByJournal is just a mixin but provides a default value for its children
|
1247
|
+
# elif cls.params_layout is not None:
|
1248
|
+
# raise Exception("{} has a params_layout but no parameters".format(cls))
|
1246
1249
|
|
1247
1250
|
# if isinstance(cls, type) and cls.__name__.endswith("Users"):
|
1248
1251
|
# # if isinstance(cls, type) and cls.model is not None and cls.model.__name__ == "User":
|
lino/help_texts.py
CHANGED
@@ -17,6 +17,8 @@ help_texts = {
|
|
17
17
|
'lino.mixins.ProjectRelated' : _("""Mixin for models that are related to a “project”, i.e. to an object of the type given by your lino.core.site.Site.project_model."""),
|
18
18
|
'lino.mixins.ProjectRelated.project' : _("""Pointer to the project to which this object is related."""),
|
19
19
|
'lino.mixins.ProjectRelated.update_owned_instance' : _("""When a project-related object controls another project-related object, then the controlled automatically inherits the project of its controller."""),
|
20
|
+
'lino.mixins.clonable.CloneRow' : _("""See /dev/duplicate."""),
|
21
|
+
'lino.mixins.clonable.Clonable' : _("""See /dev/duplicate."""),
|
20
22
|
'lino.mixins.dupable.CheckedSubmitInsert' : _("""Like the standard lino.core.actions.SubmitInsert, but adds a confirmation if there is a possible duplicate record."""),
|
21
23
|
'lino.mixins.dupable.PhoneticWordBase' : _("""Base class for the table of phonetic words of a given dupable model. For every (non-abstract) dupable model there must be a subclass of PhoneticWordBase. The subclass must define a field owner which points to the Dupable, and the Dupable’s dupable_word_model must point to its subclass of PhoneticWordBase."""),
|
22
24
|
'lino.mixins.dupable.Dupable' : _("""Base class for models that can be “dupable”."""),
|
@@ -30,9 +32,6 @@ help_texts = {
|
|
30
32
|
'lino.mixins.dupable.DupableChecker' : _("""Checks for the following repairable problem:"""),
|
31
33
|
'lino.mixins.dupable.DupableChecker.model' : _("""alias of Dupable"""),
|
32
34
|
'lino.mixins.dupable.SimilarObjects' : _("""Shows the other objects that are similar to this one."""),
|
33
|
-
'lino.mixins.duplicable.Duplicate' : _("""Duplicate the selected row."""),
|
34
|
-
'lino.mixins.duplicable.Duplicate.run_from_ui' : _("""This actually runs the action."""),
|
35
|
-
'lino.mixins.duplicable.Duplicable' : _("""Adds a row action “Duplicate”, which duplicates (creates a clone of) the object it was called on."""),
|
36
35
|
'lino.mixins.human.Human' : _("""Base class for models that represent a human."""),
|
37
36
|
'lino.mixins.human.Human.title' : _("""Used to specify a professional position or academic qualification like “Dr.” or “PhD”."""),
|
38
37
|
'lino.mixins.human.Human.first_name' : _("""The first name, also known as given name."""),
|
@@ -87,10 +86,9 @@ help_texts = {
|
|
87
86
|
'lino.mixins.sequenced.MoveByN' : _("""Move this row N rows upwards or downwards."""),
|
88
87
|
'lino.mixins.sequenced.MoveUp' : _("""Move this row one row upwards."""),
|
89
88
|
'lino.mixins.sequenced.MoveDown' : _("""Move this row one row downwards."""),
|
90
|
-
'lino.mixins.sequenced.DuplicateSequenced' : _("""Duplicate this row."""),
|
91
89
|
'lino.mixins.sequenced.Sequenced' : _("""Mixin for models that have a field seqno containing a “sequence number”."""),
|
92
90
|
'lino.mixins.sequenced.Sequenced.seqno' : _("""The sequence number of this item with its parent."""),
|
93
|
-
'lino.mixins.sequenced.Sequenced.
|
91
|
+
'lino.mixins.sequenced.Sequenced.clone_row' : _("""Create a duplicate of this row and insert the new row below this one."""),
|
94
92
|
'lino.mixins.sequenced.Sequenced.move_up' : _("""Exchange the seqno of this item and the previous item."""),
|
95
93
|
'lino.mixins.sequenced.Sequenced.move_down' : _("""Exchange the seqno of this item and the next item."""),
|
96
94
|
'lino.mixins.sequenced.Sequenced.move_buttons' : _("""Displays buttons for certain actions on this row:"""),
|
@@ -311,8 +309,6 @@ help_texts = {
|
|
311
309
|
'lino.core.model.Model.submit_insert' : _("""The SubmitInsert action to be executed when the when the users submits an insert window."""),
|
312
310
|
'lino.core.model.Model.create_from_choice' : _("""Called when a learning combo has been submitted. Create a persistent database object if the given text contains enough information."""),
|
313
311
|
'lino.core.model.Model.choice_text_to_dict' : _("""Return a dict of the fields to fill when the given text contains enough information for creating a new database object."""),
|
314
|
-
'lino.core.model.Model.allow_cascaded_delete' : _("""A set of names of ForeignKey or GenericForeignKey fields of this model that allow for cascaded delete."""),
|
315
|
-
'lino.core.model.Model.disabled_fields' : _("""Return a set of field names that should be disabled (i.e. not editable) for this database object."""),
|
316
312
|
'lino.core.model.Model.__str__' : _("""Return a translatable text that describes this database row."""),
|
317
313
|
'lino.core.model.Model.as_str' : _("""Return a translatable text that describes this database row. Unlike __str__() this method gets an action request when it is called, so it knows the context."""),
|
318
314
|
'lino.core.model.Model.get_str_words' : _("""Yield a series of words that describe this database row in plain text."""),
|
@@ -705,15 +701,22 @@ help_texts = {
|
|
705
701
|
'lino.modlib.weasyprint.WeasyHtmlBuildMethod' : _("""Renders the input template and returns the unmodified output as plain HTML."""),
|
706
702
|
'lino.modlib.weasyprint.WeasyPdfBuildMethod' : _("""Like WeasyBuildMethod, but the rendered HTML is then passed through weasyprint which converts from HTML to PDF."""),
|
707
703
|
'lino.core.model.Model' : _("""Lino extension of Django’s database model. This is a subclass of Django’s Model class (django.db.models.Model)."""),
|
704
|
+
'lino.core.model.Model.__init__' : _("""The first positional argument is the optional label, other arguments should be specified as keywords and can be any of the existing class attributes."""),
|
708
705
|
'lino.core.model.Model.overview' : _("""A multi-paragraph representation of this database row."""),
|
709
706
|
'lino.core.model.Model.navigation_panel' : _("""A virtual field that displays the navigation panel for this row. This may be included in a detail layout, usually either on the left or the right side with full height."""),
|
710
707
|
'lino.core.model.Model.workflow_buttons' : _("""Shows the current workflow state of this database row and a list of available workflow actions."""),
|
711
708
|
'lino.core.model.Model.workflow_state_field' : _("""Optional default value for the workflow_state_field of all data tables based on this model."""),
|
712
709
|
'lino.core.model.Model.workflow_owner_field' : _("""Optional default value for workflow_owner_field on all data tables based on this model."""),
|
710
|
+
'lino.core.model.Model.disabled_fields' : _("""Return a set of field names that should be disabled (i.e. not editable) for this database object."""),
|
713
711
|
'lino.core.model.Model.FOO_changed' : _("""Called when field FOO of an instance of this model has been modified through the user interface."""),
|
714
712
|
'lino.core.model.Model.FOO_choices' : _("""Return a queryset or list of allowed choices for field FOO."""),
|
715
713
|
'lino.core.model.Model.create_FOO_choice' : _("""For every field named “FOO” for which a chooser exists, if the model also has a method called “create_FOO_choice”, then this chooser will be a learning chooser. That is, users can enter text into the combobox, and Lino will create a new database object from it."""),
|
716
714
|
'lino.core.model.Model.get_choices_text' : _("""Return the text to be displayed when an instance of this model is being used as a choice in a combobox of a ForeignKey field pointing to this model. request is the web request, actor is the requesting actor."""),
|
717
715
|
'lino.core.model.Model.disable_delete' : _("""Decide whether this database object may be deleted. Return None when there is no veto against deleting this database row, otherwise a translatable message that explains to the user why they can’t delete this row."""),
|
718
716
|
'lino.core.model.Model.update_field' : _("""Shortcut to call lino.core.inject.update_field() for usage during lino.core.site.Site.do_site_startup() in a settings.py or similar place."""),
|
717
|
+
'lino.core.model.Model.on_duplicate' : _("""Called after duplicating a row."""),
|
718
|
+
'lino.core.model.Model.after_duplicate' : _("""Called by lino.mixins.clonable.Duplicate on the new copied row instance, after the row and its related fields have been saved."""),
|
719
|
+
'lino.core.model.Model.delete_veto_message' : _("""Return the message Cannot delete X because N Ys refer to it."""),
|
720
|
+
'lino.core.model.Model.allow_cascaded_delete' : _("""A set of foreign key fields that link this model to a master in a “possessive” way, i.e. objects of this model should get deleted together with their master."""),
|
721
|
+
'lino.core.model.Model.allow_cascaded_copy' : _("""A set of foreign key fields that that cause database rows to be automatically cloned when their master gets cloned."""),
|
719
722
|
}
|
lino/mixins/__init__.py
CHANGED
@@ -8,8 +8,9 @@ by applications and the :ref:`xl`. But none of them is mandatory.
|
|
8
8
|
.. autosummary::
|
9
9
|
:toctree:
|
10
10
|
|
11
|
-
|
11
|
+
|
12
12
|
dupable
|
13
|
+
clonable
|
13
14
|
sequenced
|
14
15
|
human
|
15
16
|
periods
|
@@ -50,7 +51,7 @@ from .polymorphic import Polymorphic
|
|
50
51
|
from .periods import ObservedDateRange, Yearly, Monthly, Today
|
51
52
|
from .periods import DateRange
|
52
53
|
from .sequenced import Sequenced, Hierarchical
|
53
|
-
from .
|
54
|
+
from .clonable import Clonable, CloneRow
|
54
55
|
from .registrable import Registrable, RegistrableState
|
55
56
|
from .ref import Referrable, StructuredReferrable
|
56
57
|
|
@@ -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
@@ -118,12 +118,14 @@ class Registrable(model.Model):
|
|
118
118
|
# yield 'date'
|
119
119
|
|
120
120
|
def disabled_fields(self, ar):
|
121
|
+
rv = super().disabled_fields(ar)
|
121
122
|
if not self.state.is_editable:
|
122
123
|
# return self._registrable_fields
|
123
124
|
# Copy _registrable_fields otherwise _registrable_fields get
|
124
125
|
# modified as more disabled fields are added to the set.
|
125
|
-
return self._registrable_fields.copy()
|
126
|
-
|
126
|
+
# return self._registrable_fields.copy()
|
127
|
+
rv |= self._registrable_fields
|
128
|
+
return rv
|
127
129
|
|
128
130
|
def get_row_permission(self, ar, state, ba):
|
129
131
|
"""Only rows in an editable state may be edited.
|
@@ -141,7 +143,7 @@ class Registrable(model.Model):
|
|
141
143
|
# if not ar.bound_action.action.readonly:
|
142
144
|
if not ba.action.readonly:
|
143
145
|
return False
|
144
|
-
return super(
|
146
|
+
return super().get_row_permission(ar, state, ba)
|
145
147
|
|
146
148
|
def register(self, ar):
|
147
149
|
"""
|
@@ -191,7 +193,7 @@ class Registrable(model.Model):
|
|
191
193
|
|
192
194
|
@classmethod
|
193
195
|
def get_simple_parameters(cls):
|
194
|
-
for p in super(
|
196
|
+
for p in super().get_simple_parameters():
|
195
197
|
yield p
|
196
198
|
# if isinstance(cls.workflow_state_field, str):
|
197
199
|
# raise Exception("Unresolved workflow state field in {}".format(cls))
|
@@ -201,4 +203,4 @@ class Registrable(model.Model):
|
|
201
203
|
|
202
204
|
def on_duplicate(self, ar, master):
|
203
205
|
self.state = self.workflow_state_field.choicelist.draft
|
204
|
-
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/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/extjs/views.py
CHANGED
@@ -463,6 +463,13 @@ class ApiElement(View):
|
|
463
463
|
else:
|
464
464
|
datarec = ar.elem2rec_detailed(elem)
|
465
465
|
datarec.update(**vm)
|
466
|
+
|
467
|
+
fa = dict()
|
468
|
+
for k, lst in rpt._field_actions.items():
|
469
|
+
fa[k] = [ar.row_action_button(elem, ba) for ba in lst]
|
470
|
+
if fa:
|
471
|
+
datarec.update(field_actions=fa)
|
472
|
+
|
466
473
|
return json_response(datarec)
|
467
474
|
|
468
475
|
after_show = ar.get_status(record_id=pk)
|
lino/modlib/linod/__init__.py
CHANGED
@@ -50,7 +50,7 @@ class Plugin(ad.Plugin):
|
|
50
50
|
m = m.add_menu(mg.app_label, mg.verbose_name)
|
51
51
|
m.add_action("linod.Procedures")
|
52
52
|
|
53
|
-
def
|
53
|
+
def get_needed_plugins(self):
|
54
54
|
# We don't use needs_plugins because it depends on use_channels. We must
|
55
55
|
# not install the plugin when the Python package isn't installed because
|
56
56
|
# otherwise `pm install` fails with ModuleNotFoundError: No module named
|
lino/modlib/memo/__init__.py
CHANGED
@@ -50,7 +50,7 @@ class Plugin(ad.Plugin):
|
|
50
50
|
"""The front end to use when writing previews.
|
51
51
|
|
52
52
|
If this is `None`, Lino will use the default :term:`front end`
|
53
|
-
(:attr:`lino.core.site.Site.
|
53
|
+
(:attr:`lino.core.site.Site.editing_front_end`).
|
54
54
|
|
55
55
|
Used on sites that are available through more than one web front ends. The
|
56
56
|
:term:`server administrator` must then decide which front end is the primary
|
@@ -115,7 +115,6 @@ class Plugin(ad.Plugin):
|
|
115
115
|
def post_site_startup(self, site):
|
116
116
|
if self.front_end is None:
|
117
117
|
self.front_end = site.kernel.editing_front_end
|
118
|
-
# web_front_ends[0]
|
119
118
|
else:
|
120
119
|
self.front_end = site.plugins.resolve(self.front_end)
|
121
120
|
|
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`.
|