How to Raise DoesNotExist - Django SoftDelete? - django

I have a Abstract Model SoftDelete like follow.
class SoftDeleteManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class SoftDeleteModel(models.Model):
is_deleted = models.BooleanField(default=0)
deleted_at = models.DateTimeField(null=True)
objects = SoftDeleteManager()
def delete(self):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
class Meta:
abstract = True
class Employee(SafeDeleteModel):
pass
Whenever model gets deleted i set is_deleted to True and updating the timestamp deleted_at, and created the custom manager to override initial queryset which return only non deleted fields(is_deleted=False).
employee = Employee.objects.get(pk=1)
employee.delete()
employee.refresh_from_db() // not raising DoesNotExist
But lets say i have a Employee model which uses the SafeDeleteModel for soft delete, after deleting the model like Employee.objects.get(pk=1).delete() when i call employee.refresh_from_db(),its not raising DoesNotExist, but updates the value of is_deleted, deleted_at as expected, what mistake i made here, why its not raising DoesNotExist?

There's been a change in Django 2.1: refresh_from_db() now uses a model's _base_manager, not the _default_manager anymore, like for related queries. This to ensure an object can be refreshed even if it cannot be found by the default manager.
So you should set your SoftDeleteManager as the base manager using base_manager_name. But note this comment:
Base managers aren’t used when querying on related models. For example, if the Question model from the tutorial had a deleted field and a base manager that filters out instances with deleted=True, a queryset like Choice.objects.filter(question__name__startswith='What') would include choices related to deleted questions.
Also I don't know how you would retrieve any object that has been deleted after you make this change, except if you make a special manager to not filter the deleted objects (e.g. deleted_objects).
Note also that I would expect the safedelete package you referred to in your comments to have the same issue, as it's not changing the _base_manager either.

Related

Django (DRF) ManyToMany field choices / limit

Working with Django REST Framework I am wondering if it's possible to limit the choices / options of a ManyToMany field on a model to a specific QuerySet?
Using the models below (scroll down to see models), I am curious if it's possible to define this limit in the definition of the model, to achieve the following:
# Having the following Employee instance
emp = Employee(...)
# Should return only the instances with value 'case' in EmployeeSubstitute.type field
emp.substitute_case.all()
# Should return only the instances with value 'phone' in EmployeeSubstitute.type field
emp.substitute_phone.all()
Models:
class Employee(models.Model):
substitute_case = models.ManyToMany(through=EmployeeSubstitute, ...)
substitute_phone = models.ManyToMany(through=EmployeeSubstitute, ...)
class EmployeeSubstitute(models.Model):
from = models.ForeignKey(Employee, ...)
to = models.ForeignKey(Employee, ...)
type = models.CharField(choices=..., ...) # choose between type 'case' and 'phone'
I see that there's the limit_choices_to parameter, but that's not what I am looking for, since that only effects the options shown when using a ModelForm or the admin.
Well, ManyToManyField returns related objects and as docs state
By default, Django uses an instance of the Model._base_manager manager
class when accessing related objects (i.e. choice.question), not the
_default_manager on the related object. This is because Django needs to be able to retrieve the related object, even if it would otherwise
be filtered out (and hence be inaccessible) by the default manager.
If the normal base manager class (django.db.models.Manager) isn’t
appropriate for your circumstances, you can tell Django which class to
use by setting Meta.base_manager_name.
Base managers aren’t used when querying on related models, or when
accessing a one-to-many or many-to-many relationship. For example, if
the Question model from the tutorial had a deleted field and a base
manager that filters out instances with deleted=True, a queryset like
Choice.objects.filter(question__name__startswith='What') would include
choices related to deleted questions.
So if I read it correctly, no, it's not possible.
When you do queries and have through in your ManyToManyField, Django complains you should run these queries on your through model, rather than the "parent". I can't find it in the docs but I remember seeing it a few times.
substitute_case and substitute_phone is something that belongs to substitute and it is it's type. So just do that instead of creating those columns in Employee.
from django.db import models
class SubstituteTypes(models.TextChoices):
case = "case", "case"
phone = "phone", "phone"
class EmployeeSubstituteQueryset(models.QuerySet):
def from_employee(self, e):
return self.filter(_from=e)
def case(self):
return self.filter(type=SubstituteTypes.case)
def phone(self):
return self.filter(type=SubstituteTypes.phone)
class Employee(models.Model):
substitute = models.ManyToManyField(through='EmployeeSubstitute', to='self')
class EmployeeSubstitute(models.Model):
_from = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='a')
to = models.ForeignKey(Employee, on_delete=models.PROTECT, related_name='b')
type = models.CharField(choices=SubstituteTypes.choices, max_length=5, db_index=True)
objects = EmployeeSubstituteQueryset.as_manager()
Then, once you get your emp object (or only its id), you can do
EmployeeSubstitute.objects.from_employee(emp).case().all()
which is designed in Django philosophy.

