from django.utils import timezone from django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ class TrackableModel(models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("créé le")) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.PROTECT, related_name="+", verbose_name=_("créé par"), ) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("modifié le")) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.PROTECT, related_name="+", verbose_name=_("modifié par"), ) obsolete = models.BooleanField(default=False, verbose_name=_("obsolète")) obsolete_at = models.DateTimeField( null=True, blank=True, verbose_name=_("obsolète le") ) class Meta: abstract = True def soft_delete(self, cascade_m2m=True): """ Soft delete of trackable models. Objct becomes obsolete and obsolescence date is indicated. """ if cascade_m2m: self._cascade_soft_delete_m2m() self.obsolete = True self.obsolete_at = timezone.now() self.save(update_fields=["obsolete", "obsolete_at"]) def _cascade_soft_delete_m2m(self): """ Propagation of soft delete to M2M fields. """ for field in self._meta.get_fields(): if field.many_to_many and not field.auto_created: through_model = field.remote_field.through if issubclass(through_model, TrackableModel): field_name = None for through_field in through_model._meta.get_fields(): if through_field.related_model == self.__class__: field_name = through_field.name break if field_name: filter_kwargs = {field_name: self} for relation in through_model.objects.filter( **filter_kwargs, obsolete=False ): relation.soft_delete(cascade_m2m=False) def restore(self, cascade_m2m=True): """ Restore an object that was soft deleted. """ if cascade_m2m: self._cascade_restore_m2m() self.obsolete = False self.obsolete_at = None self.save(update_fields=["obsolete", "obsolete_at"]) def _cascade_restore_m2m(self): for field in self._meta.get_fields(): if field.many_to_many and not field.auto_created: through_model = field.remote_field.through if issubclass(through_model, TrackableModel): field_name = None for through_field in through_model._meta.get_fields(): if through_field.related_model == self.__class__: field_name = through_field.name break if field_name: filter_kwargs = {field_name: self} for relation in through_model.objects.filter( **filter_kwargs, obsolete=True ): relation.restore(cascade_m2m=False)