Why the additional fields that I am trying to add to the form are not being rendered?

class QuestionForm(forms.ModelForm):

  def __init__(self, *args, **kwargs):
      super().__init__(*args, **kwargs)
      self.extra_attributes = Attribute.objects.all()
      question_attribute_values = {}
      if self.instance.pk:
          question_attribute_values = {
              attr.attribute.id : attr.value 
              for attr in QuestionAttribute.objects.filter(question=self.instance)
          }

      for attribute in self.extra_attributes:
          self.fields[f"attribute_{attribute.id}"] = forms.CharField(
              label=attribute.name, 
              required=False, 
              initial=question_attribute_values.get(attribute.id, '')
          )

  def save(self, commit=True):
      question = super().save(commit=commit)
      for attribute in self.extra_attributes:
          value = self.cleaned_data[f"attribute_{attribute.id}"]
          QuestionAttribute.objects.update_or_create(
              question=question,
              attribute=attribute,
              defaults={"value": value}
          )

      return question

  class Meta:
      model = Question
      exclude = ('deleted_at', 'restored_at', 'transaction_id')

Welcome @amosanurag !

It would help if you posted the models and the template where you are rendering the form.

Also, are you getting any error messages in the console with this? Or are these fields simply not being displayed on the page?

Hello @KenWhitesell,

First off, I just wanted to say I’m a big fan and have been following your responses on the Django forum for quite some time—they’ve been incredibly helpful!

Context:
I’m building a custom form for the Question model in the QuestionAdmin class. The goal is to allow dynamic addition of fields based on attributes associated with the Question model. Here’s the setup:

  • I added a Many-to-Many relationship between the Question model and an Attribute model via an intermediate QuestionAttribute model.
  • The QuestionAttribute model includes the fields: question_id, attribute_id, and a value (a CharField).
  • Conventionally, these additional fields could be displayed using inlines, but I want to render them as regular fields directly in the admin form, prefilled with values (if any).

Problem:
When customizing the form in QuestionAdmin, only the fields from the Question model are rendered. The dynamically added fields from the Attribute model (via QuestionAttribute) are not being displayed.

Code Overview:
Here’s a brief summary of the models for context:

# The QuestionAdmin
class QuestionAdmin(admin.ModelAdmin):
    list_display = ('id', 'question_text', 'answer_text', 'image_id', 'image_exists_in_gcp', 'view_link')
    list_filter = ('level_tags','topic', 'image_exists_in_gcp')
    form = QuestionForm
    readonly_fields = ['image_exists_in_gcp']

# The models :
class Question(SoftDeleteModel):
    question_text = models.TextField()
    image = models.ImageField(upload_to="question_images/", blank=True, null=True)
    image_id = models.CharField(max_length=100,blank=True, null=True)
    image_exists_in_gcp = models.BooleanField(default=False, verbose_name="Does image exist in GCP")
    answer_text = models.TextField()
    additional_image=models.CharField(max_length=100,blank=True, null=True)
    image_request=models.TextField(blank=True, null=True)
    visible_extra_info=models.TextField(blank=True, null=True)
    hidden_extra=models.CharField(max_length=255,blank=True, null=True)
    level_tags=models.CharField(max_length=100,blank=True, null=True)
    subject_tags=models.CharField(max_length=100,blank=True, null=True)
    keywords=models.CharField(max_length=100,blank=True, null=True)
    card_family=models.CharField(max_length=100,blank=True, null=True)
    needs_a_mirrored_card=models.CharField(max_length=100,blank=True, null=True)
    additional_chapters=models.CharField(max_length=100,blank=True, null=True)
    directionality=models.CharField(max_length=100,blank=True, null=True)
    created_map=models.CharField(max_length=255,blank=True, null=True)
    notes=models.CharField(max_length=255,blank=True, null=True)
    ap_terms=models.CharField(max_length=100,blank=True, null=True)
    texas_standards=models.CharField(max_length=100,blank=True, null=True)
    va_standards=models.CharField(max_length=100,blank=True, null=True)
    ny_standards=models.CharField(max_length=100,blank=True, null=True)
    former_images=models.CharField(max_length=100,blank=True, null=True)
    topic=models.CharField(max_length=255,blank=True, null=True)
    chapters=models.ManyToManyField(Chapter, related_name="questions")
    extras = models.ManyToManyField("Attribute", through="QuestionAttribute", related_name="questions")

    class Meta:
        db_table = "questions"

    def __str__(self):
        return self.question_text  # Return first 50 characters of the question text


class Attribute(models.Model):
    name = models.CharField(max_length=150, unique=True)

    def clean(self):
        # Ensure the attribute name does not conflict with any Question model field names, foreign keys, or many-to-many fields
        prohibited_names = [field.name for field in Question._meta.get_fields()]
        if self.name in prohibited_names:
            raise ValidationError(f"Attribute name '{self.name}' conflicts with an existing field name.")
    
    def __str__(self):
        return self.name


class QuestionAttribute(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    attribute = models.ForeignKey(Attribute, on_delete=models.CASCADE)
    value = models.CharField(max_length=255, null=True)

    class Meta:
        unique_together = ('question', 'attribute')

    def __str__(self):
        return f"{self.question} - {self.attribute}: {self.value}"

I don’t have a definitive answer for this, but I’d guess you’d need to override the get_form method on the ModelAdmin class in addition to (possibly) needing to override the fields attribute. You’ll want to read the source code for that class to get a feel of what needs to be changed.

Fundamentally however, I think this is the wrong solution.

If you need to do this because this is an “end-user-facing page”, then you probably shouldn’t be using the admin for this.

Quoting from the docs at The Django admin site | Django documentation | Django

If you need to provide a more process-centric interface that abstracts away the implementation details of database tables and fields, then it’s probably time to write your own views.

I’ve seen it happen many times. You could easily spend much more time trying to make the admin work the way you want it to than you would by making a custom view.