Django: save() prohibited to prevent data loss due to unsaved related object 'predictions'

I am a creating a Django project, where i am using inline_formset to add multiple forms from the frontend (Just like the backend). I am trying create a new object now from the frontend, the main models get saved but the the model that is a foreignKey to the main model is not saving (Inline Model).
I am getting the error message: save() prohibited to prevent data loss due to unsaved related object 'predictions'. Here is my CreateView Code.

views.py

class ProductInline():
    form_class = PredictionForm
    model = Predictions
    template_name = "core/create/create_bet.html"

    def form_valid(self, form):
        named_formsets = self.get_named_formsets()
        if not all((x.is_valid() for x in named_formsets.values())):
            return self.render_to_response(self.get_context_data(form=form))

        # self.object = form.save()
        new_form = form.save(commit=False)
        new_form.user = self.request.user
        new_form.save()

        # for every formset, attempt to find a specific formset save function
        # otherwise, just save.
        for name, formset in named_formsets.items():
            formset_save_func = getattr(self, 'formset_{0}_valid'.format(name), None)
            if formset_save_func is not None:
                formset_save_func(formset)
            else:
                formset.save()
        return redirect('core:dashboard')

    def formset_variants_valid(self, formset):
        """
        Hook for custom formset saving.. useful if you have multiple formsets
        """
        variants = formset.save(commit=False)  # self.save_formset(formset, contact)
        # add this, if you have can_delete=True parameter set in inlineformset_factory func
        for obj in formset.deleted_objects:
            obj.delete()
        for variant in variants:
            variant.prediction = self.object
            variant.save()

    def formset_images_valid(self, formset):
        """
        Hook for custom formset saving.. useful if you have multiple formsets
        """
        images = formset.save(commit=False)  # self.save_formset(formset, contact)
        # add this, if you have can_delete=True parameter set in inlineformset_factory func
        for obj in formset.deleted_objects:
            obj.delete()
        for image in images:
            image.product = self.object
            image.save()


class PredictionCreate(ProductInline, CreateView):

    def get_context_data(self, **kwargs):
        ctx = super(PredictionCreate, self).get_context_data(**kwargs)
        ctx['named_formsets'] = self.get_named_formsets()
        return ctx

    def get_named_formsets(self):
        if self.request.method == "GET":
            return {
                'variants': PredictionDataFormSet(prefix='variants'),
            }
        else:
            return {
                'variants': PredictionDataFormSet(self.request.POST or None, self.request.FILES or None, prefix='variants')
            }
    

What is your definition for PredictionDataFormSet?

It’s defined in my forms.py

class PredictionForm(forms.ModelForm):

    class Meta:
        model = Predictions
        fields = [
            'image',
            'title',
            'description',
            'status',
            'starting_date',
            'ending_date',
        ]
        
class PredictionDataForm(forms.ModelForm):

    class Meta:
        model = PredictionData
        fields = [
            'name',
            'description',
            'amount',
            'odds',
            'won',
            'image',
        ]


PredictionDataFormSet = inlineformset_factory(
    Predictions, PredictionData, form=PredictionDataForm,
    extra=1, can_delete=True,
    can_delete_extra=True
)

If you look at the docs and example at Using an inlineformset in a view, you’ll see that when you’re binding data to a formset on a post, you should specify the instance of the parent class so that the foreign keys of the child class is set to refer to the proper object.

Since you appear to be submitting both the parent and child classes in the same post, that means you need to save the parent object before you bind the post data to the formset to be able to supply that instance to the formset.

1 Like

That is what i tried doing here, or maybe that’s not it?

As I wrote in my reply:

(emphasis added)

Where are you binding the post data to the formset?

Where is that code in relation to this code?

This is where i did it, seems the problem is from there but i cannot really tell what i did wrong

Review the example from the doc I referenced. Use that as a guide for how you code the logic for your view.

I found out a couple things, thanks to your refrence.

i was supposed to pass in variant.predictions instead or variant.product which was what i was using.

Now i noticed something else. when i create a new object using the frontend form, it saved the initial model but does not save the inline model, and that is because of this block of code.

but when i uncomment the self.object = form.save() everything works well apart from the fact that the model won’t have any user instance that created the post.

I want to save the user that made the current post and at the same time save the inline objects, is there something wrong with this code.

