I'm using the following setup to implement soft deletes in Django. I'm not very familiar with Django under the hood so I'd appreciate any feedback on gotchas I might encounter. I'm particular uncomfortable subclassing a QuerySet.
The basic idea is that the first call to delete on a MyModel changes MyModel's date_deleted to the current datetime. A second delete will actually delete the object. (Catching a delete requires two overrides, one on the object and one on the QuerySet, which can bypass an object's delete method.) Since the default manager will hide deleted objects, deleted objects disappear and must be explicitly requested via the deleted_objects manager.
Using this setup requires defining DeletionQuerySet and DeletionManager and adding date_deleted, objects, and deleted_objects to your model(s).
Thanks,
P.S., forgot to mention that this method of filtering objects out of the default manager is strongly discouraged!
class DeletionQuerySet(models.query.QuerySet):
def delete(self):
prev_deleted = self.filter(date_deleted__isnull=False)
prev_deleted.actual_delete()
prev_undeleted = self.filter(date_deleted__isnull=True)
prev_undeleted.update(date_deleted=datetime.datetime.now())
def actual_delete(self):
super(DeletionQuerySet, self).delete()
class DeletionManager(models.manager.Manager):
# setting use_for_related_fields to True for a default manager ensures
# that this manager will be used for chained lookups, a la double underscore,
# and therefore that deleted Entities won't popup unexpectedly.
use_for_related_fields = True
def __init__(self, hide_deleted=False, hide_undeleted=False):
super(DeletionManager, self).__init__()
self.hide_deleted = hide_deleted
self.hide_undeleted = hide_undeleted
def get_query_set(self):
qs = DeletionQuerySet(self.model)
if self.hide_deleted:
qs = qs.filter(date_deleted__isnull=True)
if self.hide_undeleted:
qs = qs.filter(date_deleted__isnull=False)
return qs
class MyModel(models.Model):
# Your fields here...
date_deleted = models.DateTimeField(null=True)
#the first manager defined in a Model will be the Model's default manager
objects = DeletionManager(hide_deleted=True)
deleted_objects = DeletionManager(hide_undeleted=True)
def delete(self):
if self.date_deleted is None:
self.date_deleted = datetime.datetime.now()
self.save()
else:
super(Agreement, self).delete()
I think anything with current in use, popular, technologies, there is no way to have problem domain agnostic, generic soft deletes.
I think it is more linked to historical/history oriented database systems than to what we are used.
I recommend you to not circumvent django's delete (which is a hard delete). Keep as is.
The "delete" that you most likely will have in our system, is in 90% of the case, a visual delete ...
In this regard, try to find synonyms with delete for your specific domain problem and do this from the start of the project.
Because complain that a IsVisible, IsUnpublished (even IsDeleted) mess up your queries, they complain that you must always be careful to include them...
But this is obviously ignorance of the domain problem, if the domain has objects that can be made invisible, or become unpublished - of course when you query the list of all the objects you want to display, you should FROM THE START, QUERY ALL THE OBJECTS that are not visible and unpublished because this is how your domain problem is solved in a complete form.
Cheers.
Related
Is there an option to disable the delete function in the project object if a non-admin user tries to do this via delete function?
For example:
class Product(models.Model):
name = models.CharField(max_lenght=200)
show = models.BooleanField()
logs = models.TextField()
And now if we have in code of project product.delete() we block it through the model property and check if it is an admin, if it is not an admin add a reflection in the field.:
class Product(models.Model):
name = models.CharField(max_lenght=200)
show = models.BooleanField(default=True)
logs = models.TextField()
def delete(self, *args, **kwargs):
super().save(*args, **kwargs)
if request.user.is_superuser
Product.delete()
else:
show = False
logs = 'ther user try remove this ads in time: 08.11 04.11.2022'
save()
It's the responsibility of the view to check that the user (in request.user) has sufficient privilege to perform the requested operation. There's no standard way for a Django object method to obtain the current user, and a programmer with access through the Django shell >>> obj.delete() has to be prevented by other means (such as the risk of losing his job). There simply is no Django logged-in user in that context, but the DB to which it has access ought not to be a production one.
Out on a limb, it is possible to disable the object's delete() method completely by subclassing it to a no-op or to raise an exception. Deleting such an object would then require other means. Either relying on a CASCADE when some other object was deleted, or using raw SQL (the psql command, if PostgreSQL) to remove its data row from the DB table.
(I haven't done the latter, but there are certain objects in the project I am working on which should never require deletion under any normal circumstances, and for which there are no delete views or similar. They will accumulate at the rate of around one per week in production, and removing ones which are completely stale is a problem which can safely be deferred until the year 2100 or later :-)
I have a particular model and that model has finegrained access settings. Something like:
class Document(models.Model):
...
access = models.ManyToManyField(Group)
Groups consist of particular tags, and those tags are linked to users. Long story short, one way or another the documents are only accessible by particular users. It is very important that this check does not slip through the cracks. So I can see a number of options. One is that every time I access a Document, I add the check:
Document.objects.filter(access__group__tag__user=request.user)
But there are two drawbacks: a) I query the documents model > 100 times in my views so I will have a LOT of repeated code, and b) it's quite likely that someone will at some point forget to add this restriction in, leaving documents exposed.
So I am thinking that overwriting the objects() makes most sense, through a custom manager. That way I don't duplicate code and I don't risk forgetting to do this.
class HasAccessManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(access__group__tag__user=request.user)
class Document(models.Model):
...
access = models.ManyToManyField(Group)
objects = HasAccessManager()
However, the problem becomes that request is not accessible there:
name 'request' is not defined
How to solve this? Or are there better solutions?
Create a mixin that your views inherit from. This will prevent having duplicated code everywhere. You'll want to write unit tests to make sure your views are locked down appropriately.
class HasAccessMixin(object):
def get_queryset(self):
qs = super().get_queryset()
# you can still leverage a custom model manager here if you want
# qs = qs.custom_method(access__group__tag__user=self.request.user)
qs = queryset.filter(access__group__tag__user=self.request.user)
return qs
class SomeListView(HasAccessMixin, ListView):
...
class SomeDetailView(HasAccessMixin, DetailView):
...
I followed this example and it works great but I'm wondering if I can put in an exception so that when I am in the admin all objects show up (active and inactive). This might be simple but I can't find how to do it in the docs.
Here's what my manager looks like now:
class ShareManager(models.Manager):
def get_query_set(self):
return super(ShareManager, self).get_query_set().filter(active=True)
There are several solutions that come to mind:
define what queryset to use for change list with ModelAdmin.queryset().
install 2 managers on your model, the first one that admin finds will be used as the default one AFAIK.
class SomeThing(models.Model):
objects = models.Manager()
shares = ShareManager()
add new method on your custom manager that returns only active stuff and leave get_query_set as it is by default.
class ShareManager(models.Manager):
def get_active_items(self):
return self.get_query_set().filter(active=True)
Follow-up
I think the most suitable solution in your case would be combining #1 and variation of #2.
Set your custom manager as objects so everyone can access it (I think this should work for your reusability problem) and also install default manager on your model and use it in ModelAdmin.queryset().
class SomeThing(models.Model):
objects = ShareManager()
admin_objects = models.Manager()
I should have included ModelAdmin.queryset() method example too, so here it is.
def queryset(self, request):
qs = self.model.admin_objects.get_query_set()
# TODO: this should be handled by some parameter to the ChangeList.
# otherwise we might try to *None, which is bad ;)
ordering = self.ordering or ()
if ordering:
qs = qs.order_by(*ordering)
return qs
Note the line qs = self.model.admin_objects.get_query_set() is working with admin_objects
which is the instance of plain manager and includes unpublished items.
The rest of this implementation of queryset method is default Django's implementation which usually calls qs = self.model._default_manager.get_query_set().
I hope this clears things up a bit.
I have altered the default manager on some of the objects which a GenericForeignKey() can reference such that those objects may no longer appear within that default manager.
I have other managers which will be able to find these deleted objects, but I see no way to tell the content types framework about them. Is this possible?
I am implementing 'soft deletion' with some models which involves the following managers:
from django.db import models
SDManager(models.Manager):
def get_query_set(self):
return super(SDManager, self).get_query_set().filter(is_deleted=False)
SDDeletedManager(models.Manager):
def get_query_set(self):
return super(SDDeletedManager, self).get_query_set().filter(is_deleted=True)
This allows me to do the following:
SDModel(models.Model):
# ...
objects = SDManager() # Only non (soft) deleted objects
all_objects = models.Manager() # The default manager
deleted_objects = SDDeletedManager() # Only (soft) deleted objects
When using a GenericForeignKey() field in a model to reference an object defined such as SDModel, it uses the _default_manager attribute which evaluates to the objects manager, to get the reference. This means it looses references when objects are soft deleted.
This was one of the main reasons I was using GenericForeignKey() fields. A solution I have been milling over is implementing a lesser version of the content types framework, so that I can define my own get_object() which uses the all_objects manager to access the references object.
So my question really is:
Is it possible to use a non-default manager with the existing content types framework so that it finds the soft deleted objects, or will I have to re implement all the parts I need from scratch?
I have the exact same issue as you, and after diving into the documentation/source it looks like Django does not provide an out of the box way to do this. The simplest method I found was to subclass GenericForeignKey and then override the __get__ method.
The troublesome line is where it calls:
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
So this line needs to be rewritten as:
rel_obj = ct.model_class().all_objects.get(pk=getattr(instance, self.fk_field))
It's a little bit hackish but it works, and you then get to use the full power of GenericForeignKey like you usually would.
There must be a problem with super(InviteManager, self).get_query_set() here but I don't know what to use. When I look through the RelatedManager of a user instance,
len(Invite.objects.by_email()) == len(user.invite_set.by_email())
Even if the user does not have any invites. However, user.invite_set.all() correctly returns all of the Invite objects that are keyed to the User object.
class InviteManager(models.Manager):
"""with this we can get the honed querysets like user.invite_set.rejected"""
use_for_related_fields = True
def by_email(self):
return super(InviteManager, self).get_query_set().exclude(email='')
class Invite(models.Model):
"""an invitation from a user to an email address"""
user = models.ForeignKey('auth.User', related_name='invite_set')
email = models.TextField(blank=True)
objects = InviteManager()
'''
u.invite_set.by_email() returns everything that Invite.objects.by_email() does
u.invite_set.all() properly filters Invites and returns only those where user=u
'''
You may want a custom QuerySet that implements a by_email filter. See examples on Subclassing Django QuerySets.
class InviteQuerySet(models.query.QuerySet):
def by_email(self):
return self.exclude(email='')
class InviteManager(models.Manager):
def get_query_set(self):
model = models.get_model('invite', 'Invite')
return InviteQuerySet(model)
Try:
def by_email(self):
return super(InviteManager, self).exclude(email='')
If nothing else, the .get_query_set() is redundant. In this case, it may be returning a whole new queryset rather than refining the current one.
The documentation specifies that you should not filter the queryset using get_query_set() when you replace the default manager for related sets.
Do not filter away any results in this type of manager subclass
One reason an automatic manager is used is to access objects that are related to from some other model. In those situations, Django has to be able to see all the objects for the model it is fetching, so that anything which is referred to can be retrieved.
If you override the get_query_set() method and filter out any rows, Django will return incorrect results. Don’t do that. A manager that filters results in get_query_set() is not appropriate for use as an automatic manager.
Try using .all() in place of .get_query_set(). That seemed to do the trick for a similar problem I was having.
def by_email(self):
return super(InviteManager, self).all().exclude(email='')