How to Set Initial Data For Inline Model Formset in Django

How to Set Initial Data For Inline Model Formset in Django

In Django, sometimes we may want to set some initial value for inline formset when creating new model.

For example, we have a Poll model with multiple choices:

class Poll(models.Model):
    title = models.CharField(max_length=255)

class Choice(models.Model):
    poll = models.ForeignKey(Poll, related_name='choices')
    title = models.CharField(max_length=255)

A typical admin for Choice model is to use the inline admin :

class ChoiceAdminInline(admin.TabularInline):
    model = Choice

class PollAdmin(admin.ModelAdmin):
    list_display = ['id', 'title']
    inlines = [ChoiceAdminInline]

When we create a new Poll, we will see such a form with three choices provided by default, as described in Figure 1.

enter image description here Figure 1

Consider such a situation that we want to have two default choices with title choice_1 and choice_2 for every new poll, as indicated in Figure 2.

enter image description here Figure 2

Unfortunately, Django do not fully support initial data for inline formset. However, indeed, you can pass initial parameter when creating a formset like:

inline_initial_data = [
    {'title': 'choice_1'},
    {'title': 'choice_2'}
]
formset_params['initial'] = inline_initial_data
formsets.append(FormSet(**formset_params))

In Django implementation, when creating model formset, it will pop out the initial parameter from kwargs and assign it to initial_extra variable. Later in the _construct_form method, it use initial_extra as the form initial data if needed.

class BaseModelFormSet(BaseFormSet):
    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None, **kwargs):
        self.queryset = queryset
        self.initial_extra = kwargs.pop('initial', None)
        defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
        defaults.update(kwargs)
        super(BaseModelFormSet, self).__init__(**defaults)

    def _construct_form(self, i, **kwargs):
        ...
        if i >= self.initial_form_count() and self.initial_extra:
            # Set initial values for extra forms
            try:
                kwargs['initial'] = self.initial_extra[i - self.initial_form_count()]
            except IndexError:
                pass
        return super(BaseModelFormSet, self)._construct_form(i, **kwargs)

At a first glance, it seems Django do support initial data for inline model formset, but after carefully inspection, you will find it impossible due to the implementation of total_form_count method.

The detail Django implementation is trivial, just skip it. In order to support initial data for model inline admin, we need roughly three steps to do.

Customize Inline Formset

First, we need to customize inline formset. The key point here is to override method initial_form_count and total_form_count.

class CustomInlineFormset(BaseInlineFormSet):
    """
    Custom formset that support initial data
    """

    def initial_form_count(self):
        """
        set 0 to use initial_extra explicitly.
        """
        if self.initial_extra:
            return 0
        else:
            return BaseInlineFormSet.initial_form_count(self)

    def total_form_count(self):
        """
        here use the initial_extra len to determine needed forms
        """
        if self.initial_extra:
            count = len(self.initial_extra) if self.initial_extra else 0
            count += self.extra
            return count
        else:
            return BaseInlineFormSet.total_form_count(self)

After such configuration, our inline formset will correctly use initial extra to fill the initial for each of its forms when create forms, as indicated in _construct_form method above.

kwargs['initial'] = self.initial_extra[i - self.initial_form_count()]

Customize Model Form

After customization of inline formset, the model form can get initial data in its __init__ method. However, the model form will not save this initial data because it has not been changed. So we need to override the has_changed method, telling it we want to save initial data if provided.

class CustomModelForm(ModelForm):
    """
    Custom model form that support initial data when save
    """

    def has_changed(self):
        """
        Returns True if we have initial data.
        """
        has_changed = ModelForm.has_changed(self)
        return bool(self.initial or has_changed)

Customize Inline Admin

The last step we need to do is to combine custom inline formset and custom model form into our new inline admin.

class CustomInlineAdmin(admin.TabularInline):
    """
    Custom inline admin that support initial data
    """
    form = CustomModelForm
    formset = CustomInlineFormset

How to use

Now we have the custom inline admin that support initial data, we can use it in our Poll and Choice example.

We create ChoiceAdminInline that extends the CustomInlineAdmin, with extra setting to 1. In PollAdmin, we override the _create_formsets method to provide the initial formset parameter only when we create a new poll.

class ChoiceAdminInline(CustomInlineAdmin):
    model = Choice
    extra = 1


