Create multiple objects in one form Django

I am trying to create a form in Django that can create one Student object with two Contact objects in the same form. The second Contact object must be optional to fill in (not required).

Schematic view of the objects created in the single form:

          Contact 1
Student <
          Contact 2 (not required)

I have the following models in models.py:

class User(AbstractUser):
    is_student = models.BooleanField(default=False)
    is_teacher = models.BooleanField(default=False)

class Student(models.Model):
    ACCOUNT_STATUS_CHOICES = (
            ('A', 'Active'),
            ('S', 'Suspended'),
            ('D', 'Deactivated'),
        )

    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    year = models.ForeignKey(Year, on_delete=models.SET_NULL, null=True)
    school = models.ForeignKey(School, on_delete=models.SET_NULL, null=True)
    student_email = models.EmailField() # named student_email because email conflicts with user email
    account_status = models.CharField(max_length=1, choices=ACCOUNT_STATUS_CHOICES)
    phone_number = models.CharField(max_length=50)
    homework_coach = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, blank=True, default='')
    user = models.OneToOneField(User, on_delete=models.CASCADE, null=True)
    plannings = models.ForeignKey(Planning, on_delete=models.SET_NULL, null=True)
  
    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Contact(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    contact_first_name = models.CharField(max_length=50)
    contact_last_name = models.CharField(max_length=50)
    contact_phone_number = models.CharField(max_length=50)
    contact_email = models.EmailField()
    contact_street = models.CharField(max_length=100)
    contact_street_number = models.CharField(max_length=10)
    contact_zipcode = models.CharField(max_length=30)
    contact_city = models.CharField(max_length=100)

    def __str__(self):
        return f"{self.contact_first_name} {self.contact_last_name}"

In forms.py, I have created two forms to register students and contacts. A student is also connected to a User object for login and authentication, but this is not relevant. Hence, when a user is created, the user is defined as the user.

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django.db import transaction
from .models import Student, Teacher, User, Year, School, Location, Contact


class StudentSignUpForm(UserCreationForm):
    ACCOUNT_STATUS_CHOICES = (
        ('A', 'Active'),
        ('S', 'Suspended'),
        ('D', 'Deactivated'),
    )

    #student
    first_name = forms.CharField(max_length=50, required=True)
    last_name = forms.CharField(max_length=50, required=True)
    year = forms.ModelChoiceField(queryset=Year.objects.all(), required=False)
    school = forms.ModelChoiceField(queryset=School.objects.all(), required=False) # not required for new schools / years that are not yet in the database
    student_email = forms.EmailField(required=True)
    account_status = forms.ChoiceField(choices=ACCOUNT_STATUS_CHOICES)
    phone_number = forms.CharField(max_length=50, required=True)
    homework_coach = forms.ModelChoiceField(queryset=Teacher.objects.all(), required=False)

    class Meta(UserCreationForm.Meta):
        model = User
        fields = (
            'username',
            'first_name',
            'last_name',
            'year',
            'school',
            'student_email',
            'account_status',
            'phone_number',
            'homework_coach',
            'password1',
            'password2',
            )

    @transaction.atomic
    def save(
        self, 
        first_name, 
        last_name, 
        year, 
        school, 
        student_email, 
        account_status, 
        phone_number, 
        homework_coach, 
        ):

        user = super().save(commit=False)
        user.is_student = True
        user.save()
        Student.objects.create( # create student object
            user=user,
            first_name=first_name,
            last_name=last_name,
            year=year,
            school=school,
            student_email=student_email,
            account_status=account_status,
            phone_number=phone_number,
            homework_coach=homework_coach
        )
        
        return user

class ContactForm(forms.ModelForm):
    contact_first_name = forms.CharField(max_length=50, required=True)
    contact_last_name = forms.CharField(max_length=50, required=True)
    contact_phone_number = forms.CharField(max_length=50, required=False)
    contact_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact_street = forms.CharField(max_length=100, required=False)
    contact_street_number = forms.CharField(max_length=10, required=False)
    contact_zipcode = forms.CharField(max_length=10, required=False)
    contact_city = forms.CharField(max_length=100, required=False)

    class Meta:
        model = Contact
        fields = '__all__'

In views.py, I have created a view that saves the data (so far only student data, not contact data).

class StudentSignUpView(CreateView):
    model = User
    form_class = StudentSignUpForm
    template_name = 'registration/signup_form.html'

    def get_context_data(self, **kwargs):
        kwargs['user_type'] = 'student'
        return super().get_context_data(**kwargs)

    def form_valid(self, form):
        # student
        first_name = form.cleaned_data.get('first_name')
        last_name = form.cleaned_data.get('last_name')
        year = form.cleaned_data.get('year')
        school = form.cleaned_data.get('school')
        student_email = form.cleaned_data.get('student_email')
        account_status = form.cleaned_data.get('account_status')
        phone_number = form.cleaned_data.get('phone_number')
        homework_coach = form.cleaned_data.get('email')

        user = form.save(
            # student
            first_name=first_name, 
            last_name=last_name, 
            year=year,
            school=school,
            student_email=student_email,
            account_status=account_status,
            phone_number=phone_number,
            homework_coach=homework_coach,
            )
        
        login(self.request, user)
        return redirect('home')

And in registration/signup_form.html, the template is as follows:

{% block content %} {% load crispy_forms_tags %}

<form method="POST" enctype="multipart/form-data">
    {{ formset.management_data }}
    {% csrf_token %}
    {{formset|crispy}}
    <input type="submit" value="Submit">
</form>
{% endblock %}

Urls.py:

from .views import StudentSignUpView

urlpatterns = [
    path('', views.home, name='home'),
    path('signup/student/', StudentSignupView.as_view(), name='student_signup'),
]

How can I create one view that has one form that creates 1 Student object and 2 Contact objects (of which the 2nd Contact is not required)?

Things I have tried:

Using formsets to create multiple contacts at once, but I only managed to create multiple Contacts and could not manage to add Students to that formset.

I added this to views.py:

def formset_view(request):
    context={}

    # creating the formset
    ContactFormSet = formset_factory(ContactForm, extra = 2)
    formset = ContactFormSet()

    # print formset data if it is valid
    if formset.is_valid():
        for form in formset:
            print(form.cleaned_data)

    context['formset']=formset
    return render(request, 'registration/signup_form.html', context)

Urls.py:

urlpatterns = [
    path('', views.home, name='home'),
    path('signup/student/', views.formset_view, name='student_signup'),
]

But I only managed to create multiple Contacts and was not able to add a Student object through that form. I tried creating a ModelFormSet to add fields for the Student object, but that did not work either.

You’re doing way too much manual work here. It’s a whole lot easier than what you’re trying to do.

First:

Right idea, but don’t add the Student to the formset. Limit the formset to Contact only.

What I would recommend:
Create ModelForm for each of User and Student, and a model formset for Contact.

Make sure you use the prefix attribute for each of these to ensure they are handled cleanly within Django.

Yes, you will need to render, check is_valid, and save all three forms individually - but it ends up being a whole lot easier overall.

1 Like

Thanks a lot for your quick reply! I have tried to implement your tips, also inspired by the following stackoverflow answer: Multiple forms with one single create view in Django - Stack Overflow

I created the following view in views.py:

def signup_view(request):
    if request.method == 'POST':
        student_signup_form = StudentSignUpForm(request.POST, request.FILES, prefix='student')
        ContactFormSet = formset_factory(ContactForm, extra = 2) #  creates two ContactForms
        contact_formset = ContactFormSet(request.POST, request.FILES, prefix='contact')
        if all([student_signup_form.is_valid(), contact_formset.is_valid()]):
            student = student_signup_form.save()
            contact = contact_formset.save(commit=False)
            contact.form = student
            contact.save()
            return redirect('home')
    else:
        student_signup_form = StudentSignUpForm(prefix='student')
        ContactFormSet = formset_factory(ContactForm, extra = 2)
        ContactFormSet(request.POST, request.FILES, prefix='contact')
    return render(request, 'registration/signup_form.html', {'student_signup_form': student_signup_form, 'contact_form': ContactFormSet})

This creates 2 ContactForms as a formset using the prefix attribute like you mentioned.

In Forms.py, I have created two forms, one for User and Student and one for Contact:

class StudentSignUpForm(UserCreationForm):
    ACCOUNT_STATUS_CHOICES = (
        ('A', 'Active'),
        ('S', 'Suspended'),
        ('D', 'Deactivated'),
    )

    first_name = forms.CharField(max_length=50, required=True)
    last_name = forms.CharField(max_length=50, required=True)
    year = forms.ModelChoiceField(queryset=Year.objects.all(), required=False)
    school = forms.ModelChoiceField(queryset=School.objects.all(), required=False) # not required for new schools / years that are not yet in the database
    student_email = forms.EmailField(required=True)
    account_status = forms.ChoiceField(choices=ACCOUNT_STATUS_CHOICES)
    phone_number = forms.CharField(max_length=50, required=True)
    homework_coach = forms.ModelChoiceField(queryset=Teacher.objects.all(), required=False)

    class Meta(UserCreationForm.Meta):
        model = User
        fields = (
            'username',
            'first_name',
            'last_name',
            'year',
            'school',
            'student_email',
            'account_status',
            'phone_number',
            'homework_coach',
            'password1',
            'password2',
            )

    @transaction.atomic
    def save(
        self, 
        first_name, 
        last_name, 
        year, 
        school, 
        student_email, 
        account_status, 
        phone_number, 
        homework_coach, 
        ):

        user = super().save(commit=False)
        user.is_student = True
        user.save()
        
        Student.objects.create( # create student object
            user=user,
            first_name=first_name,
            last_name=last_name,
            year=year,
            school=school,
            student_email=student_email,
            account_status=account_status,
            phone_number=phone_number,
            homework_coach=homework_coach
        )
        
        return user

class ContactForm(ModelForm):
    contact_first_name = forms.CharField(max_length=50, required=True)
    contact_last_name = forms.CharField(max_length=50, required=True)
    contact_phone_number = forms.CharField(max_length=50, required=False)
    contact_email = forms.EmailField(required=False) # not required because some students might not know contact information
    contact_street = forms.CharField(max_length=100, required=False)
    contact_street_number = forms.CharField(max_length=10, required=False)
    contact_zipcode = forms.CharField(max_length=10, required=False)
    contact_city = forms.CharField(max_length=100, required=False)

    class Meta:
        model = Contact
        fields = '__all__'
        exclude = ('student',)

It now renders all the fields for Student, User (the passwords and username) and Contact 1 and Contact 2, however, when submitted, nothing is saved in the database now. Do you know the cause of this? Thanks a lot!

If you’re not getting any error messages in the runserver console, then the most likely issue is that one (or more) of your forms are not valid.

There are a couple of items to mention here:

  • If you’re not handling files being uploaded through the forms, there’s no need to include request.FILES in the form bindings. (This isn’t a problem, just helps reduce “code clutter”.)

  • ContactForm is a ModelForm, but you don’t have ContactFormSet defined as a ModelFormset (it should be)

  • In your if block, you’re needing to set the value of student on each form within the formset. In this type of case, it is appropriate to iterate over the individual forms to set the appropriate fields for each form. See Creating forms from models | Django documentation | Django.

  • In your render, you should be rendering the instance of the formset (e.g. contact_formset) and not the formset class.

1 Like

Thanks so much again for your quick response! I have adapted 3 of your 4 points (I think), but I did not quite understand the 2nd point. Where can i define ContactFormSet as a ModelFormSet?

Here is the code I adapted according to your points:

def signup_view(request):
    if request.method == 'POST':
        student_signup_form = StudentSignUpForm(request.POST, prefix='student')
        ContactFormSet = formset_factory(ContactForm, extra = 2) #  creates two ContactForms
        contact_formset = ContactFormSet(request.POST, prefix='contact')
        if all([student_signup_form.is_valid(), contact_formset.is_valid()]):
            student = student_signup_form.save()
            
            contacts = contact_formset.save(commit=False)
            for contact in contacts:
                contact.form = student
                contact.save()

            return redirect('home')
    else:
        student_signup_form = StudentSignUpForm(prefix='student')
        ContactFormSet = formset_factory(ContactForm, extra = 2)
        ContactFormSet(request.POST, prefix='contact')
    return render(request, 'registration/signup_form.html', {'student_signup_form': student_signup_form, 'contact_form': contact_formset})

One reason why the contact form could not be valid is that the contact model also requires a student field. I included the Contact Model for reference.

class Contact(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    contact_first_name = models.CharField(max_length=50)
    contact_last_name = models.CharField(max_length=50)
    contact_phone_number = models.CharField(max_length=50)
    contact_email = models.EmailField()
    contact_street = models.CharField(max_length=100)
    contact_street_number = models.CharField(max_length=10)
    contact_zipcode = models.CharField(max_length=30)
    contact_city = models.CharField(max_length=100)

    def __str__(self):
        return f"{self.contact_first_name} {self.contact_last_name}"

And the ContactForm does not include a student field, which may cause the form to be incomplete compared to the model. I tried to test this hypothesis by deleting the student field in the model, but still, nothing was saved to the database.

See the docs for model formsets

The ModelForm (or formset) save method returns a list of the model instances being saved. This means that contacts are an iterable of Contact, and the individual contact in contacts are instances of the Contact model.

You do not have a field named form in the Contact model.

I changed contact.form into contact.student. Still, the objects are not saved in the database. Why would the form still not be valid?

Have you also changed this to use a modelformset instead of a standard formset?

Answered in the docs Creating forms from models | Django documentation | Django .

No way to tell from here yet. From what I’m still seeing from what you have posted, you don’t have a working model formset yet.
Also, I don’t know what your current templates look like, so I can’t tell whether you’re rendering any errors that may be caused by the forms.

You could add some print statements in your code to print any important data or error messages on the console. Or you could use a debugger and put a breakpoint in your code at the if statement to see what’s happening there.

For more information on this, see:

I still don’t quite understand where I can change this. Could you give me a code example of where to change this into a modelformset?

Where do you create the formset?

Change that code such that you’re creating a model formset instead.

This is where I am creating the formset:

ContactFormSet = formset_factory(ContactForm, extra = 2) #  creates two ContactForms
contact_formset = ContactFormSet(request.POST, prefix='contact')

I changed it into the following:

ContactFormSet = modelformset_factory(ContactForm, fields=('contact_first_name', 'contact_last_name', 'contact_phone_number', 'contact_email', 'contact_street', 'contact_street_number', 'contact_zipcode', 'contact_city'), extra = 2) #  creates two ContactForms
contact_formset = ContactFormSet(request.POST, prefix='contact')

I had to add the fields because it was ‘explicitly required’, but now I get the following error:

AttributeError: 'ModelFormOptions' object has no attribute 'private_fields'

What is the first parameter that you need to pass in the modelformset_factory function?

It works now! Thanks so much for your help!