How do I require an inline in the Django Admin?
Asked Answered
S

7

30

I have the following admin setup so that I can add/edit a user and their profile at the same time.

class ProfileInline(admin.StackedInline):
    """
    Allows profile to be added when creating user
    """
    model = Profile


class UserProfileAdmin(admin.ModelAdmin):
    """
    Options for the admin interface
    """
    inlines = [ProfileInline]
    list_display = ['edit_obj', 'name', 'username', 'email', 'is_active',
        'last_login', 'delete_obj']
    list_display_links = ['username']
    list_filter = ['is_active']
    fieldsets = (
        (None, {
            'fields': ('first_name', 'last_name', 'email', 'username',
                'is_active', 'is_superuser')}),
        )
    ordering = ['last_name', 'first_name']
    search_fields = ['first_name', 'last_name']

admin.site.register(User, UserProfileAdmin)

The problem is I need two of the fields in the Profile inline form to be required when adding the user. The inline form doesn't validate unless input is entered. Is there anyway to make the inline required, so that it can't be left blank?

Staphyloplasty answered 30/7, 2009 at 14:21 Comment(0)
S
35

I took Carl's advice and made a much better implementation then the hack-ish one I mentioned in my comment to his answer. Here is my solution:

From my forms.py:

from django.forms.models import BaseInlineFormSet


class RequiredInlineFormSet(BaseInlineFormSet):
    """
    Generates an inline formset that is required
    """

    def _construct_form(self, i, **kwargs):
        """
        Override the method to change the form attribute empty_permitted
        """
        form = super(RequiredInlineFormSet, self)._construct_form(i, **kwargs)
        form.empty_permitted = False
        return form

And the admin.py

class ProfileInline(admin.StackedInline):
    """
    Allows profile to be added when creating user
    """
    model = Profile
    extra = 1
    max_num = 1
    formset = RequiredInlineFormSet


class UserProfileAdmin(admin.ModelAdmin):
    """
    Options for the admin interface
    """
    inlines = [ProfileInline]
    list_display = ['edit_obj', 'name', 'username', 'email', 'is_active',
        'last_login', 'delete_obj']
    list_display_links = ['username']
    list_filter = ['is_active']
    fieldsets = (
        (None, {
            'fields': ('first_name', 'last_name', 'email', 'username',
                'is_active', 'is_superuser')}),
        (('Groups'), {'fields': ('groups', )}),
    )
    ordering = ['last_name', 'first_name']
    search_fields = ['first_name', 'last_name']


admin.site.register(User, UserProfileAdmin)

This does exactly what I want, it makes the Profile inline formset validate. So since there are required fields in the profile form it will validate and fail if the required information isn't entered on the inline form.

Staphyloplasty answered 5/8, 2009 at 14:32 Comment(2)
If you are using GenericInlineModelAdmin, replace BaseInlineFormSet with BaseGenericInlineFormSet.Cytoplasm
Thanks! However, if your form does not have required fields, it will still not validate and save when empty. Monkey-patch the form using form.has_changed = lambda: True to have it saved despite being empty.Juryrig
C
36

Now with Django 1.7 you can use parameter min_num. You do not need class RequiredInlineFormSet anymore.

See https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.min_num

class ProfileInline(admin.StackedInline):
    """
    Allows profile to be added when creating user
    """
    model = Profile
    extra = 1
    max_num = 1
    min_num = 1 # new in Django 1.7


class UserProfileAdmin(admin.ModelAdmin):
    """
    Options for the admin interface
    """
    inlines = [ProfileInline]
    ...


admin.site.register(User, UserProfileAdmin)
Chaunceychaunt answered 17/5, 2015 at 16:35 Comment(3)
This only controls the number of inlines that are shown. It doesn't require that any are filled in.Persis
Actually if inline has one of the field required, it also control that inline itself is filled in.Chaunceychaunt
Testing in 1.10, even with "min_num" set still allows users to select "Delete" on all records, leaving you with no records.Dysphemia
S
35

I took Carl's advice and made a much better implementation then the hack-ish one I mentioned in my comment to his answer. Here is my solution:

From my forms.py:

from django.forms.models import BaseInlineFormSet


class RequiredInlineFormSet(BaseInlineFormSet):
    """
    Generates an inline formset that is required
    """

    def _construct_form(self, i, **kwargs):
        """
        Override the method to change the form attribute empty_permitted
        """
        form = super(RequiredInlineFormSet, self)._construct_form(i, **kwargs)
        form.empty_permitted = False
        return form

And the admin.py

class ProfileInline(admin.StackedInline):
    """
    Allows profile to be added when creating user
    """
    model = Profile
    extra = 1
    max_num = 1
    formset = RequiredInlineFormSet


class UserProfileAdmin(admin.ModelAdmin):
    """
    Options for the admin interface
    """
    inlines = [ProfileInline]
    list_display = ['edit_obj', 'name', 'username', 'email', 'is_active',
        'last_login', 'delete_obj']
    list_display_links = ['username']
    list_filter = ['is_active']
    fieldsets = (
        (None, {
            'fields': ('first_name', 'last_name', 'email', 'username',
                'is_active', 'is_superuser')}),
        (('Groups'), {'fields': ('groups', )}),
    )
    ordering = ['last_name', 'first_name']
    search_fields = ['first_name', 'last_name']