"list_editable" doesn't work in admin changelist view, using custom get_queryset() method

I use custom abstract model with manager throughout my project.
class BaseQueryset(models.QuerySet):
pass
class BaseManager(models.Manager):
queryset_class = BaseQueryset
def get_queryset(self, exclude_no_published=True):
""" exclude all objects with is_published=False by default """
q = self.queryset_class(self.model)
if exclude_no_published:
q = q.exclude(is_published=False)
return q
def all_objects(self):
""" allows geting all objects in admin """
return self.get_queryset(exclude_no_published=False)
class BaseAbstractModel(models.Model):
is_published = models.BooleanField(default=True)
objects = BaseManager()
class Meta:
abstract = True
All models inherit from this abstract model and I need a way to represent all objects in admin. So I wrote my own mixin for admin classes with get_queryset method
class AdminFullQuerysetMixin(object):
def get_queryset(self, request):
"""
Allows showing all objects despite on is_public=False
"""
qs = self.model.objects.all_objects()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
There is my typical admin class:
#admin.register(SomeModel)
class SomeModelAdmin(AdminFullQuerysetMixin, admin.ModelAdmin):
list_display = ('name', 'slug', 'is_published')
list_filter = ('is_published',)
list_editable = ('is_published',)
All works fine, I can see all objects in admin whether with is_published False or True. But such attributes like list_filter or list_editable don't work, when I use it in admin objects list page. There is no exception provided, just text at the top of the list: "Please correct the error below".
What methods except get_queryset should I override for solving my problem?
You may want to read this :
if you use custom Manager objects, take note that the first Manager
Django encounters (in the order in which they’re defined in the model)
has a special status. Django interprets the first Manager defined in a
class as the “default” Manager, and several parts of Django (...) will
use that Manager exclusively for that model. As a result, it’s a
good idea to be careful in your choice of default manager in order to
avoid a situation where overriding get_queryset() results in an
inability to retrieve objects you’d like to work with.
I strongly suspect you fell upon one of those cases...
The solution would then be to change your model to:
class BaseAbstractModel(models.Model):
is_published = models.BooleanField(default=True)
# this one will be the default manager
all_objects = models.Manager()
# and this one will be known as 'objects'
objects = BaseManager()
class Meta:
abstract = True
Then you can remove your AdminFullQuerysetMixin (or rewrite it's get_queryset() method to use self.model._default_manager instead)
NB : I may of course be wrong and the problem be totally unrelated ;)

Model Method from rest_framework modelSerializer

this is a simple question but I'm very new to django-rest-framework.
I was wondering if there is any way to access a method defined on the model from the serializer.?
Say I have a model
class Listing(models.Model):
listingid = models.BigIntegerField(primary_key=True)
mappingid = models.BigIntegerField()
projectlevelid = models.IntegerField()
subsellerid = models.IntegerField()
iscreatedbyadmin = models.BooleanField(default=None, null=True)
createdon = models.DateTimeField(auto_now_add=True, editable=False)
validationstatus = models.SmallIntegerField(default=0)
def is_project(self):
""" Returns True if listing is of Project Type (projectlevelid=6) else False"""
if self.projectlevelid == 6:
return True
else:
return False
def get_project_info(self):
"""Returns False if listing is not mapped to a project, else returns the project info"""
if self.is_project() == False:
return False
return models.Project.objects.get(projectid=self.mappingid)
Is it possible for the serializer
class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = models.MasterListing
to have access to Listing.is_project i.e. for an object of the Listing model, can the serializer call its is_project method?
If so, can I set a field in the serializer such that if is_project returns true, the field is populated?
I am trying for something like this,
class ListingSerializer(serializers.ModelSerializer):
project = serializers.SomeRELATEDFieldTYPE() # this field if populated if the `is_project` is true
class Meta:
model = models.MasterListing
I understand I can do this using some combination of required=False and SerializerMethodField, but maybe there is a simpler way?.
Note: It is not possible for me to set a foreign key to the mappingid, since it depends on the projectlevelid. I also can't affect this relationship so no further normalization is possible. I know that there might be some way using content-types, but we are trying to avoid that if it is possible..
EDIT: I solved the problem, but not as the question specified.
I used this:
class ListingSerializer(serializers.ModelSerializer):
project = serializers.SerializerMethodField()
def get_project(self, obj):
"""Returns False if listing is not mapped to a project, else returns the project info"""
if str(obj.projectlevelid) == str(6):
projectObj = models.Project(projectid=obj.mappingid)
projectObjSerialized = ProjectSerializer(projectObj)
return projectObjSerialized.data
return False
class Meta:
model = models.MasterListing
So, the original question still stands: "Is it possible for the modelSerializer to access its models methods?"
Also, another problem that now appears is, can I make the serializer exclude fields on demand, i.e. can it exclude mappingid and projectlevelid if it is indeed a project?
For your first question source attribute is the answer, citing:
May be a method that only takes a self argument, such as
URLField('get_absolute_url')
For your second answer, yes it is also possible. Check the example it provides in their docs: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
PS: I really love drf for its very complete documentation =).
EDIT
To use the source attribute you can just declare a new explicit field like so:
is_project = serializers.BooleanField(source='is_project')
With this, is_project field has the value of the is_project method of your instance. Having this, when creating the dynamic serializer (by modifying its init method) you can add the 'project' field if it's True.
#argaen is absolutely right, source is a DRF core argument, and would most definitely solve your problem. However, it's redundant to use source, if the field name is the same as the source. So the above answer won't require you specify source, since field name is_project is the same as source name is_project.
So in your case:
is_project = serializers.BooleanField()

