How to delete object in admin and keep parent? - django

I have following problem:
class Gift(models.Model):
name = models.CharField(max_length=255,default='')
class ProblematicGift(Gift):
# it does not help gift_ptr = models.OneToOneField(Gift, parent_link=True, default=None, null=True, blank=True, on_delete=models.DO_NOTHING)
notes = models.CharField(max_length=255,default='')
How I can delete the object of ProblematicGift in admin interface and keep the object of Gift ?
Simplified background: Automat select problematic gift and add it to table, where admin look at it, fix the gift and delete the ProblematicGift

You have three choices:
Quickest and hackiest is to just create a new Gift based on ProblematicGift and then delete ProblematicGift.
You can use abstract inheritance to make Gift a primitive type and then subclass it to create ProblematicGifts and something like GoodGifts. The procedure after that is pretty much the same: they each get separate tables, so you add a GoodGift and then delete the ProblematicGift. It's pretty much the same as #1, but a little more semantic.
Is probably your best choice: using proxy models. You add an boolean attribute to gift of the form of something like 'is_problematic'. Then, create ProblematicGift as a proxy for Gift that automatically sets is_problematic to True on creation, and override the manager to only return gifts with is_problematic set to True. Then, you simply set that attribute to False instead of deleting ProblematicGift and it leaves the queryset.
--
class Gift(models.Model):
name = models.CharField(max_length=255,default='')
notes = models.CharField(max_length=255,default='')
is_problematic = models.BooleanField(default=False)
class ProblematicGiftManager(models.Manager):
def get_query_set(self, *args, **kwargs):
qs = super(ProblematicGiftManager, self).get_query_set(*args, **kwargs)
return qs.filter(is_problematic=True)
class ProblematicGift(models.Model):
objects = ProblematicGiftManager()
class Meta:
proxy = True
def save(self, *args, **kwargs):
# Make sure it's new
if not self.pk:
self.is_problematic = True
super(ProblematicGift, self).save(*args, **kwargs)
def resolve(self):
self.is_problematic = False
self.save()
EDIT: Moved note from ProblematicGift to Gift. When using proxy models, you can't add any new fields to the subclass.

Honestly, the mistake you're making is trying to inherit from Gift. You don't want to do that for your use case.
The best way is to make Gift a stand-alone model:
class Gift(models.Model):
name = models.CharField(max_length=255,default='')
And then have ProblematicGift reference it:
class ProblematicGift(models.Model):
gift = models.OneToOneField(Gift, null=True, blank=True)
notes = models.CharField(max_length=255,default='')
# this method added based on a comment
def __unicode__(self):
return self.gift.name
Now you can delete the ProblematicGift safely.

Related

How to I automatically filter out is_deleted records in an associated table in Django?

I am using soft deletes on one of my models in Django, and I am overwriting the default manager to always return active records only, using something like:
class ActiveRecordManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
class Tag(models.Model):
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveRecordManager()
class Photo(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="photos")
objects = ActiveRecordManager()
All works well. However, when I do:
tag = Tag.objects.get(pk=100)
And then I try to get the associated photos:
photos = tag.photos.all()
Then I get photos that are deleted. I only want to return objects that are not deleted (so my regular objects list. I was reading about _base_mangers in Django, which seems to control this, but the documentation recommends against filtering objects out:
If you override the get_queryset() method and filter out any rows,
Django will return incorrect results. Don’t do that. A manager that
filters results in get_queryset() is not appropriate for use as a base
manager.
But what I am not clear about is how I am supposed to filter these results. Any thoughts?
UPDATE:
I was asked to explain how this question is different from this one:
How to use custom manager with related objects?
In this 8 year old question they mention a deprecated method. That deprecated method is superseded by the method I outline below (base_managers) which according to the documentation I should not use. If people think I should use it, can you please elaborate?
why not use custom query methods instead of overriding manager as it may produce problems for example in admin pages?
class ActiveModelQuerySet(models.QuerySet):
def not_active(self, *args, **kwargs):
return self.filter(is_deleted=True, *args, **kwargs)
def active(self, *args, **kwargs):
return self.filter(is_deleted=False, *args, **kwargs)
class Tag(models.Model):
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveModelQuerySet().as_manager()
class Photo(models.Model):
tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="photos")
is_deleted = models.BooleanField(default=False, db_index=True)
objects = ActiveModelQuerySet().as_manager()
you can then filter your models however you want
tag = Tag.objects.active(pk=100)
deleted_tags = Tag.objects.not_active()
photos = tag.photos.active()
also note that you need is_deleted attribute in all your models that have the soft delete functionality like Photo in your case

Django ModelForm field queryset based on another field

