Dynamic fields in Django Admin
Asked Answered
M

9

28

I want to have additional fields regarding value of one field. Therefor I build a custom admin form to add some new fields.

Related to the blogpost of jacobian 1 this is what I came up with:

class ProductAdminForm(forms.ModelForm):
    class Meta:
        model = Product

    def __init__(self, *args, **kwargs):
        super(ProductAdminForm, self).__init__(*args, **kwargs)
        self.fields['foo'] = forms.IntegerField(label="foo")

class ProductAdmin(admin.ModelAdmin):
    form = ProductAdminForm

admin.site.register(Product, ProductAdmin)

But the additional field 'foo' does not show up in the admin. If I add the field like this, all works fine but is not as dynamic as required, to add the fields regarding the value of another field of the model

class ProductAdminForm(forms.ModelForm):

    foo = forms.IntegerField(label="foo")

    class Meta:
        model = Product

class ProductAdmin(admin.ModelAdmin):
    form = ProductAdminForm

admin.site.register(Product, ProductAdmin)

So is there any initialize method that i have to trigger again to make the new field working? Or is there any other attempt?

Mairemaise answered 4/11, 2011 at 9:22 Comment(0)
M
26

Here is a solution to the problem. Thanks to koniiiik i tried to solve this by extending the *get_fieldsets* method

class ProductAdmin(admin.ModelAdmin):
    def get_fieldsets(self, request, obj=None):
        fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
        fieldsets[0][1]['fields'] += ['foo'] 
        return fieldsets

If you use multiple fieldsets be sure to add the to the right fieldset by using the appropriate index.

Mairemaise answered 5/11, 2011 at 14:39 Comment(5)
Unknown field(s) (foo) specified for GlobalLabel. Check fields/fieldsets/exclude attributes of class GlobalLabelAdmin. I am getting this error, I dunno why... can you please help me out ?Fluoridate
@bhushya: were you able to figure this out? I also can't get it to work in django 1.9.3, eg: django.core.exceptions.FieldError: Unknown field(s) (dynamicfield1, dynamicfield2) specified for MyModelPaperboard
@Paperboard it seems you have not defined field in your model.. can you please post your model structure on pastebin.com and share link?Fluoridate
@ bhushya: you're correct; the fields (dynamicfield1, etc.) are not defined on my model. Like in the original question, I want to add fields dynamically in a ModelForm, and the get_fieldsets override mentioned above doesn't seem to work in Django 1.9.3Paperboard
@bhushya: i found a potential solution for Django 1.9.3, posted belowPaperboard
C
15

The accepted answer above worked in older versions of django, and that's how I was doing it. This has now broken in later django versions (I am on 1.68 at the moment, but even that is old now).

The reason it is now broken is because any fields within fieldsets you return from ModelAdmin.get_fieldsets() are ultimately passed as the fields=parameter to modelform_factory(), which will give you an error because the fields on your list do not exist (and will not exist until your form is instantiated and its __init__ is called).

In order to fix this, we must override ModelAdmin.get_form() and supply a list of fields that does not include any extra fields that will be added later. The default behavior of get_form is to call get_fieldsets() for this information, and we must prevent that from happening:

# CHOOSE ONE
# newer versions of django use this
from django.contrib.admin.utils import flatten_fieldsets
# if above does not work, use this
from django.contrib.admin.util import flatten_fieldsets

class MyModelForm(ModelForm):
  def __init__(self, *args, **kwargs):
      super(MyModelForm, self).__init__(*args, **kwargs)
      # add your dynamic fields here..
      for fieldname in ('foo', 'bar', 'baz',):
          self.fields[fieldname] = form.CharField()

