SoftDelete in Django - django

Problem Statement
I have created a base model:
class CreateUpdateDeleteModel(models.Model):
from django.contrib.auth import get_user_model
from django.utils.text import gettext_lazy as _
from .managers import BakeryManager
from drfaddons.datatypes import UnixTimestampField
create_date = UnixTimestampField(_('Create Date'), auto_now_add=True)
created_by = models.ForeignKey(get_user_model(), on_delete=models.PROTECT,
related_name='%(app_label)s_%(class)s_creator')
delete_date = models.DateTimeField(_('Delete Date'), null=True, blank=True)
deleted_by = models.ForeignKey(get_user_model(), on_delete=models.PROTECT, null=True, blank=True,
related_name='%(app_label)s_%(class)s_destroyer')
update_date = models.DateTimeField(_('Date Modified'), auto_now=True)
updated_by = models.ForeignKey(get_user_model(), on_delete=models.PROTECT, null=True, blank=True,
related_name='%(app_label)s_%(class)s_editor')
objects = BakeryManager()
class Meta:
abstract = True
I want that in my system, all the elements are soft deleted i.e. whenever an object is deleted, it should behave as follow:
Behave like normal delete operation: Raise error if models.PROTECT is set as value for on_delete, set null if it's models.SET_NULL and so on.
Set delete_date
Never show it anywhere (incl. admin) in any query. Even model.objects.all() should not include deleted objects.
How do I do this?
I am thinking to override get_queryset() which may solve problem 3. But what about 1 & 2?

Very strange request.
Point 1. It's not clear. Show an error where?
Point 2. The delete is managed as a status and not a hide of the data. I would suggest you add a status field and manage the delete using a special function that changes the status. You can override the delete, but be aware that the delete in queryset does not call the Model.delete but run directly SQL code.
Saying that it's a bad idea. The delete must be there, but just not used. You can easily remove the delete form the Django admin and a developer has no reason to delete code unless he/she is playing with the Django shell irresponsibly. (DB backup?)
Point 3. If you don't want to show the data in admin, simply override the Admin ModelForm to hide the data when the STATUS of the object is DELETE. It's bad design manipulate the domain to accommodate the presentation layer.

Related

How to make recursive django admin inline relationships?

I need to create an arbitrarily deep modular structure using Django admin. The nested_admin package has gotten me part of the way there, but there's a key piece of functionality that's still eluding me — the ability to create an object B inline off object A and then create another object A off that instance of object B. Here's a closer look at my models (simplified):
class ChatConditional(models.Model):
triggering_question = models.ForeignKey(
'texts.MessageChain',
on_delete=models.CASCADE,
)
keyword = models.CharField(max_length=50, blank=False, null=False)
keyword_response = models.ForeignKey(
'texts.MessageChain',
related_name='keyword_answer',
on_delete=models.CASCADE,
)
class MessageChain(models.Model):
conditional_trigger = models.ForeignKey(
'texts.ChatConditional',
blank=True, null=True,
)
message = models.TextField(max_length=160, blank=True, null=True)
So in the admin, I want to be able to create a ChatConditional to serve as a response to a certain MessageChain. Then once I create that Conditional, create another MessageChain to send that will potentially link to another Conditional. Here's what my admin models look like:
class SMSConditionalAdmin(nested_admin.NestedStackedInline):
model = ChatConditional
extra = 0
fk_name = "triggering_question"
inlines = [SMSChainAdmin]
class SMSChainAdmin(nested_admin.NestedStackedInline):
model = MessageChain
extra = 0
inlines = [SMSConditionalAdmin]
And now you can likely see the problem: In order to fully define the SMSConditionalAdmin, I need to set up an inline relationship to SMSChainAdmin, and vice versa. I might be missing something obvious, but how can I work around this need for the two admin inlines to recursively refer to one another?
Thanks in advance!

Using UniqueConstraint to solve "get() returned more than one" error in Django admin