Consider my models.py,
PowerPolicy:
class PowerPolicy(models.Model):
name = models.CharField(max_length=15)
...
Group:
class Group(models.Model):
name = models.CharField(max_length=15)
#But then, we also have:
power_policies = models.ManytoManyField(PowerPolicy)
Player:
class Player(models.Model):
name = models.CharField(max_length=15)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
...
And then another model called,
UsePower:
class UserPower(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
power_policy = models.ForeignKey(PowerPolicy, on_delete=models.CASCADE)
...
But! Here's the catch: I want to make it so that my superuser (Note that my superuser isn't a player, he's simply a superuser) can only create a UsePower object of the Powers specified in the Player's Group. Now, I do know that I have to create a custom form and override the queryset of the power_policy field that returns, my the custom queryset according to my needs through a function.
- Here's what it would look something like:
class UsePowerForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UsePowerForm, self).__init__(*args, **kwargs)
def MyCustomFunctionThatReturnsTheQuerySet():
This function returns the Power policies that are allowed to the player in
their player Group. The only problem is,
Our little function here doesn't know how to get the player chosen.
could you help
return TheQuerySet
self.fields['power_policy'].queryset = MyCustomFunctionThatReturnsTheQuerySet()
And then use it on the Admin Site, by doing this:
class UsePowerAdmin(admin.ModelAdmin):
form = UsePowerForm
admin.site.register(UsePower, UsePowerForm)
I really hope this makes sense, and you guys could help me out.
Thank you for your time reading this, I honestly do appreciate it.
EDIT: Using form cleaning, or verifying during save, is not an option for me :(
You can get the player when the form is being initialized:
class UserPowerForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UsePowerForm, self).__init__(*args, **kwargs)
player = Player.objects.get(id=self.initial['player'])
###from here you can use player to get the power policies and put into list
self.fields['power_policy'] = forms.ChoiceField(choices=power_policy_list)
class Meta:
model = UserPower
fields = ['player', 'power_policy']

Django: Adding property to User model after creating model based on abstract class

