Django - after changing all reverse related objects the parent object still cannot be deleted

Viewed 50

I have the following model (for demonstration purposes):

class ProjectModule(models.Model):
   name = models.CharField(...)
   module = models.ForeignKey('ProjectModule', ....., on_delete=models.PROTECT)
   ... (many more fields and relations) ...

I wrote a function to merge instances to get rid of duplicates and such. For this I need to change all related objects of the first object (succumber) and point them to their new parent (a submodule of the survivor). After changing all foreign keys I delete the succumber.

new_parent = survivor.projectmodule_set.filter(name=entry.name).first()
succumber.projectmodule_set.update(module=new_parent)
succumber.delete()

But that's not working because Django throws the following error:

"Cannot delete some instances of model 'ProjectModule' because they are referenced through a protected foreign key: 'ProjectModule.module'", <QuerySet [<ProjectModule: 1.1 : LCP : 5583>, <ProjectModule: 1.2 : JB1 : 5583>, <ProjectModule: 1.3 : JB2 : 5583>]>

Now, I just changed these (reverse) related objects and gave them a new foreign key. Their name even reflects that (5583 is the id of the new parent). Reloading the succumber before deleting does not help either. It just can not "forget" these relation and therefore can not be deleted although the relations have already changed.

I also tried the "long approach":

for submodule in succumber.projectmodule_set.all():
    submodule.module = new_parent
    submodule.save()

But no luck. What am I doing wrong here?

Update

Sure, I can share a bit more code but I think this will only make things more confusing.

These maintenance views use a generic function I wrote that can merge model instances together. (I am omiting sanity checks and other unrelated stuff like standard processing of the function and unique field handling, etc)

def merge_model_instances(self, succumber, survivor, *, callback = None, debug = False, **kwargs):

After some error checks the function gets all relations and all their related objects:

foreign_key_reverse_relations = [field for field in succumber._meta.model._meta.get_fields(include_hidden=True) if
                                             field.auto_created and not field.concrete]
for reverse_relation in foreign_key_reverse_relations:
    reverse_entries_succumber = reverse_relation.related_model.objects.filter(
                    **{reverse_relation.field.name: succumber})

Of course not all relations can be solved automatically so the function allows for the attachment of a callback function in which we can process special cases:

# custom handling for this reverse relation if a callback function was attached
if reverse_entries_succumber and callback and callback is not None and callable(callback):
    reverse_entries_succumber = callback(reverse_relation, reverse_entries_succumber, survivor)

In this case there is a callback function with special treatments for "ProjectModule" objects. As these are self referential (and therefore form a tree) we only need to change the immediate children of root nodes (root nodes have .module=None). The rest can stay relative to each other.

def merge_callback(reverse_relation, reverse_entries, survivor, *, debug, **kwargs):
    (... other stuff ...)
    if reverse_relation.name == "projectmodule":
        for entry in reverse_entries:
            # we are looking for root modules because all other project modules do not need to be "moved"
            if entry.module is None:
                # we get the new parent (how is not really important here)
                new_module = survivor.projectmodule_set.filter(name=entry.name).first()
                # set new_module as new parent 
                entry.projectmodule_set.update(module=new_module)  # this should save these child modules
                # entry.refresh_from_db()       # this does not work either :/
                entry.delete()                  # objects are still attached to entry.projectmodule_set even though they should not be anymore and the error is thrown here

    return reverse_entries    
0 Answers
Related