Trash functionality¶
Introduction¶
The Trash feature enables both logical deletion and permanent deletion of objects. Combined with History, it provides full control over data management, including recovery and auditing of deleted records.
Model Integration¶
To enable trash management in your models, inherit from DeletedWithTrash.
This abstract class is related to the Trash model and provides the necessary logic.
from djgentelella.models import DeletedWithTrash
class Customer(DeletedWithTrash):
name = models.CharField(max_length=150)
phone_number = models.CharField(max_length=150)
email = models.EmailField()
def __str__(self):
return self.name
def delete(self, using=None, keep_parents=False, *, hard=False, user=None, **kwargs):
if self.is_deleted and not hard:
return
result = super().delete(
using=using, keep_parents=keep_parents,
hard=hard, user=user
)
return result
ViewSets and Deletion¶
There are two possible cases when overriding perform_destroy in your ViewSet:
Case 1: Without history
def perform_destroy(self, instance):
instance.delete(user=self.request.user)
Case 2: With history
from djgentelella.history.api import BaseViewSetWithLogs
from djgentelella.history.utils import add_log, DELETION
class CustomerViewSet(BaseViewSetWithLogs):
...
def perform_destroy(self, instance):
# add log for history
add_log(
self.request.user,
instance,
DELETION,
"customer",
[],
change_message=_("Deleted"),
)
# add user to deleted_by for trash
instance.delete(user=self.request.user)
In both cases, you must override the perform_destroy method.
Forms¶
When using forms, you should exclude the is_deleted field:
from djgentelella.forms.forms import GTForm
import djgentelella.widgets.core as gtw
class CustomerForm(GTForm, forms.ModelForm):
class Meta:
model = Customer
fields = "__all__"
exclude = ["is_deleted"]
widgets = {
"name": gtw.TextInput,
"email": gtw.EmailInput,
"phone_number": gtw.PhoneNumberMaskInput,
}
Template Example¶
Example HTML template for Trash:
<div class="card mt-2">
<div class="card-body">
<div class="card-title titles">
<h2 class="text-center"> {% trans 'Trash' %} </h2>
</div>
<div class="row mt-3 ">
<div class="col-12">
<table class="table table-hover table-striped w-100 table-responsive" id="table-trash">
</table>
</div>
</div>
</div>
</div>
{# Modals actions #}
{% url "api-trash-list" as list_trash_url %}
{% trans 'Delete Instance' as delete_trash_tittle %}
{% include 'gentelella/blocks/modal_template_delete.html'
with form=form_delete form_id="delete_trash_form"
id="delete_trash_modal" title=delete_trash_tittle
url=detail_trash_url %}
JavaScript Initialization¶
const trash_urls = {
list_url: "{% url 'api-trash-list' %}",
restore_url: "{% url "api-trash-restore" 0 %}",
destroy_url: "{% url "api-trash-detail" 0 %}",
}
const modal_trash_ids = {
destroy: "#delete_trash_modal",
}
const actions_trash = {
table_actions: [],
object_actions: [
{
'name': "restore",
'action': 'restore',
'in_action_column': true,
'i_class': 'fa fa-undo',
'method': 'POST',
'title': gettext("Restore"),
data_fn: function (data) {
return data;
}
}
],
title: 'Actions',
className: "no-export-col"
}
const datatable_trash_inits = {
columns: [
{data: "id", name: "id", title: "ID", type: "string", visible: false},
{
data: "created_at",
name: "created_at",
title: gettext("Deleted Date"),
visible: true,
type: "date",
dateformat: document.datetime_format
},
{
data: "deleted_by",
name: "deleted_by",
title: gettext("Deleted by"),
type: "readonly",
visible: true,
},
{
data: "object_repr",
name: "object_repr",
title: gettext("Description"),
type: "readonly",
visible: true,
},
{
data: "model_name",
name: "content_type__model",
title: gettext("Model"),
type: "readonly",
visible: true,
},
{
data: "actions",
name: "actions",
title: gettext("Actions"),
type: "string",
visible: true,
},
],
addfilter: true,
}
const trash_config = {
datatable_element: "#table-trash",
modal_ids: modal_trash_ids,
actions: actions_trash,
datatable_inits: datatable_trash_inits,
add_filter: true,
relation_render: {'field_autocomplete': 'text'},
delete_display: data => {
return `${gettext("Registration ID")}: ${data['id']} <br>
${gettext('Model')}: ${data["model_name"]} <br>
<span class="text-danger">
${gettext("This action will permanently delete the register, as well as all related data.")}
</span>`;
},
icons: icons,
urls: trash_urls,
}
const trash_crud = ObjectCRUD("trashcrudobj", trash_config)
trash_crud.restore = function (data) {
const url = trash_urls.restore_url.replace("0", data.id)
$.ajax({
url: url, type: "POST", data: {}, headers: {
"X-CSRFToken": getCookie('csrftoken'),
}, success: function (response) {
if (response.result) {
Swal.fire({
icon: 'success',
title: gettext('Success'),
text: gettext(response.detail),
confirmButtonText: gettext('Accept'),
})
ocrud.datatable.ajax.reload() // only for example
trash_crud.datatable.ajax.reload()
} else {
window.location.reload();
}
}, error: function (xhr, status, error) {
console.error("Error executing the API:", error);
Swal.fire({
icon: 'error',
title: gettext('Error'),
text: gettext("Sorry, an error occurred."),
confirmButtonText: gettext('Accept'),
})
}
});
}
trash_crud.init();
Object Managers¶
When inheriting from DeletedWithTrash, three different managers are available to handle objects:
objects = ObjectManager()→ Returns only non-deleted objects.objects_with_deleted = AllObjectsManager()→ Returns all objects, including deleted.objects_deleted_only = DeletedObjectsManager()→ Returns only deleted objects.
This allows flexible querying and management of your application data.
Behaviour notes¶
restoregoes throughget_object(): a subclass that scopesget_queryset()(a per-tenant trash) also scopes what can be restored.Restoring an entry whose original object no longer exists answers
410 Gone; hard-deleting such an orphan removes the trash row.perform_destroyneeds no override when the viewset extendsBaseViewSetWithLogs: the base logs the action and callsinstance.delete(user=request.user)sodeleted_byis recorded.A queryset-level
delete(user=...)onDeletedWithTrashmanagers creates the trash rows too, so a bulk soft-delete stays recoverable from the trash screen.Restoring is gated by
djgentelella.change_trasheverywhere (viewset and serializer actions).
Deletion context and multi-tenant scoping¶
A deleted object rarely stands alone: it belonged to an organization, a
project, a folder. delete() — instance and queryset level — accepts
related_objects (a model instance or an iterable of instances; bare pks
raise ValueError) and records one TrashRelation row per instance next
to the Trash entry:
document.delete(user=request.user,
related_objects=[organization, project])
Document.objects.filter(...).delete(user=request.user,
related_objects=[organization])
The first deletion that provides context wins: an entry already carrying relations keeps them on a re-delete. Relations disappear with their entry (restore or hard delete) through the cascade.
That context is what a multi-tenant trash screen scopes by. TrashViewSet
offers two hooks, mirroring HistoryViewSet:
The generic query params
related_contenttype(app.model) andrelated_id(repeatable) narrow the list to entries related to the given object.scope_queryset(queryset)is the last word on what one request may see; it also guardsrestoreanddestroy, which resolve throughget_object(). Override it per tenant:class OrgTrashViewSet(TrashViewSet): def scope_queryset(self, queryset): return queryset.filter( gt_relations__content_type=self.org_ctype, gt_relations__object_id=self.kwargs['org_pk'], ).distinct()
recordsTotal follows the scoped queryset, so the count never leaks the
size of other tenants’ trash.