Modify one form instance in a Django formset? - django

I have a Django formset in which I render four instances of a form. Each form in the formset has two fields, but I want the last (4th) instance to only show/input one field. How can I do this, or is there a better way? I tried limiting the formset to three fields and making the fourth instance its own form, but I need to validate that field1 against the field1 fields in the formset. I couldn't see how to validate a form against a simultaneously submitted formset.
views.py:
FormSet = formset_factory(MyForm, formset=BaseMyFormSet, extra=4)
.html:
<form action="" method="post">{% csrf_token %}
{{ formset.management_form }}
{{ formset.non_form_errors }}
<div>
{% for form in formset %}
{% for field in form %}
{{ field.errors }}
{{ field.label_tag }}: {{ field }}
{% endfor %}
{% endfor %}
</div>
<p><input type="submit" value="Submit" /></p>
</form>
The formset has two fields in forms.py:
class MyForm(forms.Form):
# field1
# field2
How do I make it so the first three forms in the formset have two fields, but the last only contains field1?

Try explicitly removing the field for the last form in your views.py:
def view_func(request):
FormSet = formset_factory(MyForm, formset=BaseMyFormSet, extra=4)
if request.method == 'POST':
formset = FormSet(request.POST, request.FILES)
# assuming this is a required field for the other forms
formset.forms[-1].fields['field2'].required = False
if formset.is_valid():
...
else:
formset = FormSet()
del formset.forms[-1].fields['field2']
return render(request, 'form.html', {'formset': formset})
Any further adjustments depend on your form and validation logic. BaseMyFormSet.clean() is the appropriate place for formset-wide validation and it sounds like you already have code there to compare field1 across forms.
(From a purist sense, it might be better to have the FormSet class handle this entirely, but this is easier. FormSet code is decently complex. It'd make sense to override BaseFormSet.forms() but with that #cached_property bit, you're involved with implementation details best left to Django.)

Related

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?
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

Edit a Key/Value Parameters list Formset

