0

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})
Josheee
  • 143
  • 1
  • 1
  • 7

2 Answers2

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 :

Beikeni
  • 832
  • 7
  • 17
  • Thank you very much for your answer - I have solved it and took a lot of inspiration from your answer. – Josheee Nov 27 '20 at 00:15
0

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})
Josheee
  • 143
  • 1
  • 1
  • 7