nehalem
nehalem

Reputation: 417

Manually laying out fields from an inline formset in Django

I have created an inline formset for the profile information which is added to the user form:

UserSettingsFormSet = inlineformset_factory(
    User,
    Profile,
    form=ProfileForm,
    can_delete=False,
    fields=(
        "title",
        ...
    ),
)


class SettingsView(UpdateView):
    model = User
    template_name = "dskrpt/settings.html"
    form_class = UserForm

    def get_object(self):
        return self.request.user

    def get_context_data(self, **kwargs):
        context = super(SettingsView, self).get_context_data(**kwargs)
        context["formset"] = UserSettingsFormSet(
            instance=self.request.user, prefix="user"
        )
        return context

This works and calling {formset} in the template file renders the complete form for both User and Profile.

Yet, I would like to lay out the individual inputs myself. This works for fields belonging to User:

<input 
  type="text" 
  name="{{form.last_name.html_name}}" 
  id="{{form.last_name.auto_id}}" value="{{form.last_name.value}}">

But doing the same for fields of the formset does not work. Here the attribute formset.form.title.value appears to be empty. All other attributes, e.g. formset.form.title.auto_id exist though.

Why does {{formset}} render completely with values but values are missing individually?

Upvotes: 0

Views: 678

Answers (1)

convers39
convers39

Reputation: 322

To answer your questions shortly:

The reason why profile fields are not showing is that they belong to a context data formset instead of form.

If you check the source code of UpdateView, it inherits from a ModelFormMixin, which defines methods related to forms, such as get_form_class.

Why this matter?

Because get_form_class grab the attribute form_class = UserForm or from your model attribute, and pass an context variable to your template called form. Thus in your template, the form variable only refers to UserForm, not your formset.

What is a context variable?

To make it simple that is a variable passed to your template, when your view is rendering the page, it will use those variables to fill in those {{}} slots.

I am sure you also use other generic views such as ListView, and probably you have overwritten the get_context_data method. That basically does the same thing to define what data should be passed to your template.

So how to solve?

Solution A: You need to use formset in your template instead of form, however this would be quite complicated:

<form action="" method="post" enctype="multipart/form-data">
    {{ form_set.management_form }}
    {{ form_set.non_form_errors }}

{% for form in formset.forms %}
    {% for field in form.visible_fields %}
        {# Include the hidden fields in the form #}
        {% if forloop.first %}
          {% for hidden in form.hidden_fields %}
             {{ hidden }}
          {% endfor %}
        {% endif %}
        {{ field.errors.as_ul }}
        {{ field }}
    {% endfor %}
    {{ form.non_field_errors }}
{% endfor %}

{# show errors #}
{% for dict in formset.errors %}
    {% for error in dict.values %}
        {{ error }}
    {% endfor %}
{% endfor %}
</form>

Solution B: Pass correct variable in your view, which will be easier to use:

class SettingsView(UpdateView):
    model = User 
    #...
    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        if self.request.POST:
            data["formset"] = UserSettingsFormset(self.request.POST, instance=self.object)
        else:
            data["formset"] = UserSettingsFormset(instance=self.object)
        return data

Then in your template you can simply do:

<h1>User Profile</h1>
<form method="post">{% csrf_token %}
    {{ form.as_p }}
    <h2>Profile</h2>
    {{ formset.as_p }}
    <input type="submit" value="Save">
</form>

I think in your formest the form attribute is not necessary form=ProfileForm,, by default inlineformset_factory will look for a ModelForm as form, and in this case is your User model.

Solution C: Is that really necessary to use a formset? Formest is usually used to generate multiple forms related to an object, which means should be considered to use in a many-to-one relationship.

Usually, for the case of User and Profile, they are inOneToOne relationship, namely, each user only has one profile object. In this case, you can just pass your profile form into your template:

class SettingsView(UpdateView):
    model = User 
    template_name = "dskrpt/settings.html"
    form_class = UserForm
    #...
    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        profile = self.request.user.profile
        # your Profile model should have an one-to-one field related with user, and related name should be 'profile'
        # or profile = Profile.objects.get(user=self.request.user)
        data['profile_form'] = ProfileForm(instance=profile)
        return data

Then in your template, you can refer to the profile form by profile_form only.

As a conclusion, I will suggest using solution C.

More detailed example for inline formsets: check this project

Upvotes: 1

Related Questions