class PollAdmin(admin.ModelAdmin):
    list_display = ['id', 'title']
    inlines = [ChoiceAdminInline]

    def _create_formsets(self, request, obj, change):
        """overide to provide initial data for inline formset"""
        formsets = []
        inline_instances = []
        prefixes = {}
        get_formsets_args = [request]
        if change:
            get_formsets_args.append(obj)
        for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
            prefix = FormSet.get_default_prefix()
            prefixes[prefix] = prefixes.get(prefix, 0) + 1
            if prefixes[prefix] != 1 or not prefix:
                prefix = "%s-%s" % (prefix, prefixes[prefix])
            formset_params = {
                'instance': obj,
                'prefix': prefix,
                'queryset': inline.get_queryset(request),
            }
            if request.method == 'POST':
                formset_params.update({
                    'data': request.POST,
                    'files': request.FILES,
                    'save_as_new': '_saveasnew' in request.POST
                })

            if change:
                formsets.append(FormSet(**formset_params))
                inline_instances.append(inline)
            else:
                if isinstance(inline, ChoiceAdminInline):
                    inline_initial_data = [
                        {'title': 'choice_1'},
                        {'title': 'choice_2'}
                    ]
                    formset_params['initial'] = inline_initial_data
                    formsets.append(FormSet(**formset_params))
                    inline_instances.append(inline)
                else:
                    formsets.append(FormSet(**formset_params))
                    inline_instances.append(inline)

        return formsets, inline_instances

That's all. When we create new poll in Django admin, we will be provided with two default choices with title choice_1 and choice_2, just as shown in Figure 2.

Full Example

from django.contrib import admin
from django.forms.models import BaseInlineFormSet
from django.forms.models import ModelForm

# Register your models here.
from .models import Poll, Choice

class CustomInlineFormset(BaseInlineFormSet):
    """
    Custom formset that support initial data
    """

    def initial_form_count(self):
        """
        set 0 to use initial_extra explicitly.
        """
        if self.initial_extra:
            return 0
        else:
            return BaseInlineFormSet.initial_form_count(self)

    def total_form_count(self):
        """
        here use the initial_extra len to determine needed forms
        """
        if self.initial_extra:
            count = len(self.initial_extra) if self.initial_extra else 0
            count += self.extra
            return count
        else:
            return BaseInlineFormSet.total_form_count(self)


class CustomModelForm(ModelForm):
    """
    Custom model form that support initial data when save
    """

    def has_changed(self):
        """
        Returns True if we have initial data.
        """
        has_changed = ModelForm.has_changed(self)
        return bool(self.initial or has_changed)


class CustomInlineAdmin(admin.TabularInline):
    """
    Custom inline admin that support initial data
    """
    form = CustomModelForm
    formset = CustomInlineFormset


class ChoiceAdminInline(CustomInlineAdmin):
    model = Choice
    extra = 1


class PollAdmin(admin.ModelAdmin):
    list_display = ['id', 'title']
    inlines = [ChoiceAdminInline]

    def _create_formsets(self, request, obj, change):
        """overide to provide initial data for inline formset"""
        formsets = []
        inline_instances = []
        prefixes = {}
        get_formsets_args = [request]
        if change:
            get_formsets_args.append(obj)
        for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
            prefix = FormSet.get_default_prefix()
            prefixes[prefix] = prefixes.get(prefix, 0) + 1
            if prefixes[prefix] != 1 or not prefix:
                prefix = "%s-%s" % (prefix, prefixes[prefix])
            formset_params = {
                'instance': obj,
                'prefix': prefix,
                'queryset': inline.get_queryset(request),
            }
            if request.method == 'POST':
                formset_params.update({
                    'data': request.POST,
                    'files': request.FILES,
                    'save_as_new': '_saveasnew' in request.POST
                })

            if change:
                formsets.append(FormSet(**formset_params))
                inline_instances.append(inline)
            else:
                if isinstance(inline, ChoiceAdminInline):
                    inline_initial_data = [
                        {'title': 'choice_1'},
                        {'title': 'choice_2'}
                    ]
                    formset_params['initial'] = inline_initial_data
                    formsets.append(FormSet(**formset_params))
                    inline_instances.append(inline)
                else:
                    formsets.append(FormSet(**formset_params))
                    inline_instances.append(inline)

        return formsets, inline_instances


admin.site.register(Poll, PollAdmin)
Current rating: 4.8

Comments

ADDRESS

  • Email: jimmykobe1171@126.com
  • Website: www.catharinegeek.com