djangodjango-formsformset

Django - Editing/updating records on formset triggering duplicate error


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

Solution

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