I have a normal model and an abstract model like so:
class TaggedSubject(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
category = models.CharField(max_length=200)
foo = models.CharField(max_length=50)
bar = models.CharField(max_length=50)
# etc
content_type = models.ForeignKey(ContentType)
content_object_pk = models.CharField(max_length=255)
content_object = generic.GenericForeignKey("content_type", "content_object_pk")
def __unicode__(self):
if self.user:
return "%s" % (self.user.get_full_name() or self.user.username)
else:
return self.label
class Taggable(models.Model):
tagged_subjects = generic.GenericRelation(TaggedSubject, content_type_field='content_type', object_id_field='content_object_pk')
#property
def tagged_users(self):
return User.objects.filter(pk__in=self.tagged_subjects.filter(user__isnull=False).values("user"))
class Meta:
abstract = True
The Taggable abstract model class then gets used like so:
class Photo(Taggable):
image = models.ImageField(upload_to="foo")
# ... etc
So if we have a photo object:
photo = Photo.objects.all()[0]
I can all the users tagged in the photo with photo.tagged_users.all()
I want to add the inverse relation to the user object, so that if I have a user:
user = User.objects.filter(pk__in=TaggedSubject.objects.exclude(user__isnull=True).values("user"))[0]
I can call something like user.tagged_photo_set.all() and have it return all the photo objects.
I suspect that since TaggedSubject connects to the Taggable model on a generic relation that it won't be possible to use it as a through model with a ManyToMany field.
Assuming this is true, this is the function I believe I'd need to add (somehow) to the User model:
def tagged_photo_set(self):
Photo.objects.filter(pk__in=TaggedSubject.objects.filter(user=self, content_type=ContentType.objects.get_for_model(Photo))
I'm wondering if it's possible to set it up so that each time a new model class is created based on Taggable, it creates a version of the function above and adds it (ideally as a function that behaves like a property!) to User.
Alternatively, if it is somehow possible to do ManyToMany field connections on a generic relation (which I highly doubt), that would work too.
Finally, if there is a third even cooler option that I am not seeing, I'm certainly open to it.
You could use add_to_class and the class_prepared signal to do some post processing when models subclassing your base class are set up:
def add_to_user(sender, **kwargs):
def tagged_FOO_set(self):
return sender.objects.filter(pk__in=TaggedSubject.objects.filter(
user=self,
content_type=ContentType.objects.get_for_model(sender)))
if issubclass(sender, MyAbstractClass):
method_name = 'tagged_{model}_set'.format(model=sender.__name__.lower())
User.add_to_class(method_name, property(tagged_FOO_set))
class_prepared.connect(add_to_user)

Setting default value for Foreign Key attribute

What is the best way to set a default value for a foreign key field in a model? Suppose I have two models, Student and Exam with student having exam_taken as foreign key. How would I ideally set a default value for it? Here's a log of my effort
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=1)
Works, but have a hunch there's a better way.
def get_exam():
return Exam.objects.get(id=1)
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam", default=get_exam)
But this fails with tables does not exist error while syncing.
Any help would be appreciated.
I would modify #vault's answer above slightly (this may be a new feature). It is definitely desirable to refer to the field by a natural name. However instead of overriding the Manager I would simply use the to_field param of ForeignKey:
class Country(models.Model):
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, to_field='sigla', default='IT')
As already implied in #gareth's answer, hard-coding a default id value might not always be the best idea:
If the id value does not exist in the database, you're in trouble. Even if that specific id value does exist, the corresponding object may change. In any case, when using a hard-coded id value, you'd have to resort to things like data-migrations or manual editing of existing database content.
To prevent that, you could use get_or_create() in combination with a unique field (other than id).
Here's one way to do it:
from django.db import models
class Exam(models.Model):
title = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=255)
#classmethod
def get_default_pk(cls):
exam, created = cls.objects.get_or_create(
title='default exam',
defaults=dict(description='this is not an exam'),
)
return exam.pk
class Student(models.Model):
exam_taken = models.ForeignKey(
to=Exam, on_delete=models.CASCADE, default=Exam.get_default_pk
)
Here an Exam.title field is used to get a unique object, and an Exam.description field illustrates how we can use the defaults argument (for get_or_create) to fully specify the default Exam object.
Note that we return a pk, as suggested by the docs:
For fields like ForeignKey that map to model instances, defaults should be the value of the field they reference (pk unless to_field is set) instead of model instances.
Also note that default callables are evaluated in Model.__init__() (source). So, if your default value depends on another field of the same model, or on the request context, or on the state of the client-side form, you should probably look elsewhere.
I use natural keys to adopt a more natural approach:
<app>/models.py
from django.db import models
class CountryManager(models.Manager):
"""Enable fixtures using self.sigla instead of `id`"""
def get_by_natural_key(self, sigla):
return self.get(sigla=sigla)
class Country(models.Model):
objects = CountryManager()
sigla = models.CharField(max_length=5, unique=True)
def __unicode__(self):
return u'%s' % self.sigla
class City(models.Model):
nome = models.CharField(max_length=64, unique=True)
nation = models.ForeignKey(Country, default='IT')
In my case, I wanted to set the default to any existing instance of the related model. Because it's possible that the Exam with id 1 has been deleted, I've done the following:
class Student(models.Model):
exam_taken = models.ForeignKey("Exam", blank=True)
def save(self, *args, **kwargs):
try:
self.exam_taken
except:
self.exam_taken = Exam.objects.first()
super().save(*args, **kwargs)
If exam_taken doesn't exist, django.db.models.fields.related_descriptors.RelatedObjectDoesNotExist will be raised when a attempting to access it.
The issue with most of these approaches are that they use HARD CODED values or lambda methods inside the Model which are not supported anymore since Django Version 1.7.
In my opinion, the best approach here is to use a sentinel method which can also be used for the on_delete argument.
So, in your case, I would do
# Create or retrieve a placeholder
def get_sentinel_exam():
return Exam.objects.get_or_create(name="deleted",grade="N/A")[0]
# Create an additional method to return only the id - default expects an id and not a Model object
def get_sentinel_exam_id():
return get_sentinel_exam().id
class Exam(models.Model):
....
# Making some madeup values
name=models.CharField(max_length=200) # "English", "Chemistry",...
year=models.CharField(max_length=200) # "2012", "2022",...
class Student(models.Model):
....
.....
exam_taken = models.ForeignKey("Exam",
on_delete=models.SET(get_sentinel_exam),
default=get_sentinel_exam_id
)
Now, when you just added the exam_taken field uses a guaranteed existing value while also, when deleting the exam, the Student themself are not deleted and have a foreign key to a deleted value.
You could use this pattern:
class Other(models.Model):
DEFAULT_PK=1
name=models.CharField(max_length=1024)
class FooModel(models.Model):
other=models.ForeignKey(Other, default=Other.DEFAULT_PK)
Of course you need to be sure that there is a row in the table of Other. You should use a datamigration to be sure it exists.
I'm looking for the solution in Django Admin, then I found this:
class YourAdmin(admin.ModelAdmin)
def get_changeform_initial_data(self, request):
return {'owner': request.user}
this also allows me to use the current user.
see django docs
the best way I know is to use lambdas
class TblSearchCase(models.Model):
weights = models.ForeignKey('TblSearchWeights', models.DO_NOTHING, default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want'))
so you can specify the default row..
default=lambda: TblSearchWeights.objects.get(weight_name='value_you_want')

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