class MyAdmin(ModelAdmin): 
   form = MyModelForm

    fieldsets = [
       # here you put the list of fieldsets you want displayed.. only
       # including the ones that are not dynamic
    ]

    def get_form(self, request, obj=None, **kwargs):
        # By passing 'fields', we prevent ModelAdmin.get_form from
        # looking up the fields itself by calling self.get_fieldsets()
        # If you do not do this you will get an error from 
        # modelform_factory complaining about non-existent fields.

        # use this line only for django before 1.9 (but after 1.5??)
        kwargs['fields'] =  flatten_fieldsets(self.declared_fieldsets)
        # use this line only for django 1.9 and later 
        kwargs['fields'] =  flatten_fieldsets(self.fieldsets)

        return super(MyAdmin, self).get_form(request, obj, **kwargs)

    def get_fieldsets(self, request, obj=None):
        fieldsets = super(MyAdmin, self).get_fieldsets(request, obj)

        newfieldsets = list(fieldsets)
        fields = ['foo', 'bar', 'baz']
        newfieldsets.append(['Dynamic Fields', { 'fields': fields }])

        return newfieldsets
Cilium answered 20/8, 2015 at 0:47 Comment(8)
Unfortunately, ModelAdmin.declared_fieldsets has been removed in Django 1.9Paperboard
Hmm.. well I guess that when I upgrade my servers to 1.9 I'm going to have some work to do ;) But luckily I have replicated most of the admin functionality elsewhere in my application...Cilium
Also django.contrib.admin.util is now django.contrib.admin.utilsAfternoon
Thanks, does my answer still work otherwise? If so I'll correct it.Cilium
Hey what about django 2.5. I have some problems with kwargs['fields'] = flatten_fieldsets(self.fieldsets) flatten_fieldsets for name, opts in fieldsets: TypeError: 'NoneType' object is not iterableRehabilitation
Maybe you don't have fieldsets configured for your form? I'm on django 1.11 at the moment, I just finished upgrading to python 3 a couple of weeks ago so that I can go to django 2.x in the next few weeks. So if you can't figure it out, soon I will update my answer for django 2....Cilium
just update the line to kwargs['fields'] = flatten_fieldsets(self.fieldsets or []). The error happens if one hasn't declared the fieldsets in the classPenland
Great answer. Note that self.fields[fieldname] = form.CharField() should be self.fields[fieldname] = forms.CharField().Varicella
B
9

Maybe I am a bit late... However, I am using Django 3.0 and also wanted to dynamically ad some custom fields to the form, depending on the request.

I end up with a solution similar to the one described by @tehfink combined with @little_birdie.

However, just updating self.form.declared_fields as suggested didn't help. The result of this procedure is, that the list of custom fields defined in self.form.declared_fields always grows from request to request.

I solved this by initialising this dictionary first:

class ModelAdminGetCustomFieldsMixin(object):
    def get_fields(self, request, obj=None):
        fields = super().get_fields(request, obj=None)
        self.form.declared_fields = {}
        if obj:
            for custom_attribute in custom_attribute_list:
                self.form.declared_fields.update({custom_attribute.name: custom_attribute.field})
        return fields

where custom_attribute.field is a form field instance.

Additionally, it was required to define a ModelForm, wherein during initialisation the custom fields have been added dynamically as well:

class SomeModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for custom_attribute in custom_attribute_list:
            self.fields[custom_attribute.name] = custom_attribute.field

and use this ModelForm in the ModelAdmin.

Afterwards, the newly defined attributes can be used in, e.g., a fieldset.

Beatriz answered 3/7, 2020 at 17:3 Comment(2)
Hey, thanks for the Django 3.0 update, I'm kinda new in the framework. How did you implement the first class ModelAdminGetCustomFieldsMixin with SomeModelForm. I wish to show in Django admin this dynamic form based on the choice in a dropdown menu, that is being selected in the same model that will use this dynamic formGoldstone
Hi Francisco, it is vice versa: SomeModelForm is used in the ModelAdmin as form and the ModelAdminGetCustomFieldsMixin is a mixin, which needs to be given as an additional class to inherit from, e.g., SomeModelAdmin(ModelAdminGetCustomFieldsMixin, ModelAdmin). The page itself is static. The form will not change dynamically. You need either a page reload in order to change the form or a bunch of js.Beatriz
P
7

This works for adding dynamic fields in Django 1.9.3, using just a ModelAdmin class (no ModelForm) and by overriding get_fields. I don't know yet how robust it is:

