Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic fields in Django Admin

Tags:

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?

like image 797
Stephan Hoyer Avatar asked Nov 04 '11 09:11

Stephan Hoyer


4 Answers

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.

like image 116
Stephan Hoyer Avatar answered Sep 24 '22 01:09

Stephan Hoyer


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
like image 38
little_birdie Avatar answered Sep 23 '22 01:09

little_birdie


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
like image 37
tehfink Avatar answered Sep 25 '22 01:09

tehfink


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.

like image 23
Gillanius Avatar answered Sep 22 '22 01:09

Gillanius