Updating custom model manager doesn't take effect

I have an Order model with a custom model manager called OrderManager as follows.
class Order(models.Model):
# Model fields are declared here.
...
objects = OrderManager()
all_objects = models.Manager()
class OrderManager(models.Manager):
def get_queryset(self):
return super(OrderManager, self).get_queryset().filter(Q(x='y') | Q(x='y'))
I updated OrderManager class by adding another filter to the queryset, and the new implementation turned out to be
class OrderManager(models.Manager):
def get_queryset(self):
return super(OrderManager, self).get_queryset().filter(~Q(order_status='Cancelled'), Q(x='y') | Q(x='y'))
However, when I retrieve data from the Order model using the updated manager, it ignores the changes and still brings the objects whose order_status is 'Cancelled'.
orders = Order.objects.all() # Retrieves cancelled orders as well.
On the other hand, applying the last additional filter explicitly works:
orders = Order.objects.filter(~Q(order_status='Cancelled') # Ignores the cancelled orders.
Is it a migration issue or what? I feel like there is a silly mistake I'm making somewhere (probably not related to this code) but I just got blind. Any suggestions are much appreciated.
ps: I'm using Django 1.5.5 on Webfaction.
Before django 1.6 method was named get_query_set, not get_queryset (check additional underscore).
So in django 1.5 and earlier it is needed to override get_query_set method:
class OrderManager(models.Manager):
def get_query_set(self):
return super(OrderManager, self).get_query_set().filter(~Q(order_status='Cancelled'), Q(x='y') | Q(x='y'))

Overriding QuerySet.delete() in Django

I have a Django model that holds settings core to the function of an app. You should never delete this model. I'm trying to enforce this application-wide. I've disabled the delete function in the admin, and also disabled the delete method on the model, but QuerySet has it's own delete method. Example:
MyModel.objects.all()[0].delete() # Overridden, does nothing
MyModel.objects.all().delete() # POOF!
Ironically, the Django docs say has this to say about why delete() is a method on QuerySet and not Manager:
This is a safety mechanism to prevent you from accidentally requesting Entry.objects.delete(), and deleting all the entries.
How having to include .all() is a "safety mechanism" is questionable to say the least. Instead, this effectively creates a backdoor that can't be closed by conventional means (overriding the manager).
Anyone have a clue how to override this method on something as core as QuerySet without monkey-patching the source?
You can override a Manager's default QuerySet by overriding the Manager.get_query_set() method.
Example:
class MyQuerySet(models.query.QuerySet):
def delete(self):
pass # you can throw an exception
class NoDeleteManager(models.Manager):
def get_query_set(self):
return MyQuerySet(self.model, using=self._db)
class MyModel(models.Model)
field1 = ..
field2 = ..
objects = NoDeleteManager()
Now, MyModel.objects.all().delete() will do nothing.
For more informations: Modifying initial Manager QuerySets
mixin approach
https://gist.github.com/dnozay/373571d8a276e6b2af1a
use a similar recipe as #manji posted,
class DeactivateQuerySet(models.query.QuerySet):
'''
QuerySet whose delete() does not delete items, but instead marks the
rows as not active, and updates the timestamps
'''
def delete(self):
self.deactivate()
def deactivate(self):
deleted = now()
self.update(active=False, deleted=deleted)
def active(self):
return self.filter(active=True)
class DeactivateManager(models.Manager):
'''
Manager that returns a DeactivateQuerySet,
to prevent object deletion.
'''
def get_query_set(self):
return DeactivateQuerySet(self.model, using=self._db)
def active(self):
return self.get_query_set().active()
and create a mixin:
class DeactivateMixin(models.Model):
'''
abstract class for models whose rows should not be deleted but
items should be 'deactivated' instead.
note: needs to be the first abstract class for the default objects
manager to be replaced on the subclass.
'''
active = models.BooleanField(default=True, editable=False, db_index=True)
deleted = models.DateTimeField(default=None, editable=False, null=True)
objects = DeactivateManager()
class Meta:
abstract = True
other interesting stuff
http://datahackermd.com/2013/django-soft-deletion/
https://github.com/hearsaycorp/django-livefield