I'm looking for a convenient solution to create an 'edit settings' key/values page.
Parameters model :
class Parameter(models.Model):
key = models.CharField(max_length=50)
value = models.CharField(max_length=250)
showInUI = models.SmallIntegerField()
Initial Keys/Values are already inserted in table.
I load them and send them using a model formset factory using these lines :
ParameterFormSet = modelformset_factory(Parameter, extra=0, fields=('key', 'value'))
parameterFormSet = ParameterFormSet(queryset=Parameter.objects.filter(showInUI=1))
return render_to_response('config.html', {'parameterFormSet': parameterFormSet}, context_instance=RequestContext(request))
Template side, when formset is displayed, keys and values are shown as inputs.
I'd like to find a convenient way to display form keys as readonly labels and values as inputs. And, when submited, validate them according django standards.
I've read a lot of stuff, I guess the solution may be a custom widget, but I could find a reliable solution.
Thanks for reading.
EDIT :
Working solution
views.py
def config(request):
ParameterFormSet = modelformset_factory(Parameter, extra=0, fields=('value',))
if request.method == "POST":
try:
formset = ParameterFormSet(request.POST, request.FILES)
except ValidationError:
formset = None
return HttpResponse("ko")
if formset.is_valid():
formset.save()
return HttpResponse("ok")
#ParameterFormSet = modelformset_factory(Parameter, extra=0, fields=('value',))
parameterFormSet = ParameterFormSet(queryset=Parameter.objects.filter(showInUI=1))
return render_to_response('config.html', {'parameterFormSet': parameterFormSet}, context_instance=RequestContext(request))
template
<form method="post">
{% csrf_token %}
{{ parameterFormSet.management_form }}
{% for form in parameterFormSet %}
<div>
{{ form.instance.key }}
{{ form }}
</div>
{% endfor %}
<input type="submit" />
</form>
If you do not want the value to be editable, don't include it in fields when creating the form set.
ParameterFormSet = modelformset_factory(Parameter, extra=0, fields=('value',)) # don't forget the trailing comma after 'value' otherwise it's not a tuple!
In your template, you can then loop through the forms in the form set, and display the key at the same time.
{% for form in parameter_form_set %}
{{ form.instance.key }}{# display the key related to this form #}
{{ form }}{# display the form #}
{% endfor %}

Django model formset query generates extra object

I want to create a formset of confirmation models. I've succesfully created the formset however formset creates an extra confirmation object.
Here is my code:
VIEW
def render_fulfillment_modal(request,template='test.html'):
....
formset = modelformset_factory(Confirmation)
form = formset(queryset=Confirmation.objects.filter(customer_order__deal = deal))
if request.method == 'POST':
form = formset(request.POST, request.FILES)
if form.is_valid():
form.save()
TEMPLATE
<form method="post" action="{% url open_fullfill_modal deal.id %}">{% csrf_token %}
{{ form.management_form }}
{% for f in form %}
<tr>
<td>{{f.fullfilled}}</td>
<td>
<p class="name">{{f.instance|confirmation_user_info}}</p>
</td>
<td><input type="text" class="input-small datepicker"></td>
<td>{{f.tracking_code}}</td>
</tr>
{% endfor %}
<div class="pull-right button-box">
<button type="submit" class="btn btn-primary btn-large">Save Changes</button>
</div>
I'm getting an extra form for unrelated object which is not in my queryset. I've tried this with another models and each time I'm getting an extra object. I suppose it's something with the formsets to handle data or something, I'm not sure. The problem occurs when I post this form. It gives me MultiValueDictKeyError which is :
"Key 'form-0-id' not found in <QueryDict: {u'form-MAX_NUM_FORMS': [u''], u'form-TOTAL_FORMS': [u'3'] ...
Any ideas ?
Just put a {{f.id}} before {{f.fullfilled}}
It's given a hidden form-id for all f, and pass it to QueryDict in request.Post
As you can see from the definition of modelformset_factory below (django docs) the extra parameter defaults to 1, which creates the extra object you mentioned.
modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True)
So all you need to pass extra=0 to the formset:
formset = modelformset_factory(Confirmation, extra=0)
From Django docs: Limiting the number of editable objects
As with regular formsets, you can use the max_num and extra parameters to modelformset_factory() to limit the number of extra forms displayed.

Django Inline Formset Custom Validation only Validates Single Formset at a time

I'm using Django and have a form with two additional inline formsets. I want to validate that each formset contains at least one populated form. I've written code such that this works but it only works for each formset at a time. If I submit the form without any formset forms populated, only the first one shows a validation error. If I then populate the first formset form, and leave the second one blank, the second one errors.
I want errors to appear on both forms if both are not valid.
The forms are just standard ModelForm instances. Here's my view:
class RequiredBaseInlineFormSet(BaseInlineFormSet):
def clean(self):
self.validate_unique()
if any(self.errors):
return
if not self.forms[0].has_changed():
raise forms.ValidationError("At least one %s is required" % self.model._meta.verbose_name)
def create(request):
profile_form = ProfileForm(request.POST or None)
EmailFormSet = inlineformset_factory(Profile, Email, formset=RequiredBaseInlineFormSet, max_num=5, extra=5, can_delete=False)
email_formset = EmailFormSet(request.POST or None)
PhoneFormSet = inlineformset_factory(Profile, Phone, formset=RequiredBaseInlineFormSet, max_num=5, extra=5, can_delete=False)
phone_formset = PhoneFormSet(request.POST or None)
if profile_form.is_valid() and email_formset.is_valid() and phone_formset.is_valid():
profile = profile_form.save()
emails = email_formset.save(commit=False)
for email in emails:
email.profile = profile
email.save()
phones = phone_formset.save(commit=False)
for phone in phones:
phone.profile = profile
phone.save()
messages.add_message(request, messages.INFO, 'Profile successfully saved')
return render_to_response(
'add.html', {
'profile_form': profile_form,
'email_formset': email_formset,
'phone_formset': phone_formset
}, context_instance = RequestContext(request)
)
And here's my template's form, incase it's useful:
<form action="" method="post" accept-charset="utf-8">
{{ email_formset.management_form }}
{{ phone_formset.management_form }}
{{ profile_form|as_uni_form }}
<div class="formset-group" id="email_formset">
{{ email_formset.non_form_errors }}
{% for email_form in email_formset.forms %}
<div class='form'>
{{ email_form|as_uni_form }}
</div>
{% endfor %}
</div>
<div class="formset-group" id="phone_formset">
{{ phone_formset.non_form_errors }}
{% for phone_form in phone_formset.forms %}
<div class='form'>
{{ phone_form|as_uni_form }}
</div>
{% endfor %}
</div>
<input type="submit" value="Save Profile" id="submit">
</form>
call the is_valid() function for each form that you want validation to occur on. In your example you do if a.is_valid and b.is_valid anc c.is_valid... If a is false, b and c will never get called. Try something different, like:
alpha=a.is_valid()
beta=b.is_valid()
gamma=c.is_valid()
if alpha and beta and gamma:
do stuff
I had a similar issue and the problem was that extra forms were not being validated due to how Django handles extra form fields. Take a look: Django Formset.is_valid() failing for extra forms

Django formset doesn't validate

I am trying to save a formset but it seems to be bypassing is_valid() even though there are required fields.
To test this I have a simple form:
class AlbumForm(forms.Form):
name = forms.CharField(required=True)
The view:
#login_required
def add_album(request, artist):
artist = Artist.objects.get(slug__iexact=artist)
AlbumFormSet = formset_factory(AlbumForm)
if request.method == 'POST':
formset = AlbumFormSet(request.POST, request.FILES)
if formset.is_valid():
return HttpResponse('worked')
else:
formset = AlbumFormSet()
return render_to_response('submissions/addalbum.html', {
'artist': artist,
'formset': formset,
}, context_instance=RequestContext(request))
And the template:
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{{ formset.management_form }}
{% for form in formset.forms %}
<ul class="addalbumlist">
{% for field in form %}
<li>
{{ field.label_tag }}
{{ field }}
{{ field.errors }}
</li>
{% endfor %}
</ul>
{% endfor %}
<div class="inpwrap">
<input type="button" value="add another">
<input type="submit" value="add">
</div>
</form>
What ends up happening is I hit "add" without entering a name then HttpResponse('worked') get's called seemingly assuming it's a valid form.
I might be missing something here, but I can't see what's wrong. What I want to happen is, just like any other form if the field is required to spit out an error if its not filled in. Any ideas?
Heh, I was having this exact same problem. The problem is that you're using a formset!! Formsets allow all fields in a form to be blank. If, however, you have 2 fields, and fill out only one, then it will recognize your required stuffs. It does this because formsets are made for "bulk adding" and sometimes you don't want to fill out all the extra forms on a page. Really annoying; you can see my solution here.
For each of the fields that are required, add an extra entry in the attrs parameter
resident_status = forms.ChoiceField(widget=forms.Select(
attrs={'class': 'form-control', 'required': 'required'}), choices=President.RESIDENT_STATUS,
required=True)
As you can see, I maintain the required=True for django's form validation but specify 'required':'required' for the template to insist for the field be required.
Hope that helps.
Add 2 lines.
if request.method == 'POST':
def initial_form_count(self): return 10 # the number of forms
AlbumFormSet.initial_form_count = initial_form_count
formset = AlbumFormSet(request.POST, request.FILES)
Good luck!
use:
if not any(formset.errors): ...
instead of:
if formset.is_valid(): ...