In Django, I have this FBV that updates selected records:
def edit_selected_accountsplan(request):
selected_items_str = request.GET.get('items', '')
selected_items = [int(item) for item in selected_items_str.split(',') if item.isdigit()]
accountsplan_to_edit = AccountsPlan.objects.filter(id__in=selected_items)
AccountsPlanFormSet = formset_factory(form=AccountsPlanForm, extra=0)
if request.method == 'POST':
formset = AccountsPlanFormSet(request.POST)
if formset.is_valid():
for form, accountplan in zip(formset, accountsplan_to_edit):
accountplan.subgroup = form.cleaned_data['subgroup']
accountplan.name = form.cleaned_data['name']
accountplan.active = form.cleaned_data['active']
accountplan.save()
return redirect('confi:list_accountsplan')
else:
initial_data = []
for accountplan in accountsplan_to_edit:
initial_data.append({
'subgroup': accountplan.subgroup,
'name': accountplan.name,
'active': accountplan.active,
})
formset = AccountsPlanFormSet(initial=initial_data)
return render(request, 'confi/pages/accountsplan/edit_selected_accountsplan.html', context={
'formset': formset,
})
Everything works as expected (the page loads, the form is filled correctly etc.) except when saving the data. The 'name' field is defined as unique in the database, so when I try to change other fields but don't change the name, it gives me a duplicate error. If I change the name to anything else that is not already in the database, it updates the current name correctly, so it's not creating a new record. I've tried to change the loop block to this just to see if I could save:
for form, accountplan in zip(formset, accountsplan_to_edit):
form.instance = accountplan
form.save()
return redirect('confi:list_accountsplan')
But again, the duplicate error is triggered. I don't understand why this is happening. If I'm just updating the current record, why is it giving a duplicate error? How can I have this updated correctly?
Here's the model code:
class AccountsPlan (models.Model):
subgroup = models.ForeignKey(Subgroups, on_delete=models.PROTECT)
name = models.CharField(
max_length=100,
unique=True,
error_messages={
'unique': "An account with this name already exists. Please, choose another.",
}
)
active = models.BooleanField(default=True)
show_acc = models.BooleanField(default=True)
show_man = models.BooleanField(default=True)
class Meta:
verbose_name_plural = 'AccountPlans'
def __str__(self):
return self.name
Turns out there's a much better way to achieve what I want by using a model formset. This way no initial data or messing with forms is necessary. Here's my implementation, based on the docs:
def edit_selected_accountsplan(request):
selected_items_str = request.GET.get('items', '')
selected_items = [int(item) for item in selected_items_str.split(',') if item.isdigit()]
AccountsPlanFormSet = modelformset_factory(AccountsPlan, fields=('subgroup', 'name', 'active'), extra=0)
if request.method == 'POST':
formset = AccountsPlanFormSet(request.POST, queryset=AccountsPlan.objects.filter(id__in=selected_items))
if formset.is_valid():
formset.save()
return redirect('confi:list_accountsplan')
else:
formset = AccountsPlanFormSet(queryset=AccountsPlan.objects.filter(id__in=selected_items))
return render(request, 'confi/pages/accountsplan/edit_selected_accountsplan.html', context={
'formset': formset,
})
And in the template, I just had to add the form.id to the form, as stated in the docs:
{% for form in formset %}
{{ form.id }} # added this
<tr>
<td>{{ form.subgroup }}</td>
<td>{{ form.name }}</td>
<td>{{ form.active }}</td>
<td>
{% if form.errors %}
{% for error in form.errors %}
{{ error }}
{% endfor %}
{% endif %}
</td>
</tr>