29

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?

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129

7 Answers7

35

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)
quick
  • 1,104
  • 10
  • 19
  • 1
    This only controls the number of inlines that are shown. It doesn't require that any are filled in. – spookylukey May 19 '15 at 11:53
  • 6
    Actually if inline has one of the field required, it also control that inline itself is filled in. – quick May 20 '15 at 13:55
  • 7
    Testing in 1.10, even with "min_num" set still allows users to select "Delete" on all records, leaving you with no records. – joebeeson Jan 31 '17 at 15:28
34

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.

L42y
  • 972
  • 1
  • 9
  • 18
  • 2
    If you are using `GenericInlineModelAdmin`, replace `BaseInlineFormSet` with `BaseGenericInlineFormSet`. – L42y Feb 24 '12 at 03:39
  • 1
    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. – Bouke May 13 '12 at 10:16
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!

Carl Meyer
  • 122,012
  • 20
  • 106
  • 116
  • 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! –  Jul 31 '09 at 16:07
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).

mrts
  • 16,697
  • 8
  • 89
  • 72
  • 2
    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`. – Mitar Jun 13 '12 at 20:37
7

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
un1t
  • 4,259
  • 2
  • 30
  • 33
  • 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. – Luis Palacios Jul 15 '19 at 15:22
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

Mahbub Alam
  • 368
  • 2
  • 6
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]
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129