I'm trying to access an (unmanaged) model via Django administration dashboard. The model doesn't have a primary key, but instead is unique across three fields.
class MyObjectView(models.Model):
solution_id = models.IntegerField(blank=True, null=True)
scenario_id = models.IntegerField(blank=True, null=True)
object_id = models.CharField(max_length=256, blank=True, null=True)
description= models.CharField(max_length=1000, blank=True, null=True)
year_of_creation = models.DateField(blank=True, null=True)
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'myobject_view'
While I am able to access the list of all items in the admin dashboard, as soon as I try to view one specific item I get the error:
get() returned more than one MyObjectView -- it returned 4!
As per the documentation, I tried adding a UniqueConstraint in the Meta class, but the constraint doesn't seem to have any effect, and the error above persists:
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'myobject_view'
constraints = [
models.UniqueConstraint(fields=['solution_id', 'scenario_id', 'object_id '], name='unique_object'),
]
Is this the proper way to solve the get() returned more than one error? Should the constraint work even on an unmanaged model?
I think if the object is unmanaged adding an UniqueConstraint in the Meta won't insert any constraint in the database.
You need to catch the exception:
try:
the_object = MyObjectView.objects.get(
object_id=object_id, scenario_id=scenario_id, solution_id=solution_id
)
return the_object # or do something different
except MyObjectView.MultipleObjectsReturned:
# multiple objects have the three same values
# manage that case
return None # just as example
Take a look to the reference of MultipleObjectsReturned exception
Another approach is to add .first() to your get(). That guarantees you will get a single result (or None), and will avoid errors at that point.
If the duplicates are a problem it sounds like you need to investigate how they are being created, fix that and then do some clear up.

Django deleting where SET_NULL

I have an issue with a ForeignKey and on_delete=SET_NULL.
When deleting the last_data referenced in my Stuff model, it also deletes the stuff object just as if this was a cascade, which is obviously not what I expected, rather setting the last_data field null.
Here is how my models are defined in two different django apps.
# App 1
class Device(models.Model):
last_data = models.ForeignKey('Data', null=True, blank=True, related_name="last_data_device", on_delete=models.SET_NULL, help_text="Latest data")
class Data(models.Model):
content = models.CharField(max_length=255)
date_created = models.DateTimeField(blank=True, null=False, db_index=True)
device = models.ForeignKey(Device, related_name="data", db_index=True, on_delete=models.CASCADE)
# App 2
class Stuff(models.Model):
device = models.OneToOneField(Device, null=True, blank=True, related_name="stuff", db_index=True, on_delete=models.CASCADE)
last_data = models.ForeignKey(Data, null=True, blank=True, help_text="Latest data", db_index=True, on_delete=models.SET_NULL)
I must have misunderstood how this is linked, what I want is that a stuff object is never deleted when data is removed, but that the last_data reference it has may be nulled when this happens.
How should I write this or what did I do wrong here?
Thanks
PS: Migrations are up to date and db is synced.
Well, seing the answers given, already, it seems I should clarify.
When I do :
>>> stuff = Stuff.objects.get(...)
>>> stuff.last_data.delete()
Is that this is the stuff object that gets removed as "dependancy" and I cannot understand why.
What I'd expect is that the last_data field gets nulled and the stuff object is left alone.
Maybe this is due to communication OneToOneField. If you delete your last_data referenced in your Stuff model, where device connet OneToOne with Device. You can set on_delete=models.SET_NULL on your OneToOne field
Change to models.DO_NOTHING. That would avoid the delete.
If I understood correctly, your problem is that when you do:
>>> stuff = Stuff.objects.get(...)
>>> stuff.last_data.delete()
then the corresponding Data record gets removed from the database as well.
This is expected, and it's how it supposed to work. If all you want to do is to set last_data to NULL, while keeping the Data table intact, then:
>>> stuff.last_data = None
>>> stuff.save()
The purpose of on_delete=SET_NULL is to support the opposite use case: when you delete a Data record, then all Stuff records that were pointing to that Data will get their last_data set to NULL:
>>> stuff = Stuff.objects.get(...)
>>> stuff.last_data
<Data: ...>
>>> Data.objects.all().delete()
>>> stuff.last_data
None

Django user audit

