After looking for a way to check if a model instance can be deleted in django, I found many options, but none was working as expected. Hope this solution can help.
Let start by creating an Abstract model class which can be inherited by other model
class ModelIsDeletable(models.Model):
name = models.CharField(max_length=200, blank=True, null=True, unique=True)
description = models.CharField(max_length=200, blank=True, null=True)
date_modified = models.DateTimeField(auto_now_add=True)
def is_deletable(self):
# get all the related object
for rel in self._meta.get_fields():
try:
# check if there is a relationship with at least one related object
related = rel.related_model.objects.filter(**{rel.field.name: self})
if related.exists():
# if there is return a Tuple of flag = False the related_model object
return False, related
except AttributeError: # an attribute error for field occurs when checking for AutoField
pass # just pass as we dont need to check for AutoField
return True, None
class Meta:
abstract = True
Example
So let say we have three model Organization and Department and StaffType
So many Department can be in an Organization
And an Organization has a particular StaffType
class StaffType(ModelIsDeletable):
pensionable = models.BooleanField(default=False)
class Organization(ModelIsDeletable):
staff_type = models.ForeignKey(to=StaffType)
class Department(ModelIsDeletable):
organization = models.ForeignKey(to=Organization, to_field="id")
so let say after adding some information you want to remove an organization model instance
that is already tied to a Department
For instance we have
Organization Table => (name = Engineering, pk = 1)
Department Table => (name=Developer, organization_fk=1, pk=1)
Now when you try to delete an organization after get it with the pk
a_org = Organization.objects.get(pk=1)
With this at hand you can check if it deletable
deletable, related_obj = a_org.is_deletable()
if not deletable:
# do some stuff with the related_obj list
else:
# call the delete function
a_org.delete()
Your question appears to be "How to detect what related model objects would be deleted if I delete this model object?" or "How to detect what related rows would be deleted if I delete this row?"
Another option is to use a transaction, do the delete, save the information provided by django, but rollback before committing the change. This works in databases like Postgres and MySQL, but I don't know about others.
In this example I want to know what will be deleted if I delete my organization named 'pdemo', and I see that it has 408 related Property objects.
https://gist.github.com/cgthayer/25aa97bb4b74efb75e3467fb7bbdaacb
>>> from django.db import transaction
>>> transaction.set_autocommit(autocommit=False)
>>> o = Organization_v2.objects.get(name='pdemo')
>>> del_info = o.delete()
>>> del_info
(1404, {'data.Property': 408, [..more stuff..], 'data.Organization_v2': 1})
>>> Property.objects.filter(scope__organization_id=o).count()
0
>>> transaction.rollback()
>>> o = Organization_v2.objects.get(name='pdemo')
>>> Property.objects.filter(scope__organization_id=o).count()
408
This could be translated into a generic function.
When looking into this, I found many old solutions that use the functions in the django.contrib.admin to determine this, but that's an undocumented api that seems to change from time to time, so using transactions seems to be easier iff your database supports it.
Related
I’m developing a web application where users can subscribe to different series of texts, that would be seen inside the system. I want to create a relationship between the User and the SeriesText, so I can grab the current logged user in the VIEW, and just return all of his subscriptions at the dashboard template.
An user would be able to subscribe to as many series as he wants.
I’m using the default User model from django.contrib.auth.models, and I’m not sure how to create this relationship.
I read a lot and I think the correct usage here would be Many-to-many (is that correct?), so I tried this, using a pivot table/model called Subscriptions:
from django.contrib.auth.models import User as BaseUserClass
class User(BaseUserClass):
subscriptions = models.ManyToManyField(SeriesText, through="Subscription")
class SeriesText(models.Model):
series_name = models.CharField(max_length=255)
series_text = models.CharField(max_length=255)
subbed_users = models.ManyToManyField(User, through="Subscription")
class Subscription(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
series = models.ForeignKey(SeriesText, on_delete=models.CASCADE)
def subscribe(self, user_id, series_id):
self.user = user_id
self.series = series_id
self.save()
But that didn’t seem to work, I got errors even trying to use a User.objects.get(pk=1), don’t really know why.
I’m really confused if I need to put the relationship both ways, like created models.ManyToMany on SeriesText model, and on an extended User model (that I don’t even know if that’s really the way to do it). I'm not even sure if that the correct way to make a relationship using the default auth user model.
To be able to later also search for all users subscribed to a series, I think that the models.ManyToMany should also be on the SeriesText model, is that also correct?
Can someone help me understand if I’m using the correct relationship (many-to-many), and how to make this relationship? Thanks in advance, I’m pretty new to Django I’m hitting a wall here.
You only need to add a m2m field on SeriesText. Fundamentally, it doesn't matter which model a many-to-many field is attached to (the database will look the same and the data access will be similar if not identical). Based on my experience, it's better if you don't have to mess with the Django User model:
class SeriesText(models.Model):
series_name = models.CharField(max_length=255)
series_text = models.CharField(max_length=255)
subscribers = models.ManyToManyField(User, related_name='subscriptions')
def __str__(self):
return self.series_name
To see how that works, first add some data:
s1 = SeriesText.objects.create(series_name='name1', series_text='text1')
s2 = SeriesText.objects.create(series_name='name2', series_text='text2')
u1 = User.objects.create_user('user1')
u2 = User.objects.create_user(username='user2')
u3 = User.objects.create_user(username='user3')
u4 = User.objects.create_user(username='user4')
u5 = User.objects.create_user(username='user5')
s1.subscribers.add(u1, u2, u3)
s2.subscribers.add(u3, u4, u5)
Then to fetch all subscribers for a SeriesText (I'm fetching the objects from the database here to show that it is not an artefact of the last two lines above - data has been changed in the database):
>>> SeriesText.objects.get(series_name='name1').subscribers.all()
[<User: user1>, <User: user2>, <User: user3>]
>>> SeriesText.objects.get(series_name='name2').subscribers.all()
[<User: user3>, <User: user4>, <User: user5>]
to fetch all subscriptions for a user (we declared that the relation from User to SeriesText should be named subscriptions in the many-to-many field):
>>> User.objects.get(username='user1').subscriptions.all()
[<SeriesText: name1>]
>>> User.objects.get(username='user2').subscriptions.all()
[<SeriesText: name1>]
>>> User.objects.get(username='user3').subscriptions.all()
[<SeriesText: name1>, <SeriesText: name2>]
>>> User.objects.get(username='user4').subscriptions.all()
[<SeriesText: name2>]
>>> User.objects.get(username='user5').subscriptions.all()
[<SeriesText: name2>]
If we hadn't declared the related_name we would have had to use the default:
User.objects.get(username='user1').seriestext_set.all()
which doesn't read as well.
My DB models looks similar to this
class Leave(models.Model):
available_leaves = models.IntegerField(default = 8)
class Policy(models.Model):
name = models.CharField(max_length=20)
class Student(models.Model):
name = models.CharField(max_length=20)
rating = models.IntegerField(default=7)
class StudentLeave(models.Model):
leave = models.ForeignKey(Leave, on_delete=models.CASCADE)
policy = models.ForeignKey(Policy, on_delete=models.CASCADE)
student = models.ForeignKey(Student, on_delete=models.CASCADE)
I want to deduct the leave of the student from here,
I have tried this
leave = Leave.objects.filter(pk = 1).first()
policy = Policy.objects.filter(pk = 1).first()
student = Student.objects.filter(name = 'matthew').first()
studentleave = StudentLeave.objects.filter(student = student, policy = policy, leave = leave).first()
Now I have studentleave object through which I can access every table connected to this table.
So I have tried accessing this
studentleave.leave.available_leaves-=1 # Tried reducing it by one
print(studentleave.leave.available_leaves) #prints as 7 as expected because 8 is default
But when I access the same model again, it's value is still 8 (old value)
I have tried update and save method on studentleave.leave.update() and studentleave.update() which are basically throwing error because that object has no attribute update
How can I save those new values for them.
Thanks in advance for spending your time to solve this problem.
To save changed data into database, one should use .save() method of the model instance. So what you need is to call studentleave.leave.save(), which will perform an UPDATE sql query into database.
You can use get method to get data from db via primary key. Get method return only one value from db if it exists.
I use django and postgress and have a very odd exception.
I have a model object called ProductModel (with upc as a unique_id).
This is how the model looks like:
class ProductModel(models.Model):
def __str__(self):
return self.clean_name + " " + str(self.product_upc)
product_name = models.CharField(max_length=300, blank=True)
product_upc = models.CharField(max_length=300, primary_key=True)
official_price = models.DecimalField(default=0, decimal_places=5, max_digits=10)
mrsp = models.DecimalField(default=0, decimal_places=5, max_digits=10)
last_seen = models.DateTimeField(default=now)
clean_name = models.CharField(max_length=200, default='')
unfortunately (as I understand now) at the begining of times I made a mistake and created a class called product that inherits the ProductModel - this class has some regular methods nothing fancy.
class Product(ProductModel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.create_clean_name()
self.img_list = []
self.scraped_content = ""
self.specs = {}
Now, I was sure that this class has nothing to do with the db and the db doesnt aware of its existence - but now When I try to delete some product records using this command:
p = ProductModel.objects.all().filter(last_seen__month__lt=4,product_name__contains='a')
p.delete()
I get the following error -
django.db.utils.IntegrityError: update or delete on table "my_app_productmodel" violates foreign key constraint "f64b7bfb6c6019a35bf6b81e4125240f" on table "my_app_product"
DETAIL: Key (product_upc)=(852896336240) is still referenced from table "my_app_product"
And in this point I got totally lost and confused what the h*** is table "my_app_product" - why it exists??
I went to my PostgreSQL and found out that this table is really exists.
I'd like to understand how does it work and how I delete records in this situation.
ohh and one more detail - as I try to delete my records via django admin I manage to do it without any problems.
Thanks to the kind helper.
Here is what you actually did: https://docs.djangoproject.com/en/1.11/topics/db/models/#multi-table-inheritance
Subclassing a model (that is not defined as abstract) will create a new table called Product that has a OneToOneField to ProductModel pk which is product_upc. You are trying to delete a ProductModel which has a reference to Product table, this is why you get the error.
Depending on how you use that Product class I suggest to just change its superclass to object and run makemigrations to remove the table, since you don't actually need it.
I want to have an abstract Company model in Django, and extend it depending on the type of the company involved:
class Company(models.Model):
name = models.CharField(max_length=100)
address = models.CharField(max_length=100)
class Meta:
abstract = True
class Buyer(Company):
# Buyer fields..
pass
class Seller(Company):
# Seller fields...
pass
Every user on the system is associated with a company, so I want to add the following to the User profile:
company = models.ForeignKey('Company')
But this gives the dreaded error:
main.Profile.company: (fields.E300) Field defines a relation with
model 'Company', which is either not installed, or is abstract.
So I imagine what I'm trying to do cannot be done. I saw that the contenttypes framework could be used for this purpose, as answered in this question. My issue with that is that I don't want the company field to point to any model, but just subclasses of the Company model.
Is there anything else I can use for this purpose?
The reason the ForeignKey cannot reference an abstract model directly is that individual models that inherit from the abstract model actually have their own tables in the database.
Foreign keys are simply integers referencing the id from the related table, so ambiguity would be created if a foreign key was related to an abstract model. For example there might be be a Buyer and Seller instance each with an id of 1, and the manager would not know which one to load.
Using a generic relation solves this problem by also remembering which model you are talking about in the relationship.
It does not require any additional models, it simply uses one extra column.
Example -
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
class Foo(models.Model):
company_type = models.ForeignKey(ContentType)
company_id = models.PositiveIntegerField()
company = GenericForeignKey('company_type', 'company_id')
And then -
>>> seller = Seller.objects.create()
>>> buyer = Buyer.objects.create()
>>>
>>> foo1 = Foo.objects.create(company = seller)
>>> foo2 = Foo.objects.create(company = buyer)
>>>
>>> foo1.company
<Seller: Seller object>
>>> foo2.company
<Buyer: Buyer object>
I have the following database tables:
class Story(models.Model):
user = models.ForeignKey(User)
group = models.ForeignKey(Group, blank=True, null=True)
date_added = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
location = models.CharField(max_length=100)
title = models.CharField(max_length=150)
description = models.CharField(blank=True, null=True, max_length=2000)
exp_text = models.TextField()
category = models.ForeignKey(Category, blank=True, null=True)
def __unicode__(self):
return self.title
class Comment(models.Model):
user = models.ForeignKey(User)
date_added = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
emailOnReply = models.NullBooleanField(blank=True, null=True)
comment_text = models.TextField()
story = models.ForeignKey(Story)
def __unicode__(self):
return self.comment_text
When i have created an object of Comment and need to access the title of a story i would expect to be able to do this:
c = Comment.objects.all()
for com in c:
com.comment_set.title
but django says that comment_set isnt defined. In the documentation it says that if you are tring to reach a field in the other database table where the foreign is not defined you can use the _set method where the word before it is the name of the class.
After trying different ways i found that this works:
c = Comment.objects.all()
for com in c:
com.story.title
Since the foreign key is defined in Comment i dont see how that can work, but it does, and why doesnt the _set work. Since im working on an object in the model where the foreign key is defined and i need to go to the story then i would need to use _set according to the docs...when working with a story objects im able to refer straigt to the comment model when defining the related_name property so i dont need _set there either.. why doesnt _set work here?
Update:
I now got the relationship working backwards when using a story object and refering to the comment class by:
s = Story.objects.all()
for st in s:
print st.comment_set.all()
I was using st.story_set instead of st.comment_set but i still find it weird that this works:
c = Comment.objects.all()
for com in c:
print com.story.title
When trying to work from the story object i dont have any foreign key to the comment table (only a related name on the foreign key in the comment table) so i dont get the same access it seems.
Actually everything is working as expected.
Comment object doesn't have a comment foreign key, it has a "story" foreign key.
comments are "pointing" to stories. as such - a comment only has one story, but a story may have a "set" of comments.
This is why st.comment_set works - because it "looks backward" to the comments pointing to it, while a comment just points directly to the story it is related to (i.e. com.story).
if you are interested in understanding why that works - see here:
https://docs.djangoproject.com/en/dev/topics/db/queries/#how-are-the-backward-relationships-possible
that's part of what makes Django's ORM so cool...
Following relationships in Django is a bit implicit. Here are simplified versions of your models:
class Story(models.Model): # instances have access to Manager comment_set (backward)
title = models.CharField(max_length=150)
description = models.CharField(blank=True, null=True, max_length=2000)
class Comment(models.Model): # has access to Story (forward)
comment_text = models.TextField()
story = models.ForeignKey(Story) # stored in database as story_id, which refers to (implicit) id column in Story
Your database will look like this (unless you specify otherwise):
Story (table)
id # automatically inserted by Django, unless you specify otherwise
title
description
Comment (table)
id # automatically inserted by Django, unless you specify otherwise
comment_text
story_id # points back to id in Story table
FORWARD
Instances of Comment have access to Story by matching Comment's story_id column to Story's id column (the latter being implicit to Django tables and not in your model but definitely in your database table, unless you have specified otherwise).
>>> c = Comment.objects.get(comment_text='hello, world')
>>> c.story.title # c => story_id => row in Story where id == c.story_id
'first post'
BACKWARD
Comment has a ForeignKey pointing back to Story, so instances of Story have access to a Manager named comment_set that can follow relationships from Story to Comment.
>>> s = Story.objects.get(title='first post')
>>> s.comment_set.comment_text # accesses Comment model from Story instance
'hello, world'
Or if you want to iterate over all comment_sets as you mention in your comment, try this instead:
>>> s = Story.objects.get(title='first post') # returns a single, non-iterable query object
>>> for story in s.comment_set.all(): # each comment_set object can have more than one item
print story.comment_text # story does not have a comment_set attribute
'hello, world' # my comment set here just happens to have one item
UPDATE
Or, based upon your comment, you want to take the iteration to one level higher, try the following:
>>> s = Story.objects.all()
>>> for story in s: # iterate over top level
>>> for row in story.comment_set.all():
>>> print row.comment_text # again, row does not have a comment_set attribute
'hello, world' # again, my comment set has only one item