how to submit a form and formset at the same time
Asked Answered
D

3

10

I am trying to render a form and a formset at once. The formset is working fine (i think), but the form is not validating (as if there was no data being posted)

i have tried playing with the button but its main submit function comes through js.

the forms all work independently but not when submitted togetehr so it seem like the problem is in the views here is the code:

views.py

from django.shortcuts import render, render_to_response
from django.http import HttpResponseRedirect
from forms import LessonForm, AddMaterialForm
from models import Lesson, SUBJECT_OPTIONS, Materials, MATERIAL_TYPES
from django.forms.formsets import formset_factory

def Create_Lesson(request):
    AddMaterials=formset_factory(AddMaterialForm, extra=9)
    if request.method == "POST": # If the form has been submitted...
        lesson = LessonForm(request.POST, prefix="lesson") # A form bound to the POST data
        formset = AddMaterials(request.POST, request.FILES) # A form bound to the POST data
        if lesson.is_valid() and formset.is_valid(): # All validation rules pass
            lesson = lesson.save(commit=False)
            lesson.creator = request.user
            lesson.save()
            for form in formset:
                form = form.save(commit=False)
                form.lesson = lesson.pk
                form.save()
            return render(request, 'index.html',)
    else:
        lesson= LessonForm(prefix='lesson') # An unbound form
        formset = AddMaterials()
    return render(request, 'create_lesson/create.html', {
    'form': lesson,'formset':formset
})

.html

    <form id="create_lesson_form" method="post" action="">
    <h2>1: Create Your Lesson</h2>

        {{ form.non_field_errors }}
        <label for="subject"><span>Subject</span></label>
        {{form.subject}}
        {{ form.subject.errors }}
        <label for="title"><span>Title</span></label>
        <input type="text" id="title" name="name" placeholder="Give it a name"/>
        {{ form.name.errors }}
        <label class="error" for="title" id="title_error">You must choose a title!</label>            
        <label for="subtitle"><span>Subtitle</span></label>
        <input type="text" id="subtitle" name="subtitle" placeholder="Type your subtitle here"/>
        {{ form.subtitle.errors }}
        <label class="error" for="subtitle" id="subtitle_error">are you sure you want to leave subtititle blank?</label>
        <label for="description"><span>Description</span></label>
        <textarea id="description" name= "description" cols="42" rows="5" placeholder="why is it important? this can be a longer description"'></textarea>
        {{ form.description.errors }}
        <label class="error" for="description" id="description_error">are you sure you want to leave the description blank?</label>
        <label for="success" id="Goals_title"><span>Goals</span></label>
        <textarea id="success" name="success" cols="42" rows="5" placeholder="explain what sucess might look like for someone doing this lesson...what does mastery look like?" '></textarea>
        {{ form.success.errors }}
        <label class="error" for="success" id="success_error">are you sure you want to leave the goals blank?</label>
    {{ form.directions.errors }}
        <label class="error" for="directions" id="directions_error">are you sure you do not want to include dierections?</label>
    <label for="directions" id="Directions_title"><span>Directions</span></label>
        <textarea id="directions" name="directions" cols="42" rows="5" placeholder="you can add simple directions here" '></textarea><br>
    </form>


    <form id="add_elements_form" method="post" action="">
    {% csrf_token %}
    {{ formset.as_p}}
    <button type='submit' id='finish'>Finish Editing Lesson</button>
    </form>
Defeat answered 1/3, 2013 at 16:10 Comment(0)
C
10

This will submit the form and the formset at the same time.

//When your uploading files or images don't forget to put "multipart/form-data" 
//   in your form. 
//To connect formset in your form, don't forget to put the model in the formset 
//   for instance.
//In this you can add many lines as you want or delete it.

forms.py

class LessonForm(forms.ModelForm):
    class Meta:
        model = Lesson


MaterialsFormset = inlineformset_factory(Lesson, Materials, 
    fields=('field_name', 'field_name'), can_delete=True)

views.py

def view_name(request):
    form = LessonForm()
    formset = MaterialsFormset(instance=Lesson())
    if request.method == 'POST':
        form = LessonForm(request.POST)
        if form.is_valid():
            lesson = form.save()
            formset = MaterialsFormset(request.POST, request.FILES,
                instance=lesson)
            if formset.is_valid():
                formset.save()
                return render(request, 'index.html',)
    return render(request, "page.html", {
        'form': form, 'formset': formset
    })