I even tried doing it this way

        new_form = form.save(commit=False)
        new_form.user = self.request.user
        self.object = new_form.save()

It working now, this is what i did

self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save()

Thanks alot for your time and for the docs you referred me too.

You’re getting closer.

But you’re still doing a lot more work manually than necessary.

You’re binding data to your forms here:

    def get_named_formsets(self):
        if self.request.method == "GET":
            return {
                'variants': PredictionDataFormSet(prefix='variants'),
            }
        else:
            return {
                'variants': PredictionDataFormSet(self.request.POST or None, self.request.FILES or None, prefix='variants')
            }

but you’re not passing your instance to this function, which is what’s requiring you to manually set the reference for each instance.

You should be saving the base form first, then calling this function to bind the data, then saving the formset.

I have tried doing this your way, instead of my own way, i don’t really get how i am going to save base form first before calling this function.

Please can you guide me to do that?

First, I would get rid of having PredictionCreate inherit from ProductInline. It makes the flow of events here very difficult to follow.

Then I would get rid of worrying about having a “name” for the formset. It’s a formset on a page, it really doesn’t matter what it’s called internally.

Finally, I would implement the inline formset as a field on the base form.

Doing all this - which includes refactoring your code to simplify it - will make it more clear how this should work.

Again, I refer you to the example at Using an inlineformset in a view for the overall organization of the sequence of events that needs to occur.

1 Like

Thanks for your response, I’ll follow these steps and try implementing it.

One more question: Can I achieve this same functionality of adding online forms using function based view and would it be a good practice?

There is no functional difference between an FBV and a CBV.

A CBV is merely a “code-organization” tool that, in some situations, greatly reduces the amount of code needing to be written for a particular type of view.

The Django-provided CBVs are not the only CBVs that can be created - they’re a starting point, not an ending point, and generally useful in a well-defined set of circumstances. (For an example of an alternative implementation of the CBV concept, see http://django-vanilla-views.org/)

Likewise, there are many situations where the Django generic CBVs are a poor choice. (I’ve written about that in other posts here in the forum.) In those situations, you would want to either create your own CBVs to enhance what Django has provide, or use FBVs.

There are others here who more strongly express the idea that FBVs are the only way to go. (See Django Views — The Right Way as an example)

In my opinion there is no absolute “right” or “wrong” here - it’s an architectural decision that should be made in the context of the overall system being developed.

1 Like

Alright I understand now, and it became more clear to me also, thanks.

Awsome @KenWhitesell :slight_smile:
Thank you.

I have another question concerning this topic.

I was trying to fix this on my own for about 30 minutes.
I only was reading the documentation about Formsets.

Here is the code I’m using:

FormSetInstalments = forms.inlineformset_factory(Credit, InstalmentCRON, exclude=[])

The classical Formsets version did not work at all. I could not find a method to link the linked object (foreign key, here Credit) to the Formset Object.
So, if I use this way:

FormSetInstalments = forms.formset_factory(FormInstalmentsCron, extra=2)

I could not find a solution to link the models when the post arrives

Is it general like: If there is another model stated as foreign key object, you need to use InlineFormSet?

The answer is located on the Creating forms from models | Django documentation | Django page.

You create the inlineformset_factory as you’ve identified, but the instance of the parent class is identified when the formset instance itself is created, not when you create the factory class.

But no, it’s not required that you use an InlineFormSet - it’s just that it does the work that you would otherwise need to be doing.

Thanks, that specific document was the solution.
I solved my Problem with the section “Using an inline formset in a view”

But If i use a ModelForm FormSet with a referenced object.
How would I set the object when I receive the form data?

The referenced object field in my case is required, but at the state of receiving this object through the form it is not stored in the database either.

Usually I would just do something like this:

if request.method == "POST":
    data = request.POST
    form = MainModelForm(data)
    if form.is_valid():
        obj = form.save()

             formset = ReferencingModelFormFormSet(data, initial=[dict(ref=obj)])
             if formset.is_valid(): 
                 formset.save()

But I cannot get it to work. It does not take the initializing data (ref=obj) that should tell the object in the FormSet that the required reference is stored in database and is the obj

Would be great if you have a hint :slight_smile:
But maybe I just blind …

Where did you come up with that formulation?

The initial parameter should be the instance that the formset relates to - a single object (in this case, obj).