class MyModelAdmin(admin.ModelAdmin):

    fields = [('title','status', ), 'description', 'contact_person',]
    exclude = ['material']

    def get_fields(self, request, obj=None):
        gf = super(MyModelAdmin, self).get_fields(request, obj)

        new_dynamic_fields = [
            ('test1', forms.CharField()),
            ('test2', forms.ModelMultipleChoiceField(MyModel.objects.all(), widget=forms.CheckboxSelectMultiple)),
        ]

        #without updating get_fields, the admin form will display w/o any new fields
        #without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin.

        for f in new_dynamic_fields:
            #`gf.append(f[0])` results in multiple instances of the new fields
            gf = gf + [f[0]]
            #updating base_fields seems to have the same effect
            self.form.declared_fields.update({f[0]:f[1]})
        return gf
Paperboard answered 4/3, 2016 at 13:28 Comment(0)
L
5

While Jacob's post might work all right for regular ModelForms (even though it's more than a year and a half old), the admin is a somewhat different matter.

All the declarative way of defining models, forms ModelAdmins and whatnot makes heavy use of metaclasses and class introspection. Same with the admin – when you tell a ModelAdmin to use a specific form istead of creating a default one, it introspects the class. It gets the list of fields and other stuff from the class itself without instantiating it.

Your custom class, however, does not define the extra form field at class level, instead it dynamically adds one after it has been instantiated – that's too late for the ModelAdmin to recognize this change.

One way to go about your problem might be to subclass ModelAdmin and override its get_fieldsets method to actually instantiate the ModelForm class and get the list of fields from the instance instead of the class. You'll have to keep in mind, though, that this might be somewhat slower than the default implementation.

Lucknow answered 4/11, 2011 at 15:6 Comment(0)
S
5

You can create dynamic fields and fieldset using the form meta class. Sample code is given below. Add the loop logic as per you requirements.

class CustomAdminFormMetaClass(ModelFormMetaclass):
    """
    Metaclass for custom admin form with dynamic field
    """
    def __new__(cls, name, bases, attrs):
        for field in get_dynamic_fields: #add logic to get the fields
            attrs[field] = forms.CharField(max_length=30) #add logic to the form field
        return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)


class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
    """
    Custom admin form
    """

    class Meta:
        model = ModelName
        fields = "__all__" 


class CustomAdmin(admin.ModelAdmin):
    """
    Custom admin 
    """

    fieldsets = None
    form = CustomAdminForm

    def get_fieldsets(self, request, obj=None):
        """
        Different fieldset for the admin form
        """
        self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
        return super(CustomAdmin, self).get_fieldsets(request, obj)

    def dynamic_fieldset(self):
        """
        get the dynamic field sets
        """
        fieldsets = []
        for group in get_field_set_groups: #logic to get the field set group
            fields = []
            for field in get_group_fields: #logic to get the group fields
                fields.append(field)

            fieldset_values = {"fields": tuple(fields), "classes": ['collapse']}
            fieldsets.append((group, fieldset_values))

        fieldsets = tuple(fieldsets)

        return fieldsets
Syphon answered 3/8, 2015 at 12:56 Comment(0)
A
3

Stephan's answer is elegant, but when I used in in dj1.6 it required the field to be a tuple. The complete solution looked like this:

class ProductForm(ModelForm):
    foo = CharField(label='foo')


class ProductAdmin(admin.ModelAdmin):
    form = ProductForm
    def get_fieldsets(self, request, obj=None):
        fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
        fieldsets[0][1]['fields'] += ('foo', ) 
        return fieldsets
Askari answered 31/7, 2014 at 13:2 Comment(0)
E
0

not sure why that's not working, but could a possible workaround be to define the field statically (on the form) and then override it in the __init__?

Enravish answered 4/11, 2011 at 15:17 Comment(0)
R
0

I for a long time could not solve a problem with dynamic addition of fields. The solution "little_birdie" really works. Thank you Birdie)) The only nuance is: "Self.declared_fieldsets" should be replaced with "self.fieldsets".

#kwargs['fields'] =  flatten_fieldsets(self.declared_fieldsets)
kwargs['fields'] =  flatten_fieldsets(self.fieldsets)

I used version 1.10. Perhaps something has changed.

If someone finds an even simpler and elegant solution, show here.

Thanks to all )))

Rorrys answered 12/3, 2017 at 12:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.