Search code examples
pythondjangodjango-formsdjango-admin

Django Admin Interface: Understanding Form Saving Behavior with commit Parameter


I'm currently working with Django's admin interface and encountering some unexpected behavior related to form saving with the commit parameter. Here's what I'm trying to understand:

Context: I'm using a custom form (DeckCreateForm) with a save method overridden to handle saving deck data. Within this method, I've set the commit parameter to False to prevent immediate database commits.

Observation: Despite setting commit to False, when saving a form instance through the admin interface, it appears that the form data is being committed to the database.

Understanding Needed: I'm seeking clarification on why the form data is being committed to the database even when commit is explicitly set to False. Additionally, I want to understand the factors that could influence the behavior of the commit parameter within the context of Django's admin interface.

Efforts Made: I've reviewed the Django documentation regarding form saving and the admin interface but haven't found a clear explanation for this behavior. I've also examined my code and haven't identified any obvious bugs or misconfigurations.

Request: I'm looking for insights or explanations from the Django community on how Django's admin interface handles form saving and the factors that might affect the behavior of the commit parameter. Additionally, any suggestions for troubleshooting or further exploration would be greatly appreciated.

My Code:

Custom save function (forms.py):

def save(self, commit=False):
    deck = super().save(commit=False)

    if commit:
        deck.save()
        self.save_m2m()

        cleaned_data = self.cleaned_data
        word_items = cleaned_data.get('word_items')

        with transaction.atomic():
            try:
                for rank, word_item in enumerate(word_items, start=1):
                    deck_entry, created = DeckWord.objects.get_or_create(
                        deck=deck,
                        word_item=word_item,
                        defaults={'rank':1}
                    )
            except IntegrityError as e:
                raise
    return deck

Admin.py

@admin.register(Deck)
class DeckAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'description', 'language', 'is_ranked', 'created_by', 'visibility']
    form = DeckCreateForm

Any guidance or insights into this matter would be invaluable in helping me better understand Django's admin interface behavior. Thank you in advance for your assistance!


Solution

  • Understanding

    Yes well, that can be a bit confusing. You'll find your answers if you delve into the logic of the view that handles the form/object saving for the admin panel. Your DeckAdmin inherits from ModelAdmin. So the code for the change-form-logic is found here:

    django>contrib>admin>options.py ModelAdmin._changeform_view

    # an extraction from mentioned function
    if form_validated:
        new_object = self.save_form(request, form, change=not add)
    else:
        new_object = form.instance
    if all_valid(formsets) and form_validated:
        self.save_model(request, new_object, form, not add)
    

    As you see there are ModelAdmin.save_form and ModelAdmin.save_model called.

        def save_form(self, request, form, change):
            """
            Given a ModelForm return an unsaved instance. ``change`` is True if
            the object is being changed, and False if it's being added.
            """
            return form.save(commit=False)
    
        def save_model(self, request, obj, form, change):
            """
            Given a model instance save it to the database.
            """
            obj.save()
    

    As you can see, there is no database action when the .save_form() method is called. It calls your custom form.save(commit=False) method and your code is respected. But when afterwards the ._changeform_view method calls the .save_model() method, the obj.save() method is issued. And the obj.save() method does not respect the code that you write in your custom form but respects the save logic that you potentially write in your models.py.

    TLDR: Your form.save logic runs, but afterwards the obj.save() method is what triggers the database interaction regardless from your form.save() logic.

    Fixing

    You have two options:

    A) As a logical consequence you can configure the .save() method of your model from within the models.py file. I don't know why you would do that, but for learning purposes you can overwrite that method to not commit to the database.

    B) You can add a save_model() method to your DeckAdmin.

        def save_model(self, request, obj, form, change):
            # Call the form's save method with commit=False
            deck = form.save(commit=False)
    
            # Perform any additional logic here
            cleaned_data = form.cleaned_data
            word_items = cleaned_data.get('word_items')
    
            with transaction.atomic():
                try:
                    for rank, word_item in enumerate(word_items, start=1):
                        deck_entry, created = DeckWord.objects.get_or_create(
                            deck=deck,
                            word_item=word_item,
                            defaults={'rank': 1}
                        )
                except IntegrityError as e:
                    raise
    
            # Save the deck instance
            deck.save()
            form.save_m2m()