Django - Редактирование/обновление записей в наборе форм приводит к ошибке дублирования
В Django у меня есть этот FBV, который обновляет выбранные записи:
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,
})
Все работает как положено (страница загружается, форма заполняется правильно и т.д.), кроме сохранения данных. Поле 'name' определено как уникальное в базе данных, поэтому, когда я пытаюсь изменить другие поля, но не меняю имя, оно выдает ошибку дублирования. Если я меняю имя на любое другое, которого еще нет в базе данных, оно корректно обновляет текущее имя, поэтому новая запись не создается. Я попытался изменить блок цикла на такой, чтобы посмотреть, смогу ли я сэкономить:
for form, accountplan in zip(formset, accountsplan_to_edit):
form.instance = accountplan
form.save()
return redirect('confi:list_accountsplan')
Но снова возникает ошибка дублирования. Я не понимаю, почему это происходит. Если я просто обновляю текущую запись, почему возникает ошибка дублирования? Как мне правильно обновить запись?
Вот код модели:
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
Причина неудачи в том, что ModelForm думает, что вы создаете новую запись, и поэтому проверяет, существует ли уже объект с заданным именем, и если да, то, конечно, обнаруживает, что имя уже существует, следовательно, форма недействительна. Проверьте это thread out
Также вы можете избежать проверки на самом экземпляре, используя метод clean. Вы добавляете это в класс вашей формы:
def clean_name(self):
name = self.cleaned_data['name']
existing = AccountsPlan.objects.filter(name=name).exclude(id=self.instance.id)
if existing.exists():
raise forms.ValidationError("This name is already in use.")
return name
Для решения этой проблемы хорошим подходом является использование Django's ModelForm вместо ручного обновления каждого поля. Это гарантирует, что ограничения уникальности модели будут проверяться на обновляемом экземпляре, а не глобально по всей таблице. Судя по вашему описанию, вы уже двигаетесь в этом направлении, но давайте убедимся, что все настроено правильно.
Убедитесь, что ваша форма AccountsPlanForm является ModelForm:
from django import forms
from .models import AccountsPlan
class AccountsPlanForm(forms.ModelForm):
class Meta:
model = AccountsPlan
fields = ['subgroup', 'name', 'active'] # Include all fields you need from the model.
Обновите ваше представление, чтобы правильно использовать экземпляры: Вам нужно передать конкретный экземпляр каждого AccountsPlan, который вы редактируете, в каждую форму в наборе форм, чтобы сообщить Django, что вы обновляете существующие записи, а не создаете новые.
from django.forms import modelformset_factory
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 = modelformset_factory(AccountsPlan, form=AccountsPlanForm, extra=0)
if request.method == 'POST':
formset = AccountsPlanFormSet(request.POST)
if formset.is_valid():
formset.save()
return redirect('confi:list_accountsplan')
else:
formset = AccountsPlanFormSet(queryset=accountsplan_to_edit)
return render(request, 'confi/pages/accountsplan/edit_selected_accountsplan.html', context={
'formset': formset,
})
Здесь modelformset_factory автоматически обрабатывает сопоставление между каждой формой в наборе форм и редактируемыми экземплярами AccountsPlan. Когда набор форм сохраняется с помощью функции formset.save(), каждая форма связывается с соответствующим экземпляром, а операция обновления учитывает существующие значения уникальных полей.
Оказалось, что есть гораздо лучший способ добиться желаемого, используя model formset. В этом случае не требуется никаких начальных данных или возни с формами. Вот моя реализация, основанная на документации:
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,
})
А в шаблоне мне просто нужно было добавить form.id к форме, как указано в документации:
{% 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>