Customize delete behaviour

This section is a topic guide about how to customize behaviour around deleting records.

Unlike Django, Lino has PROTECT as the default on_delete strategy in ForeignKey fields. If you want CASCADE, then you specify it explicitly using the allow_cascaded_delete attribute on the model whose instances will be deleted.

The disable_delete method of a model decides whether a given database object may be deleted or not. Also the disable_delete method of an actor.

The disable_delete item in data_record is a "preview" of whether that row can be deleted or not. The front end may use this information to disable or enable its delete button.

But the DeleteSelected action will verify again before actually deleting a row.

When Lino analyzes the application's models at startup, it adds a "disable_delete handler" (lino.core.ddh) to every model.

The lino.utils.diag.Analyzer.show_foreign_keys() can help to find examples for writing tests. It is used in specs like Deletion handlers in Lino Noi or Database structure in Lino Voga.

The disable_delete method

To customize whether a database row can be deleted or not, you override the Model.disable_delete() method.

When a user has view and write permission to an actor, they usually also have permission to delete individual database rows using the delete button in the toolbar. But before actually deleting a row, Lino will call the disable_delete method do decide whether the action will actually run.

class lino.core.model.Model
disable_delete(self, ar)

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.

As an example, here is how the disable_delete method of lino_xl.lib.cal.GuestsByEvent table adds a customized veto message to refuse deleting the presence of a guest in a calendar event for which Lino manages presences automatically

@classmethod
def disable_delete(cls, obj, ar):
    msg = super(GuestsByEvent, cls).disable_delete(obj, ar)
    if msg is not None:
        return msg
    mi = ar.master_instance
    assert mi == obj.event
    if mi.can_edit_guests_manually():
        return None
    return _("Cannot edit guests manually.")

When you override this method, be careful to call super() because Lino finds a lot of veto reasons automatically for you by checking whether the database contains related objects. For example. Lino by default forbids to delete any object that is referenced by other objects. Users will get a message of type "Cannot delete X because n Ys refer to it". See About cascaded deletes below for customizing this behaviour.

About cascaded deletes

Lino changes Django's default behaviour regarding cascaded delete on ForeignKey fields. You can set the allow_cascaded_copy and allow_cascaded_copy class attributes of a model to customize this behaviour.

With Lino, unlike plain Django, you control cascaded delete behaviour on the model whose instances are going to be deleted instead of defining it on the models that refer to it. So you usually don't need to care about Django's on_delete attribute, Lino automatically (in kernel_startup) sets this to PROTECT for all FK fields that are not listed in the allow_cascaded_delete of their model.

class lino.core.model.Model
allow_cascaded_delete

A set of names of ForeignKey or GenericForeignKey fields of this model that allow for cascaded delete.

If this is a simple string, Lino expects it to be a space-separated list of field names and convert it into a set at startup.

This is also used by lino.mixins.duplicable.Duplicate to decide whether slaves of a record being duplicated should be duplicated as well.

Example: Lino should not refuse to delete a Mail just because it has some Recipient. When deleting a Mail, Lino should also delete its Recipients. That's why lino_xl.lib.outbox.models.Recipient has allow_cascaded_delete = 'mail'.

Removing the delete button altogether

class lino.core.actors.Actor
allow_delete = True

If this is False, the table won't have any delete_action.