admin.site.register(User, UserProfileAdmin)

This does exactly what I want, it makes the Profile inline formset validate. So since there are required fields in the profile form it will validate and fail if the required information isn't entered on the inline form.

Staphyloplasty answered 5/8, 2009 at 14:32 Comment(2)
If you are using GenericInlineModelAdmin, replace BaseInlineFormSet with BaseGenericInlineFormSet.Cytoplasm
Thanks! However, if your form does not have required fields, it will still not validate and save when empty. Monkey-patch the form using form.has_changed = lambda: True to have it saved despite being empty.Juryrig
D
10

You can probably do this, but you'll have to get your hands dirty in the formset/inline code.

First of all, I think you want there to be always one form in the formset in this case, and never more than one, so you'll want to set max_num=1 and extra=1 in your ProfileInline.

Your core problem is that BaseFormSet._construct_form passes empty_permitted=True to each "extra" (i.e. empty) form in the formset. This parameter tells the form to bypass validation if its unchanged. You just need to find a way to set empty_permitted=False for the form.

You can use your own BaseInlineFormset subclass in your inline, so that might help. Noticing that _construct_form takes **kwargs and allows that to override the kwargs passed to the individual Form instances, you could override _construct_forms in your Formset subclass and have it pass empty_permitted=False in every call to _construct_form. The downside there is that you're relying on internal APIs (and you'd have to rewrite _construct_forms).

Alternatively, you could try overriding the get_formset method on your ProfileInline, and after calling the parent's get_formset, manually poke at the form inside the returned formset:

def get_formset(self, request, obj=None, **kwargs):
    formset = super(ProfileInline, self).get_formset(request, obj, **kwargs)
    formset.forms[0].empty_permitted = False
    return formset

Play around and see what you can make work!

Doubletalk answered 31/7, 2009 at 0:27 Comment(1)
Thanks for the information. I did come up with a solution but it is very hack-ish, and I'm not particularly proud of it. I ended up overrideing the add_view for the ModelAdmin and copying all of the code from the default view and modifying the formset values. I'm going to take a look at your suggestions to see if I can implement it in a cleaner way. Thanks for the leads!Staphyloplasty
A
9

You need to set min_num in inline and validate_min in formset.

https://docs.djangoproject.com/en/1.8/topics/forms/formsets/#validate-min

class SomeInline(admin.TabularInline):
    ...
    min_num = 1

    def get_formset(self, request, obj=None, **kwargs):
        formset = super().get_formset(request, obj=None, **kwargs)
        formset.validate_min = True
        return formset
Antidisestablishmentarianism answered 20/12, 2018 at 11:49 Comment(1)
Why isn't this the most voted and accepted answer? This is a highly clean solution, Only thing I would ask is how do I customize the validation error it reads: Please submit {min_num} or more forms.Magnify
T
8

The easiest and most natural way to do that is via fomset clean():

class RequireOneFormSet(forms.models.BaseInlineFormSet):
    def clean(self):
        super().clean()
        if not self.is_valid():
            return
        if not self.forms or not self.forms[0].cleaned_data:
            raise ValidationError('At least one {} required'
                                  .format(self.model._meta.verbose_name))

class ProfileInline(admin.StackedInline):
    model = Profile
    formset =  RequireOneFormSet

(Inspired by this Matthew Flanagan's snippet and Mitar's comment below, tested to work in Django 1.11 and 2.0).

Tree answered 4/2, 2010 at 12:10 Comment(1)
Perfect! I changed it a bit, used if not self.is_valid(): instead of manually going through self.errors and used self.model._meta.verbose_name.Eyewash
T
3

As of Django 3+ this is as simple as you like:

class EmployeeAddressMap(admin.StackedInline):
    model = EmployeeAddress
    min_num = 1
    max_num = 1
    can_delete = False #specified that this cannnot be removed

Happy coding

Tendentious answered 4/11, 2020 at 20:56 Comment(0)
H
1

Mar, 2022 Update:

"min_num" can make inline fields required. (Inline fields are unrequired by default and 3 unrequired inline fields are displayed by default)

So, if you want 2 required inline fields, set "min_num = 2" as shown below and in this case below, 2 required and 3 unrequired inline fields are displayed and you can add more unrequired inline fields but you cannot add required inline fields in the form:

from django.contrib import admin
from .models import Profile, UserProfile

class ProfileInline(admin.TabularInline):
    model = Profile
    min_num = 2  # 2 required inline fields displayed
                 # 3 unrequired inline fields displayed by default

@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
    inlines = [ProfileInline]

In addition, you can use "extra" to set the number of unrequired fields displayed and you can use "max_num" to set the maximum number of the total inline fields including required and unrequired inline fields. So, in this case below, the maximum number of the total inline fields are 10 and 3 required and 2 unrequired inline fields are displayed and you can add 5 more unrequired inline fields as a maximum but you cannot add required inline fields in the form:

from django.contrib import admin
from .models import Profile, UserProfile

class ProfileInline(admin.TabularInline):
    model = Profile
    min_num = 3  # 3 required inline fields displayed
    extra = 2    # 2 unrequired inline fields displayed
    max_num = 10 # 10 inline fields as a maximum

@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
    inlines = [ProfileInline]
Hage answered 19/3, 2022 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.