Package: django-timestampable
Django TimestampsTimestamps and Soft Delete Patterns in Django Models.How to installOption a)$pipinstalldjango-timestampableOption b)To install django-timestampable withDjango Rest Frameworkincluded:$pipinstall"django-timestampable[drf]"You can use the first option if you have Django Rest Framework already installed.And add "timestamps" to your INSTALLED_APPS settingsINSTALLED_APPS=[# ...'timestamps',]Or, if you installed withDjango Rest Framework:INSTALLED_APPS=[# ...'rest_framework','timestamps',]Usagea) For models you want timestamps, just inherit Timestampable:fromtimestamps.modelsimportmodels,TimestampableclassYourModel(Timestampable):# your fields here ...b) For models you want soft-delete, just inherit SoftDeletes:fromtimestamps.modelsimportmodels,SoftDeletesclassYourModel(SoftDeletes):# your fields here ...c) If you want both, you can also inherit from Model for shorter convenience:# to this:fromtimestamps.modelsimportmodels,Model# explicit import Model (which contains timestamps)# instead of:# from django.db import models# Explicitly import of "Model" is required# because models.Model is the original from Django models moduleclassYourModel(Model):# your fields here ...Soft DeletingTo get all objects without the deleted ones:queryset = YourModel.objectsTo get only deleted objects:queryset = YourModel.objects_deletedTo get all the objects, including deleted ones:queryset = YourModel.objects_with_deletedTo soft delete an instancesome_model=MyModel.objects.first()some_model.delete()# or some_model.delete(hard=False)To restore an instancesome_model=MyModel.objects_deleted.first()some_model.restore()To hard delete an instancesome_model=MyModel.objects.first()some_model.delete(hard=True)To bulk soft delete a querysetqs=MyModel.objects# you can also apply filters to bulk delete a subset: qs = MyModel.objects.filter(...)qs.delete()# or qs.delete(hard=False)To bulk hard delete a querysetqs=MyModel.objects# ... bulk hard delete a subset: qs = MyModel.objects.filter(...)qs.delete(hard=True)To bulk restore a querysetqs=MyModel.objects_deleted# ... bulk restore a subset: qs = MyModel.objects_deleted.filter(...)qs.restore()# or qs.delete(hard=False)Signals for Soft Deleting and RestoringYou have 4 signals available that you can listen in your project:pre_soft_deletepost_soft_deletepre_restorepost_restoreTo use them, just import the signals and register listeners for them. Eg:Pre Soft Deletefromtimestamps.signalsimportpre_soft_deletefromdjango.dispatchimportreceiver@receiver(pre_soft_delete)defon_pre_soft_delete(sender,instance,**kwargs):print(f"Model{sender}with id{instance.pk}will be deleted!")Post Soft Deletefromtimestamps.signalsimportpost_soft_deletefromdjango.dispatchimportreceiver@receiver(post_soft_delete)defon_post_soft_delete(sender,instance,**kwargs):print(f"Model{sender}with id{instance.pk}was deleted at{instance.deleted_at}!")Pre Restorefromtimestamps.signalsimportpre_restorefromdjango.dispatchimportreceiver@receiver(pre_restore)defon_pre_restore(sender,instance,**kwargs):print(f"Model{sender}with id{instance.pk}deleted at{instance.deleted_at}will be restored!")Post Restorefromtimestamps.signalsimportpost_restorefromdjango.dispatchimportreceiver@receiver(post_restore)defon_post_restore(sender,instance,**kwargs):print(f"Model{sender}with id{instance.pk}restored!")If you're using DRFYou can use the SoftDeleteModelViewSet along with DefaultRouter present in this package
and you will have access to a complete CRUD on soft deleted objects as well.
This 2 classes allows you to expose:Consider a Dummy Model that inherits from SoftDelete.You can have all routes for CRUD operations on this model:VERBURL PATHDESCRIPTIONGET/dummy/gets all the objects, without the deleted onesPOST/dummy/creates a new objectDELETE/dummy/[?permanent=<true,false>]deletes all objects (or a filtered subject). allows hard-delete. Default: soft-deleteGET/dummy/<pk>/gets a non-deleted object (by primary key)POST/dummy/<pk>/updates an object (by primary key)PATCH/dummy/<pk>/partial updates an object (by primary key)DELETE/dummy/<pk>/[?permanent=<true,false>]deletes a non-deleted object (by primary key)PATCH/dummy/restore/restore all objects (or a filtered subject)PATCH/dummy/<pk>/restore/restores a soft-deleted object (by primary key)GET/dummy/deleted/gets all deleted objectsGET/dummy/deleted/<pk>/gets a deleted object (by primary key)GET/dummy/with-deleted/get all objects, deleted includedGET/dummy/with-deleted/<pk>/get an object (by primary key)The query parameter "permanent" it's case-sensitive and can also be one of the values:truthful_options=['t','T','y','Y','yes','Yes','YES','true','True','TRUE','on','On','ON','1',1,True]falsely_options=['f','F','n','N','no','No','NO','false','False','FALSE','off','Off','OFF','0',0,'null',False]How to expose all CRUD operations# dummy/views.pyfromtimestamps.drfimportviewsets# instead of: from rest_framework import viewsetsfrom.modelsimportDummyfrom.serializersimportDummySerializerclassDummyModelViewSet(viewsets.ModelViewSet):queryset=Dummy.objects.all()serializer_class=DummySerializer# dummy/urls.pyfromtimestamps.drfimportrouters# instead of: from rest_framework import routersfrom.viewsimportDummyModelViewSetrouter=routers.DefaultRouter()router.register(r'dummy',DummyModelViewSet)urlpatterns=router.urlsNote AFor security reasons, by default, if you pass to the query parameter "?permanent=true" on a bulk destroy,
the view will not let you hard-delete, raising a PermissionDenied.
If you want to enable it on your project, just add to the project settings:TIMESTAMPS__BULK_HARD_DELETE=TrueIt's here to prevent users of "forgetting" that the routes also expose bulk hard-delete by default.
In production, you can set this flag to True and manage hard-deleting using DRF permissions.Hard-deleting one object at time is allowed by default.NOTE BBulk actions of restoring and deleting returns no content (status code 204) by default.
If you want to return a response with the number of deleted/restored objects, just add this setting:TIMESTAMPS__BULK_RESPONSE_CONTENT=TrueExample of returned response:{"count": 3 }Note CIf you don't want to expose all the crud operations, be free to register as:router.register(r'dummy',DummyModelViewSet.as_view({'get':'list_with_deleted'}))# e.g.And you can always use the mixins instead and create your APIViews:fromrest_frameworkimportgenericfromtimestamps.drf.mixinsimportListDeletedModelMixinfrom.modelsimportDummyfrom.serializersimportDummySerializerclassMyView(ListDeletedModelMixin,generic.GenericAPIView):queryset=Dummy.objects.all()serializer_class=DummySerializerdeflist_deleted(self,request,*args,**kwargs):# optional. your code goes here...Internally, the ListDeletedModelMixin just calls the method ListModelMixin.list(self, request, *args, **kwargs).
The method of determining if the queryset must get all objects, only the deleted or all with deleted is done using AOP,
which means that the method GenericAPIView.get_queryset() is advised at runtime to map the current action
to the correct queryset the view needs.If you don't inherit from generic.GenericAPIView, you must be aware that, for this type of scenarios,
you need to override the method get_queryset() to return the objects that matches your needs.