I would like to create a view with a table that lists all changes (created/modified) that a user has made on/for any object.
The Django Admin site has similar functionality but this only works for objects created/altered in the admin.
All my models have, in addition to their specific fields, following general fields, that should be used for this purpose:
created_by = models.ForeignKey(User, verbose_name='Created by', related_name='%(class)s_created_items',)
modified_by = models.ForeignKey(User, verbose_name='Updated by', related_name='%(class)s_modified_items', null=True)
created = CreationDateTimeField(_('created'))
modified = ModificationDateTimeField(_('modified'))
I tried playing around with:
u = User.objects.get(pk=1)
u.myobject1_created_items.all()
u.myobject1_modified_items.all()
u.myobject2_created_items.all()
u.myobject2_modified_items.all()
... # repeat for >20 models
...and then grouping them together with itertool's chain(). But the result is not a QuerySet which makes it kind of non-Django and more difficult to handle.
I realize there are packages available that will do this for me, but is it possible to achieve what I want using the above models, without using external packages? The required fields (created_by/modified_by and their timefields) are in my database already anyway.
Any idea on the best way to handle this?
Django admin uses generic foreign keys to handle your case so you should probably do something like that. Let's take a look at how django admn does it (https://github.com/django/django/blob/master/django/contrib/admin/models.py):
class LogEntry(models.Model):
action_time = models.DateTimeField(_('action time'), auto_now=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.TextField(_('object id'), blank=True, null=True)
object_repr = models.CharField(_('object repr'), max_length=200)
action_flag = models.PositiveSmallIntegerField(_('action flag'))
change_message = models.TextField(_('change message'), blank=True)
So, you can add an additional model (LogEntry) that will hold a ForeignKey to the user that changed (added / modified) the object and a GenericForeignKey (https://docs.djangoproject.com/en/1.7/ref/contrib/contenttypes/#generic-relations) to the object that was modified.
Then, you can modify your views to add LogEntry objects when objects are modified. When you want to display all changes by a User, just do something like:
user = User.objects.get(pk=1)
changes = LogEntry.objects.filter(user=user)
# Now you can use changes for your requirement!
I've written a nice blog post about that (auditing objects in django) which could be useful: http://spapas.github.io/2015/01/21/django-model-auditing/#adding-simple-auditing-functionality-ourselves

django problem with foreign key to user model

I have a simple userprofile class in django such that
class Profile(models.Model):
user = models.OneToOneField(User,unique=True)
gender = models.IntegerField(blank=True, default=0, choices=UserGender.USER_GENDER,db_column='usr_gender')
education = models.IntegerField(blank=True, default=0, choices=UserEducation.USER_EDU,db_column='usr_education')
mail_preference = models.IntegerField(blank=True, default=1, choices=UserMailPreference.USER_MAIL_PREF,db_column='usr_mail_preference')
birthyear = models.IntegerField(blank=True, default=0,db_column='usr_birthyear')
createdate = models.DateTimeField(default=datetime.datetime.now)
updatedate = models.DateTimeField(default=datetime.datetime.now)
deletedate = models.DateTimeField(blank=True,null=True)
updatedBy = models.ForeignKey(User,unique=False,null=True, related_name='%(class)s_user_update')
deleteBy = models.ForeignKey(User,unique=False,null=True, related_name='%(class)s_user_delete')
activation_key = models.CharField(max_length=40)
key_expires = models.DateTimeField()
You can see that deletedBy and updatedBy are foreign key fields to user class. If I don't write related_name='%(class)s_user_update' it gives me error (I don't know why).
Although this works without any error, it doesn't push the user id's of deletedBy and updatedBy fields although I assign proper user to them.
Could give me any idea and explain the related_name='%(class)s_user_update' part ?
Thanks
'%(class)s_user_update' implies that it is a string awaiting formatting. You would normally see it in the context:
'%(foo)s other' % {'foo': 'BARGH'}
Which would become:
'BARGH other'
You can read more about python string formatting in the python docs. String Formatting Operations
I can't see how the code you have would ever work: perhaps you want:
class Profile(models.Model):
# other attributes here
updated_by = models.ForeignKey('auth.User', null=True, related_name='profile_user_update')
deleted_by = models.ForeignKey('auth.User', null=True, related_name='profile_user_deleted')
# other attributes here
If it does work, it is because django is doing some fancy magic behind the scenes, and replacing '%(class)s' by the class name of the current class.
Notes on the above:
The consistent use of *snake_case* for attributes. If you must use camelCase, then be consistent for all variables. Especially don't mix *snake_case*, camelCase and runwordstogethersoyoucanttellwhereonestartsandtheotherends.
Where you have two attributes that reference the same Foreign Key, you must tell the ORM which one is which for the reverse relation. It will default to 'profile_set' in this case for both, which will give you the validation error.
Use 'auth.User' instead of importing User into the models.py file. It is one less import you'll need to worry about, especially if you don't use the User class anywhere in your models.py file.
You can read more about the related_name stuff here:
https://docs.djangoproject.com/en/1.3/topics/db/queries/#following-relationships-backward