html

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    {{ formset.as_p }}
    <input type="submit" value="Save"/>
</form>
Castroprauxel answered 2/3, 2013 at 1:6 Comment(0)
C
1

You only need one form tag. If you expect to receive all of the data at the same time, you need to wrap all of the fields with one form tag.

Cyndie answered 1/3, 2013 at 19:44 Comment(3)
i just tried that but it still didn't work... the approach in the next answer seemed to ... but thanksDefeat
You still don't need two separate <form> tags. It's completely un-necessary. Without posting your models, it's impossible to say whether or not you have the right encoding on your form. If that's what the issue was, you should revise your question to be more accurate. If you notice, the answer you accepted only uses one form tag.Cyndie
thanks... you are correct and i fixed that,... but it wasnt enough, I think the main problem was that I did not include.. enctype="multipart/form-data"Defeat
S
1

Now that django 4 is out it's possible to do the same thing within the form itself using the Reusable templates. I prefer this solution because it is then simpler to reuse complex forms without messing with the views.

For the record, here is how I do it

The 2 related models:

# models.py
from django.db import models

class Recipe(models.Model):
    name = models.CharField(max_length=100)
    def __str__(self):
        return self.name

class Ingredient(models.Model):
    name = models.CharField(max_length=100)
    quantity = models.FloatField()
    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")
    def __str__(self):
        return self.name

The forms bound together:

# forms.py
from django import forms
from .models import Recipe, Ingredient

class IngredientForm(forms.ModelForm):
    class Meta:
        model = Ingredient
        exclude = ('recipe',)

IngredientFormSet = forms.inlineformset_factory(Recipe, Ingredient, form=IngredientForm)

class RecipeForm(forms.ModelForm):
    class Meta:
        model = Recipe
        fields = '__all__'

    template_name = 'recipe_form.html'

    def __init__(self, *args, **kwargs):
        """Initialize the formset with its fields."""
        self.formset = IngredientFormSet(*args, **kwargs)
        super().__init__(*args, **kwargs)

    def get_context(self):
        """Add the formset to the context for rendering."""
        context = super().get_context()
        context['formset'] = self.formset
        return context

    def save(self, commit=True):
        """Bind both models together."""
        instance = super().save(commit=False)
        self.formset.instance = instance
        if self.formset.is_valid():
            instance.save()
            self.formset.save()
        return instance

The template for the form:

<!-- templates/recipe_form.html -->
<p>Recipe: {{ form.name }}</p> <!-- calling "form" creates a rendering recursion -->
<p>Ingredients:</p>
{{ formset.management_form }}
<ul>
{% for elt in formset %}
  <li>{{ elt }}</li>
{% endfor %}
</ul>

And the view using it:

# views.py
from django.views.generic import TemplateView
from .forms import RecipeForm

class RecipeView(TemplateView):
    template_name = 'recipe.html'
    def get_context_data(self):
        context = super().get_context_data()
        context['form'] = RecipeForm()
        return context

    def post(self, *args, **kwargs):
        form = RecipeForm(self.request.POST)
        if form.is_valid():
            form.save()
        else:
            raise Exception('Something bad happened!')
        return self.get(*args, **kwargs)

With a very basic template:

<!-- templates/recipe.html -->
<form action="." method="post">
    {% csrf_token %}
    {{ form }}
    <button type="submit">Submit</button>
</form>

And finally you're good to go:

# tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Recipe

class TestFormSet(TestCase):
    def test_new_recipe(self):
        data = {
            "name": "quiche",
            "ingredients-TOTAL_FORMS": 3,
            "ingredients-INITIAL_FORMS": 0,
            "ingredients-0-name": 'bacon bits',
            "ingredients-0-quantity": 200,
            "ingredients-1-name": 'eggs',
            "ingredients-1-quantity": 4,
            "ingredients-2-name": 'cream',
            "ingredients-2-quantity": 150,
        }
        r = self.client.post(reverse('recipe'), data=data)
        self.assertEqual(Recipe.objects.first().ingredients.count(),3)
$ python manage.py test
OK

Hopefully it will be useful to somebody.

Strigil answered 2/2, 2022 at 15:27 Comment(2)
As a bonus, you can use the same template and form_class for an UpdateView.Strigil
This was perfect! Thanks! BTW - it can be done w. Django 3.2 if you are willing to reference form.formset in the template, instead of just formset, and the reusable template isn't there, you have link the template via the view.Thermodynamic

© 2022 - 2024 — McMap. All rights reserved.