Inlineformset_factory saving parent without child and not displaying validation errors if child is none

倾然丶 夕夏残阳落幕 提交于 2020-12-15 05:19:09

问题


I am having 2 issues, one if you submit and click back and then submit again it duplicates the instance in the database - in this case Household. In addition it is saving the parent 'Household' without the child 'Applicants' despite me setting min_num=1

can someone point me in the right direction to resolve this issue.

Many thanks in advance

class Application(models.Model):
    name = models.CharField(max_length=100, blank=True, null=True)
    application_no = models.CharField(max_length=100, unique=True, default=create_application_no)
    created_date = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )

class HouseHold(models.Model):
    name = models.CharField(max_length=100)
    application = models.ForeignKey(Application, on_delete=models.CASCADE)
    no_of_dependents = models.PositiveIntegerField(default=0)

class Applicant(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    household = models.ForeignKey("HouseHold", on_delete=models.CASCADE)

forms.py

class ApplicationForm(ModelForm):
    class Meta:
        model = Application
        fields = (
            "name",
        )


class ApplicantForm(ModelForm):
    class Meta:
        model = Applicant
        fields = [
            "household",
            "first_name",
            "last_name"
        ]

class HouseHoldForm(ModelForm):
    class Meta:
        model = HouseHold
        fields = [
            'name',
            'application',
            'no_of_dependents'
        ]

    def __init__(self, application_id=None, *args, **kwargs):
        super(HouseHoldForm, self).__init__(*args, **kwargs)
        self.fields['name'].label = 'House Hold Name'
        if application_id:
            self.fields['application'].initial = application_id
            self.fields['application'].widget = HiddenInput()


ApplicantFormset = inlineformset_factory(
    HouseHold, Applicant, fields=('household', 'first_name', 'last_name'), can_delete=False, extra=1, validate_min=True, min_num=1)

views.py

class HouseHoldCreateView(LoginRequiredMixin, generic.CreateView):
    model = models.HouseHold
    template_name = "households/household_create.html"
    form_class = HouseHoldForm

    def get_parent_model(self):
        application = self.kwargs.get('application_pk')
        return application

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.request.POST:
            context['application'] = models.HouseHold.objects.filter(application_id=self.kwargs['application_pk']).last()
            context['house_hold_formset'] = ApplicantFormset(self.request.POST, instance=self.object)
        else:
            context['application'] = models.Application.objects.get(id=self.kwargs['application_pk'])
            context['house_hold_formset'] = ApplicantFormset()
        return context

    def get_form_kwargs(self):
        kwargs = super(HouseHoldCreateView, self).get_form_kwargs()
        print(kwargs)
        kwargs['application_id'] = self.kwargs.get('application_pk')
        return kwargs
    
    def form_valid(self, form):
        context = self.get_context_data()
        applicants = context['house_hold_formset']
        with transaction.atomic():
            self.object = form.save()
            if applicants.is_valid():
                applicants.instance = self.object
                applicants.save()
        return super(HouseHoldCreateView, self).form_valid(form)

    def get_success_url(self):
        if 'addMoreApplicants' in self.request.POST:
            return reverse('service:household-create', kwargs={'application_pk': self.object.application.id})
        return reverse('service:household-list', kwargs={'application_pk': self.object.application.id})

回答1:


I had a similar problem, I solved it by adding the post() method to the view. The example is an UpdateView but the usage is the same. (the indentation is not correct but that's what stackoverflow's editor let me do, imagine all methods are 4 spaces to the right)

class LearnerUpdateView(LearnerProfileMixin, UpdateView):
    model = User
    form_class = UserForm
    formset_class = LearnerFormSet
    template_name = "formset_edit_learner.html"
    success_url = reverse_lazy('home')

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    learner = User.objects.get(learner=self.request.user.learner)
    formset = LearnerFormSet(instance=learner)
    context["learner_formset"] = formset
    return context

def get_object(self, queryset=None):
    user = self.request.user
    return user

def post(self, request, *args, **kwargs):
    self.object = self.get_object()
    form_class = self.get_form_class()
    form = self.get_form(form_class)
    user = User.objects.get(learner=self.get_object().learner)
    formsets = LearnerFormSet(self.request.POST, request.FILES, instance=user)

    if form.is_valid():
        for fs in formsets:
            if fs.is_valid():
                # Messages test start
                messages.success(request, "Profile updated successfully!")
                # Messages test end
                fs.save()
            else:
                messages.error(request, "It didn't save!")
                
        return self.form_valid(form)
    return self.form_invalid(form)

Keep in mind that to save the formset correctly you have to do some heavy lifting in the template as well. I'm referring to the hidden fields which can mess up the validation process. Here's the template corresponding to the view posted above:

<form action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
{{ learner_formset.management_form}}
    {% for form in learner_formset %}
        {% if forloop.first %}
        {% comment %} This makes it so that it doesnt show the annoying DELETE checkbox {% endcomment %}
            {% for field in form.visible_fields %}
                {% if field.name != 'DELETE' %}
                    <label for="{{ field.name }}">{{ field.label|capfirst }}</label>
                    <div id="{{ field.name }}" class="form-group">
                        {{ field }}
                        {{ field.errors.as_ul }}
                    </div>
                {% endif %}
            {% endfor %}
        {% endif %}
        {% for field in form.visible_fields %}
            {% if field.name == 'DELETE' %}
                {{ field.as_hidden }}
            {% else %}
   
                {# Include the hidden fields in the form #}
                {% if forloop.first %}
                    {% for hidden in form.hidden_fields %}
                        {{ hidden }}
                    {% endfor %}
                {% endif %}    
            {% endif %}
        {% endfor %}
    {% endfor %}
<input class="btn btn-success" type="submit" value="Update"/>

Additional reading :

  • https://medium.com/@adandan01/django-inline-formsets-example-mybook-420cc4b6225d
  • Save formset in an UpdateView



回答2:


Inspired by Beikini I have solved it using the create View

class HouseHoldCreateView(LoginRequiredMixin, generic.CreateView):
    model = HouseHold
    template_name = "households/household_create3.html"
    form_class = HouseHoldForm

    def get_parent_model(self):
        application = self.kwargs.get('application_pk')
        return application

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.request.POST:
            context['application'] = HouseHold.objects.filter(
                application_id=self.kwargs['application_pk']).last()
            context['house_hold_formset'] = ApplicantFormset(self.request.POST)
        else:
            context['application'] = Application.objects.get(id=self.kwargs['application_pk'])
            context['house_hold_formset'] = ApplicantFormset()
        return context

    def get_form_kwargs(self):
        kwargs = super(HouseHoldCreateView, self).get_form_kwargs()
        kwargs['application_id'] = self.kwargs.get('application_pk')
        return kwargs

    def form_valid(self, form):
        context = self.get_context_data()
        applicants = context['house_hold_formset']
        application_id = self.kwargs['application_pk']
        household_form = self.get_form()

        if form.is_valid() and applicants.is_valid():
            with transaction.atomic():
                self.object = form.save()
                applicants.instance = self.object
                applicants.save()
                messages.success(self.request, 'Applicant saved successfully')
                return super(HouseHoldCreateView, self).form_valid(form)
        else:
            messages.error(self.request, 'please add an applicant to the household')
            return self.form_invalid(form)

    def get_success_url(self):
        return reverse('service:household-list', kwargs={'application_pk': self.object.application.id})


来源:https://stackoverflow.com/questions/64998608/inlineformset-factory-saving-parent-without-child-and-not-displaying-validation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!