0

Problem explanation

Hello I have been having some issues with saving a many to many field when trying to inherit from another class. I have overwritten the save function for my "CharacterBanner" class which extends my "Banner" class. I attempt to create and save a new Banner instance so that I can point the CharacterBanner to the newly created and saved instance. The main issue currently is that the id of the parent (Banner) is not being generated even though it is being saved so I cannot properly add a pointer to CharacterBanner. Other fields are being saved properly however.

Additionally, even if I could get an id, I am unsure as to how I can assign the CharacterBanner pointer to the Banner that I create. That is why I have a wonky setup with both banner_ptr and banner_ptr_id in the database. When I migrate it certainly seems to expect banner_ptr_id.

Code

I have a "Banner" model that I want to contain the following fields

CHARACTER = "Character"
WEAPON = "Weapon"
BANNER_TYPE = [(CHARACTER, "Character"), (WEAPON, "Weapon"),]

class Banner(models.Model):
    name = models.CharField(max_length = 64)
    enddate = models.DateField()
    banner_type = models.CharField(max_length = 64,choices=BANNER_TYPE)

I also have the "CharacterBanner" model that has the Banner model as a parent.

models.py

class CharacterBanner(Banner):
    rateups = models.ManyToManyField(Character,blank=True)
    banner_ptr = models.OneToOneField(
        Banner, on_delete=models.CASCADE,
        parent_link=True,
        primary_key=True,
    )
    def save(self, *args, **kwargs):
        self.banner_type = "Character"
        if not self.id:
            newBanner = Banner(name=self.name, enddate=self.enddate,banner_type = self.banner_type)
            print(newBanner.id)
            newBanner.save()
            super(Banner, newBanner).save(*args, **kwargs)
            print(newBanner.id)
            # self.banner_ptr = newBanner
            self.banner_ptr_id = newBanner.id
        super(CharacterBanner,self).save(*args, **kwargs)

In this case, both the prints are "None".

I have another class called "WeaponBanner" which has "Weapons" for its many to many model and I need to be able to query both WeaponBanners and CharacterBanners from a singular type of model Banner. So having Banner as an abstract class (with my current understanding) would not work and is a solution I would generally not like to pursue. I don't know if there is some way to to OneToOne fields that makes this work.

forms.py

def save(self, commit=True):
        cleaned_data = super().clean()
        if not commit:
            raise NotImplementedError("Can't create object without database save")
        rateups = cleaned_data.get('rateups')
        kwargs = {'name': cleaned_data.get('name'), 'enddate': cleaned_data.get('enddate')}
        character_banner = self.Meta.model(**kwargs)
        print("ptr_id", character_banner.banner_ptr_id)
        character_banner.save()    
        character_banner.rateups.set(rateups)
        print(character_banner)
        character_banner.save()    
        return character_banner

views.py

def post(self, request,*args, **kwargs):
        context ={}
        form  = self.form_class(request.POST)
        if form.is_valid():
            form.save()
            return redirect(to=self.success_url)

I don't think views matters much here but I will include it anyways

This is how I create the tables for the relevant classes:

CREATE TABLE IF NOT EXISTS analyze_banner (
    id INT PRIMARY KEY,
    name TEXT,
    enddate DATE,
    banner_type TEXT
);
CREATE TABLE IF NOT EXISTS analyze_characterbanner(
    banner_ptr INT,
    banner_ptr_id INT
);
CREATE TABLE IF NOT EXISTS analyze_characterbanner_rateups(
    id INT PRIMARY KEY,
    characterbanner INT,
    character_id INT
);

I currently have both banner_ptr and banner_ptr_id because when migrating I know that the system expects a banner_ptr_id column and when I am trying to fix things in models.py it seems to want a banner_ptr column.

Outputs

I am trying to create a Banner in the overwritten save function of the CharacterBanner so I can create a Banner and get the banner_ptr to point to that newBanner. However, when I print out newBanner.id it is None before and after I save. I am unsure how ids are generated but I thought that to generate a Banner id I would need to create and save a Banner then after saving the id would be generated.

Uncommented self.banner_ptr = newBanner:

ValueError: save() prohibited to prevent data loss due to unsaved related object 'banner_ptr'

I don't have a field in the model for banner_ptr, but I have gotten by without one before the most recent set of changes. I have not yet added a banner_ptr model field since I don't know what that should look like, however I do have a banner_ptr column in database.

Looking in database in this case for the generated Banner it is saved but has null fields for id,name, and enddate. While the banner_type is assigned (with correct value).

Commented self.banner_ptr = newBanner:

"<CharacterBanner: >" needs to have a value for field "banner_ptr" before this many-to-many relationship can be used.

The other error seems complementary to the one above since it complains about needing banner_ptr to be assigned.

Looking in database in this case for the generated Banner it is saved (with correct values for non-null fields) but has null field for only id. Additionally each time it is save 3 times. I assume that has something to do with this:

newBanner.save()
super(Banner, newBanner).save(*args, **kwargs)

Other Stackexchange posts that I found that are close to mine:

Django ManyToMany model validation - seemed not too helpful

ValueError [Object] needs to have a value for field "item_ptr" before this many-to-many relationship can be used - seemed very relevant but solution doesn't help. I assumed that the newItem creation was invalid so that the ptr pointed to none. That could definitely be the case with mine but the clean function does not seem to be the issue since every field is properly assigned except id.

https://forum.djangoproject.com/t/django-save-prohibited-to-prevent-data-loss-due-to-unsaved-related-object-predictions/17730/3 - this one just made it the same as the commented commented

https://forum.djangoproject.com/t/django-save-prohibited-to-prevent-data-loss-due-to-unsaved-related-object-predictions/17730/10 - seems unhelpful since we only need to save with commit false because

Of course I have tried and tinkered with the solutions given in these articles to the best of my abilities but I couldn't get any of them to work.

Solutions Synopsis

I tried using various different manually generated tables with different names. I've tried many different ways to save and feed information to the Banner class.

I expect the parent class that I manually generate to have an ID after I save it but it doesn't.

Another problem could be with the CharField on Banner. Though since it properly gets saved to database I don't see how that could be the problem.

This could be entirely a database issue but when I try to delete the database and migrate it still complains about tables not existing (even though this used to work for me). Which is why I manually created the tables as above.

Bernoulli
  • 1
  • 1

1 Answers1

0

Found the following solution here: https://stackoverflow.com/a/60360715/20995582

I decided to try and see if I could actually reset the database to get it to work even though it wasn't working. The solution by https://stackoverflow.com/users/2487835/lex-podgorny worked for me.

Got it working with following steps:

Step 0 is to make a fixture so that all your relevant data can be restored. Additionally this doesn't work if you are production and can't just delete db.

  1. Delete migrations folder and replace it with migrations folder with just empty init.py
  2. delete db.sqlite3
  3. THIS IS THE STEP I MISSED: in urls.py of the app(s) you are changing, comment out from . import views or anything that imports from the view of any app. Additionally comment out so that urlpatterns is an empty list.
  4. python manage.py makemigrations
  5. python manage.py migrate
  6. python manage.py migrate --run-syncdb
  7. load your fixture if you created one
Bernoulli
  • 1
  • 1