I'd like to remove some object with many-to-many relationship using Django admin interface. Standard removing also removes all related objects and the list of removed objects displayed on confirmation page. But I don't need to remove related objects!
Assume we have ContentTopic and ContentItem:
class ContentTopic(models.Model):
name = models.CharField()
code = models.CharField()
class ContentItem(models.Model):
topic = models.ManyToManyField(ContentTopic, db_index=True,\
blank=True, related_name='content_item')
So, I'd like to remove ContentTopic instance using Django admin, but I don't need remove all related ContentItems. So, confirmation page should display only ContentTopic instance to remove.
What is the best way to handle this?
This happens so, coz its developed to do so.
If you want to change this behaviour, the one way can be over-riding delete method of django.db.models.Model.
This delete() method actually does two things, first gathering a list of all dependent objects and delete them. So here, you can override it, to get that list of dependent objects, iterating over it and set their reference to None, instead of deleting them. And thus deleting the concerned object cleanly.
May be if you want this behavior throughout, you can extend a class from django.db.models.Models, override delete(), and extend all your models from this new class.
Related
There is a requirement, that nothing should be deleted from database (no rows should be deleted)
So, obviously, all models should be inherited from something like this:
class BaseModel(models.Model):
is_deleted = models.BooleanField(default=False)
But, it is not obvious how to make models.Manager to handle is_deleted the best way
I can imagine two options:
1) Override BaseModel's Manager's ._get_query_set() method
So, both will return only active objects (marked as is_deleted=False):
Article.objects.all()
Article.objects.filter(id__in=[1, 2])
Even .get(...) will raise 404 if is_deleted=True:
Article.objects.get(id=1)
Also, extend with additional method, to be able to actually access is_deleted=True:
Article.objects.deleted(id=1)
2) Second option is to extend BaseModel with additional second Manager, let's say - actual
So, all three will exclude objects with is_deleted=True:
Article.actual.all()
Article.actual.filter(id__in=[1, 2])
Article.actual.get(id=1) # 404 even if in db, but is_deleted=True
At the same time, regular objects works and stands with native behaviour (ignore is_deleted or not):
Article.objects.all()
Article.objects.filter(id__in=[1, 2])
Article.objects.get(id=1)
Maybe there are another good options? Is there a best practice?
Big thx for advices!
1 or 2 options?
From the django docs:
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 (including dumpdata) 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.
Also any third party apps you use, will also likely use the default manager. Ask yourself if it is important for any of these apps to access any of your 'deleted' rows.
For the above reason I think I would probably opt for the two managers option.
Another consideration
When you say "nothing should be deleted from database" do you mean that no rows should be deleted, or no data should ever be removed. If the later, remember that when you update a row, that old data is lost forever, and in that sense the data is 'deleted'.
To avoid this you can have a system where you only ever add rows to your database. You would need a non-unique id field to identify which rows you use, and when you get a particular id, you chose the most recently updated row with that id. Just a thought.
Let's say I have a base class with, for example:
class Base(models.Model):
name = models.CharField(max_length=50, blank=False, null=False)
value1 = models.CharField(max_length=50)
value2 = models.CharField(max_length=50)
Now, I'm inputting several types of objects into the table, some of which use parts of the data, some of which use other parts, all of them using some common part (name in this example).
I want a complete listing, but I want to have different views when I click into an object, depending on it's type. Changes in the modelAdmin include: one of the classes uses inlines, others don't, list_display varies, one has extra CSS, etc, etc. Basically we're talking about different modelAdmins.
Alternatives I'm thinking: one is that each of those types subclasses Base, i.e.:
class Type1(Base):
pass
class Type2(Base):
pass
and then I define a modelAdmin for each of them, and one for the Base class just to get the table listing everything. In this one I would override the links so they don't go to /app/base/id, but instead to /app/type1/id, /app/type2/id, etc depending on the type. For each of these, I modify the modelAdmins so after saving they go back to /app/type
A different alternative would be having a single model and a single modelAdmin, and overriding every single method I'm using for change_view to consider what type of object it's rendering, i.e., get_inline_instances, get_formsets, whatever I need to modify list_display, list_display_links, list_filter, etc.
The first alternative looks way cleaner to me, although I'm not sure how to modify the link other than defining a method in the modelAdmin with the correct call to reverse and adding that method as a column in list_display.
Is there an easier way I'm missing?. How would you do it?.
Oh, and it HAS to use the admin. I'd rather do this using views, or separate models, but sadly this is the way it has to be. The High Command wants everything in one single table.
Thanks!.
Edit: also, I just found this and it looks good:
http://django-polymorphic.readthedocs.org/en/latest/admin.html
Django-Polymorphic definitely seems the way to go. It's easy to use and automatically gives me the correct modelAdmin when I click through a base object, something I couldn't replicate with Proxies.
Only problem is a table is created for each child class, even if the child class doesn't have any additional fields, and an extra query is performed per child class even though nothing is recovered from it (only column in the table is a foreign key to the base object).
But it works. I can live with that.
Probably easiest to explain with an example:
class Item(models.Model):
# ...
class ComplexItem(Item):
components = models.ManyToManyField(Item, through='ComponentItem', symmetrical=False, related_name='component_of')
class ComponentItem(models.Model):
# ...
item = models.ForeignKey(ComplexItem)
component = models.ForeignKey(Item, related_name='used_in_items
I would like a table of Items, with a name, price etc. Then I would like to define ComplexItems which are Items in their own right, but they require other Items in varying quantities.
The above causes the following exception in the admin app:
<class 'inventory.models.ComponentItem'> has more than 1 ForeignKey to <class 'inventory.models.ComplexItem'>
I need to override instance methods in ComplexItem and generally seperate the behavior from Item and the inheritance makes sense from a pure data view.
Is there some alternative definition of this relationship? I'd also like to avoid needing 'related_name' on both ComponentItem.component and ComplexItem.components.
You need to go back to the drawing board. While it's probably technically possible for a model to both inherit from and simultaneously be composed of another model, it's going to get sticky quick.
Try making ComplexItem just inherit from models.Model like Item does. Bet you that change alone will fix everything.
The model above actually works fine (I think, I haven't tested and decided against it for the moment). However the table generated for ComplexItem only has one column pointing to Item, which is fairly useless.
The functionality of ComponentItem can still be gotten by defining a ManyToMany relationship from Item to 'self' through ComponentItem.
Defining separate behavior is as easy as creating a Proxy model.
The actual error above came from my admin.Inline not being able to pick the correct foreign key to use for a ComponentItem, which can be solved like this.
I may come back to the inheritance above, but this works for now.
Django models generally handle the ON DELETE CASCADE behaviour quite adequately (in a way that works on databases that don't support it natively.)
However, I'm struggling to discover what is the best way to override this behaviour where it is not appropriate, in the following scenarios for example:
ON DELETE RESTRICT (i.e. prevent deleting an object if it has child records)
ON DELETE SET NULL (i.e. don't delete a child record, but set it's parent key to NULL instead to break the relationship)
Update other related data when a record is deleted (e.g. deleting an uploaded image file)
The following are the potential ways to achieve these that I am aware of:
Override the model's delete() method. While this sort of works, it is sidestepped when the records are deleted via a QuerySet. Also, every model's delete() must be overridden to make sure Django's code is never called and super() can't be called as it may use a QuerySet to delete child objects.
Use signals. This seems to be ideal as they are called when directly deleting the model or deleting via a QuerySet. However, there is no possibility to prevent a child object from being deleted so it is not usable to implement ON CASCADE RESTRICT or SET NULL.
Use a database engine that handles this properly (what does Django do in this case?)
Wait until Django supports it (and live with bugs until then...)
It seems like the first option is the only viable one, but it's ugly, throws the baby out with the bath water, and risks missing something when a new model/relation is added.
Am I missing something? Any recommendations?
Just a note for those who run into this issue as well, there is now an built-in solution in Django 1.3.
See the details in the documentation django.db.models.ForeignKey.on_delete Thanks for editor of Fragments of Code site to point it out.
The simplest possible scenario just add in your model FK field definition:
on_delete=models.SET_NULL
Django only emulates CASCADE behaviour.
According to discussion in Django Users Group the most adequate solutions are:
To repeat ON DELETE SET NULL scenario - manually do obj.rel_set.clear() (for every related model) before obj.delete().
To repeat ON DELETE RESTRICT scenario - manually check is obj.rel_set empty before obj.delete().
Ok, the following is the solution I've settled on, though it's far from satisfying.
I've added an abstract base class for all my models:
class MyModel(models.Model):
class Meta:
abstract = True
def pre_delete_handler(self):
pass
A signal handler catches any pre_delete events for subclasses of this model:
def pre_delete_handler(sender, instance, **kwargs):
if isinstance(instance, MyModel):
instance.pre_delete_handler()
models.signals.pre_delete.connect(pre_delete_handler)
In each of my models, I simulate any "ON DELETE RESTRICT" relations by throwing an exception from the pre_delete_handler method if a child record exists.
class RelatedRecordsExist(Exception): pass
class SomeModel(MyModel):
...
def pre_delete_handler(self):
if children.count():
raise RelatedRecordsExist("SomeModel has child records!")
This aborts the delete before any data is modified.
Unfortunately, it is not possible to update any data in the pre_delete signal (e.g. to emulate ON DELETE SET NULL) as the list of objects to delete has already been generated by Django before the signals are sent. Django does this to avoid getting stuck on circular references and to prevent signaling an object multiple times unnecessarily.
Ensuring a delete can be performed is now the responsibility of the calling code. To assist with this, each model has a prepare_delete() method that takes care of setting keys to NULL via self.related_set.clear() or similar:
class MyModel(models.Model):
...
def prepare_delete(self):
pass
To avoid having to change too much code in my views.py and models.py, the delete() method is overridden on MyModel to call prepare_delete():
class MyModel(models.Model):
...
def delete(self):
self.prepare_delete()
super(MyModel, self).delete()
This means that any deletes explicitly called via obj.delete() will work as expected, but if a delete has cascaded from a related object or is done via a queryset.delete() and the calling code hasn't ensured that all links are broken where necessary, then the pre_delete_handler will throw an exception.
And lastly, I've added a similar post_delete_handler method to the models that gets called on the post_delete signal and lets the model clear up any other data (for example deleting files for ImageFields.)
class MyModel(models.Model):
...
def post_delete_handler(self):
pass
def post_delete_handler(sender, instance, **kwargs):
if isinstance(instance, MyModel):
instance.post_delete_handler()
models.signals.post_delete.connect(post_delete_handler)
I hope that helps someone and that the code can be re-threaded back into something more useable without too much trouble.
Any suggestions on how to improve this are more than welcome.
I want to make the following modification to the Django framework.
I want it to create a "deleted" field for each model I create.
I want it to be checked as deleted when I delete it from the admin page instead of being physically deleted.
I do not want these records checked as deleted to be listed.
I'm new to Django, I'm seeing if I can do what I want to do with it easily. I need this change because it's the way we currently work.
So far these are the changes I have made, I would like to understand how the whole Django framewok works inside but I'm so far from that, is there any documentation online which explains clearly how the inside framework parts/files/modules/classes work together, the specific role of each one, etc
In the base.py file, in the modelbase class, below this code,
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
I added,
from django.db import models
new_class.add_to_class('deleted', models.BooleanField())
When it creates a model it adds the "deleted" field to it.
In the base.py file, in the save method, I changed what it was there for
self.deleted = True
self.save()
So, now it check as deleted a record instead of physically delete it.
Now what I want is those records not to be listed.
I don't understand why you're modifying the framework code instead of putting your deleted field in a model base class that all of your models extend from.
Nevertheless, a nice way to filter those records out would be to add a custom manager to the model (or your base model class, if you choose to create one). To this manager, override the get_query_set method as described here. In your overridden method, add a exclude(deleted=True) filter.
Take a look at the Django-logicaldelete app, You just inherit your models from their provided Model class and you get Logical delete for all of them.
It comes with an adminModel as well so you can manage logically deleted models there too.
Override the delete() method in your model class, set the deleted attribute there
Create a custom manager which will filter by deleted attribute and set it as the default one (objects = MyDeletedManager)