Como posso requerer um inline no Django admin?
Pergunta
Eu tenho a seguinte configuração de administrador para que eu possa adicionar / editar um usuário e seu perfil ao mesmo tempo.
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)
O problema é que eu preciso dois dos campos do formulário de perfil linha a ser necessária ao adicionar o usuário. O formulário em linha não valida a menos que a entrada é inserido. Existe uma maneira de fazer a linha necessário, de modo que não pode estar em branco esquerdo?
Solução
Eu levei o conselho de Carl e fez uma implementação muito melhor, então o truque-ish mencionei no meu comentário à sua resposta. Aqui está a minha solução:
De minha 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
E o 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)
Este é exatamente o que eu quero, ele faz a validação perfil em linha formset. Então, uma vez que não são necessários campos do formulário de perfil que irá validar e falhar se a informação requerida não for introduzido no formulário em linha.
Outras dicas
Agora, com Django 1.7 você pode usar min_num
parâmetro. Você não precisa mais de classe RequiredInlineFormSet
.
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)
Você provavelmente pode fazer isso, mas você vai ter que sujar as mãos no código formset / embutido.
Em primeiro lugar, eu acho que você quer que haja sempre um formulário no formset neste caso, e nunca mais do que um, então você vai querer definir MAX_NUM = 1 e adicional = 1 em seu ProfileInline.
O problema central é que BaseFormSet._construct_form passes empty_permitted = True para cada forma "extra" (isto é, vazios) no formset. Este parâmetro informa o formulário para validação de bypass se o seu inalterada. Você só precisa encontrar uma maneira de conjunto empty_permitted = False para o formulário.
Você pode usar seu próprio BaseInlineFormset subclasse em sua em linha, de modo que ajuda poder. Notando que _construct_form leva kwargs ** e permite que para substituir as kwargs passados ??para as instâncias forma individual, você poderia substituir _construct_forms em seu Formset subclasse e tê-lo passar empty_permitted = False em cada chamada para _construct_form. A desvantagem não é que você está confiando em APIs internas (e você teria que reescrever _construct_forms).
Como alternativa, você pode tentar substituir o método get_formset em seu ProfileInline, e depois de chamar get_formset do pai, manualmente cutucar a forma dentro do formset devolvidos:
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
Jogar ao redor e ver o que você pode fazer o trabalho!
A maneira mais fácil e mais natural de fazer isso é através 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
(Inspirado por esta Matthew Flanagan do trecho e comentário de Mitar abaixo, testado para trabalhar em Django 1.11 e 2.0).
Você precisa definir MIN_NUM na linha e validate_min no formset.
https://docs.djangoproject.com/ en / 1.8 / temas / formas / 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